流量控制实施

任何系统的请求量都有一个上限,一旦超过上限,系统就会发生瘫痪。要想防止请求量过大,可以对请求量进行限制。可以限制单位时间内的请求次数,也可以限制每个业务的请求频率。

可以通过信号量限制流量

1、可以对超载的那部分流量直接丢弃

Semaphore semphore = new Semaphore(10);  
  if(semphore.getQueueLength() > 10){  
    //等待队列阀值为10时  
    return;  
  }  
  try {  
    semphore.acquire();  
      
    //干活  
      
} catch (InterruptedException e) {  
    e.printStackTrace();  
}finally{  
    semphore.release();//释放  
}  

2、可以对超载的流量的部分请求加载到等待队列

  

  Semaphore semphore = new Semaphore(10);  
  if(semphore.getQueueLength() > 10){  
    //等待队列阀值为10时  
    return;  
  }  
  try {  
    semphore.acquire();  
      
    //干活  
      
} catch (InterruptedException e) {  
    e.printStackTrace();  
}finally{  
    semphore.release();//释放  
}

可以通过异步请求来分担压力

  可以通过ActiveMQ或者RabbitMQ来实现异步请求。

分布式常用限流算法

计数器

计数器是最简单粗暴的算法。比如某个服务最多只能每秒钟处理100个请求。我们可以设置一个1秒钟的滑动窗口,窗口中有10个格子,每个格子100毫秒,每100毫秒移动一次,每次移动都需要记录当前服务请求的次数。内存中需要保存10次的次数。可以用数据结构LinkedList来实现。格子每次移动的时候判断一次,当前访问次数和LinkedList中最后一个相差是否超过100,如果超过就需要限流了。

漏桶算法

漏桶算法即leaky bucket是一种非常常用的限流算法,可以用来实现流量整形(Traffic Shaping)和流量控制(Traffic Policing)。贴了一张维基百科上示意图帮助大家理解:

漏桶算法的主要概念如下:

  • 一个固定容量的漏桶,按照常量固定速率流出水滴;

  • 如果桶是空的,则不需流出水滴;

  • 可以以任意速率流入水滴到漏桶;

  • 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。

漏桶算法比较好实现,在单机系统中可以使用队列来实现(.Net中TPL DataFlow可以较好的处理类似的问题,你可以在这里找到相关的介绍),在分布式环境中消息中间件或者Redis都是可选的方案。

令牌桶算法

令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:

  • 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个。
  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝。
  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上。
  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

令牌算法是根据放令牌的速率去控制输出的速率,也就是上图的to network的速率。to network我们可以理解为消息的处理程序,执行某段业务或者调用某个RPC。

Nginx限流

对于Nginx接入层限流可以使用Nginx自带了两个模块:连接数限流模块ngx_http_limit_conn_module和漏桶算法实现的请求限流模块ngx_http_limit_req_module。

1. ngx_http_limit_conn_module

我们经常会遇到这种情况,服务器流量异常,负载过大等等。对于大流量恶意的攻击访问,会带来带宽的浪费,服务器压力,影响业务,往往考虑对同一个ip的连接数,并发数进行限制。ngx_http_limit_conn_module 模块来实现该需求。该模块可以根据定义的键来限制每个键值的连接数,如同一个IP来源的连接数。并不是所有的连接都会被该模块计数,只有那些正在被处理的请求(这些请求的头信息已被完全读入)所在的连接才会被计数。

我们可以在nginx_conf的http{}中加上如下配置实现限制:

#限制每个用户的并发连接数,取名one
limit_conn_zone $binary_remote_addr zone=one:10m;

#配置记录被限流后的日志级别,默认error级别
limit_conn_log_level error;
#配置被限流后返回的状态码,默认返回503
limit_conn_status 503;

然后在server{}里加上如下代码:

#限制用户并发连接数为1
limit_conn one 1;

另外刚才是配置针对单个IP的并发限制,还是可以针对域名进行并发限制,配置和客户端IP类似。

#http{}段配置
limit_conn_zone $ server_name zone=perserver:10m;
#server{}段配置
limit_conn perserver 1;

2. ngx_http_limit_req_module

上面我们使用到了ngx_http_limit_conn_module 模块,来限制连接数。那么请求数的限制该怎么做呢?这就需要通过ngx_http_limit_req_module 模块来实现,该模块可以通过定义的键值来限制请求处理的频率。特别的,可以限制来自单个IP地址的请求处理频率。 限制的方法是使用了漏斗算法,每秒固定处理请求数,推迟过多请求。如果请求的频率超过了限制域配置的值,请求处理会被延迟或被丢弃,所以所有的请求都是以定义的频率被处理的。

在http{}中配置

#区域名称为one,大小为10m,平均处理的请求频率不能超过每秒一次。

limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

在server{}中配置

#设置每个IP桶的数量为5
limit_req zone=one burst=5;

服务稳定性

 1、依赖管理

  将多个应用模块进行高度解耦,我们必须详细了解各个模块之间的依赖关系,不至于在紧急时刻快速定位到问题所在。

2、优雅降级

  通过依赖关系我们可以根据当前系统所依赖的服务及系统的流程,判断依赖的服务是否会影响主流程,一次来决定当前应用的优先级。当依赖的应用处理的请求超过一定的阈值,服务处理超时,可以通过降级。此时服务调用者可以跳过该服务的调用,当请求高峰期已过,可以将服务取消降级。

3、服务分级

  对于分布式系统,由于是多个模块组成,我们可以通过各个模块服务与主核心服务的关系来判断服务的优先级,当由于非核心业务的各个模块压力过大无法承载时,可以停用某一个模块来确保主服务的正常运行。

4、开关

  在通过一个应用向外部提供多个服务,当负载较高时,可以将一些非核心链路的调用屏蔽掉,来保证核心服务的正常运行。这时需要一个开关来控制服务提供策略。如下图所示,当应用中的服务消费者A和B依赖于服务提供者1,服务消费者C依赖于服务提供者2,服务消费者D依赖于服务提供者3,服务提供者1、2、3部署在一个应用中,在系统负载过高时,可以屏蔽掉服务消费者B、C、D对应用的服务调用,只允许主服务消费者A的调用,以此来保证主服务的正常运行。

5、应急预案

  在应急预案中定义好遇到的问题,以及相应的处理方式,明确指出哪些服务需要关闭,哪些是车,哪些是帅,一旦出现紧急情况,按照应急预案进行操作。