Go to my github

三高系统关注点——限流该怎么做?

三高系统关注点——限流该怎么做?

来源:https://www.infoq.cn/article/UhixHoWebU_TYJewJwcL

限流其实是一个盾牌,在排兵布阵中盾牌一般在最前面和保护将军的位置上。

一,怎么做【限流】

  1. 通过【压力测试】等方式获得系统的能力上限在那个水平是第一位。
  2. 其次,就是制定干预限流的策略。比如标准怎么定,是否注重结果还是过程平衡性等。
  3. 最后,就是处理“被干预掉”的流量。能不能直接丢弃?不能的话该如何处理?

获得系统的能力上限【压力测试】

一般我们做压测为了获得 2 个结果,「速率」和「并发数」。

  1. 一个时间单位内能够处理的请求数量,比如 xxx 次请求 / 秒。
  2. 同一时刻能处理的最大请求数量,比如 xxx 次的并发。

从指标上需要获得「最大值」、「平均值」或者「中位数」。后续限流策略需要设定的具体标准数值就是从这些指标中来的。

制定干预流量的策略【两窗两桶】

常用的策略就 4 种,我给它起了一个简单的定义——「两窗两桶」。

固定窗口和滑动窗口本质上还是预先划定时间片的方式,属于一种“预测”,意味着几乎肯定无法做到 100% 的物尽其用。

两窗就是:

  • 固定窗口 (限流阈值=平均并发数 *3)

​ 固定窗口就是定义一个“固定”的统计周期,比如 1 分钟或者 30 秒、10 秒这样。然后在每个周期统计当前周期中被接收到的请求数量,经过计数器累加后如果达到设定的阈值就触发「流量干预」。直到进入下一个周期后,计数器清零,流量接收恢复正常状态。

全局变量 int totalCount = 0;  // 有一个「固定周期」会触发的定时器将数值清零。 
if(totalCount > 限流阈值) {
    return; // 不继续处理请求。
}
totalCount++;
// do something...
  • 滑动窗口
    ​ 是增强版的固定窗口,如果设置的周期很短这个滑动窗口也就没有意义了。

    全局数组 链表 []  counterList = new 链表 [切分的滑动窗口数量];
    // 有一个定时器,在每一次统计时间段起点需要变化的时候就将索引 0 位置的元素移除,并在末端追加一个新元素。
    int sum = counterList.Sum();
    if(sum > 限流阈值) {
        return; // 不继续处理请求。
    }
    int 当前索引 = 当前时间的秒数 % 切分的滑动窗口数量 ;
    counterList[当前索引]++; 
    // do something...
    

两桶就是:

  • 漏桶算法

​ 漏桶模式的核心是固定“出口”的速率,不管进来多少量,出去的速率一直是这么多。如果涌入的量多到桶都装不下了,那么就进行「流量干预」。

整个实现过程我们来分解一下。

  1. 水(请求)从上方倒入水桶,从水桶下方流出(被处理);
  2. 来不及流出的水存在水桶中(缓冲),以固定速率流出;
  3. 水桶满后水溢出(丢弃)。
  4. 这个算法的核心是:缓存请求、匀速处理、多余的请求直接丢弃。
  5. 相比漏桶算法,令牌桶算法不同之处在于它不但有一只“桶”,还有个队列,这个桶是用来存放令牌的,队列才是用来存放请求的。

从作用上来说,漏桶和令牌桶算法最明显的区别就是是否允许突发流量(burst)的处理,漏桶算法能够强行限制数据的实时传输(处理)速率,对突发流量不做额外处理;而令牌桶算法能够在限制数据的平均传输速率的同时允许某种程度的突发传输。

代码思路如下:

全局变量 int unitSpeed;  // 出口当前的流出速率。每隔一个速率计算周期(比如 1 秒)会触发定时器将数值清零。
全局变量 int waterLevel; // 当前缓冲区的水位线。
if(unitSpeed < 速率阈值) {
    unitSpeed++;   
    //do something...
}else{
    if(waterLevel > 水位阈值){
        return; // 不继续处理请求。
    }
    waterLevel++;    
    while(unitSpeed >= 速率阈值){
        sleep(一小段时间)。
    }
    unitSpeed++;
    waterLevel--;
    //do something...
}

这样一来,你会发现本质就是:通过一个缓冲区将不平滑的流量“整形”成平滑的(高于均值的流量暂存下来补足到低于均值的时期),以此最大化计算处理资源的利用率。

Nginx配置如下:

Nginx按请求速率限速模块使用的是漏桶算法,即能够强行保证请求的实时处理速度不会超过设置的阈值。

Nginx官方版本限制IP的连接和并发分别有两个模块:

  • limit_req_zone 用来限制单位时间内的请求数,即速率限制,采用的漏桶算法 "leaky bucket"。
  • limit_req_conn 用来限制同一时间连接数,即并发限制。

例子:

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    server {
        location /search/ {
            limit_req zone=one burst=5 nodelay;
        }
}      
  • 令牌桶

令牌桶模式的核心是固定“进口”速率。先拿到令牌,再处理请求,拿不到令牌就被「流量干预」。因此,当大量的流量进入时,只要令牌的生成速度大于等于请求被处理的速度,那么此刻的程序处理能力就是极限。

也来分解一下它的实现过程。

  1. 控制令牌生成的速率,并放入桶中。这个其实就是单独一个线程在不断的生成令牌。
  2. 控制桶中待领取的令牌水位不超过最大水位。这个和「漏桶」一样,就是一个全局计数器,进行加加减减。

大致的代码简化表示如下(看上去像「固定窗口」的反向逻辑):

全局变量 int tokenCount = 令牌数阈值 ; // 可用令牌数。有一个独立的线程用固定的频率增加这个数值,但不大于「令牌数阈值」。

if(tokenCount == 0){
    return; // 不继续处理请求。
} 
tokenCount--; 
//do something...

聪明的你可能也会想到,这样一来令牌桶的容量大小理论上就是程序需要支撑的最大并发数。的确如此,假设同一时刻进入的流量将令牌取完,但是程序来不及处理,将会导致事故发生。

所以,没有真正完美的策略,只有合适的策略。因此,根据不同的场景能够识别什么是最合适的策略是更需要锻炼的能力。下面 z 哥分享一些我个人的经验。

二,做【限流】的最佳实践

四种策略该如何选择?

首先,固定窗口。一般来说,如非时间紧迫,不建议选择这个方案,太过生硬。但是,为了能快速止损眼前的问题可以作为临时应急的方案。

其次,滑动窗口。这个方案适用于对异常结果「高容忍」的场景,毕竟相比“两窗”少了一个缓冲区。但是,胜在实现简单。

然后,漏桶。z 哥觉得这个方案最适合作为一个通用方案。虽说资源的利用率上不是极致,但是「宽进严出」的思路在保护系统的同时还留有一些余地,使得它的适用场景更广。

最后,令牌桶。当你需要尽可能的压榨程序的性能(此时桶的最大容量必然会大于等于程序的最大并发能力),并且所处的场景流量进入波动不是很大(不至于一瞬间取完令牌,压垮后端系统)。

nginx限流:https://www.cnblogs.com/biglittleant/p/8979915.html

  • ngx_http_limit_conn_module
  • ngx_http_limit_req_module
posted @ 2019-01-04 11:25  峡谷少爷  阅读(424)  评论(0)    收藏  举报