限流算法分析

1,477 阅读6分钟

项目中用到了Sentinel,因为在接触Sentinel中的WarmUp限流算法时,不是很明白其实现原理,所以接触了Guava的RateLimiter(Sentinel借鉴了该算法)。简单记录下来,方便自己以后复习,有不对的地方请大佬指出

固定窗口

类似于计数器,比如对1s内的请求进行计数,如果设置阈值未100,则对超过100个请求进行限流。有两个问题:

  1. 突刺问题
  2. 不够平滑:假如1s前100ms来了100个请求,则这100请求通过,但后面900毫秒接口是不可用状态,而这时候接口实际的QPS是1000/s

滑动窗口

将时间周期细分成几个小周期(窗口),请求到来的时候, 统计的是近1s内相关的窗口数据,随着时间的推移,旧窗口销毁,新窗口创建

  1. 可以有效解决突刺问题
  2. 平滑程度取决于时间周期内的窗口数量

漏斗算法

原理就像一个漏斗,流量从漏斗的大口进入,以固定的速率从漏斗的小口进入系统,如果流入速率过快, 超过了漏斗的容量,则丢弃这部分溢出的请求

  1. 平滑
  2. 无法应对突发情况

令牌桶算法

  1. 以固定的速率向桶里面丢令牌
  2. 请求到来的时候,先从桶里获取令牌,如果可以获取则请求通过,否则等待
  3. guava包中RateLimiter类实现了令牌桶算法

RateLimiter

RateLimiter是基于时间间隔的思路来实现实限流的。什么意思?

假如你希望某个接口的的QPS为5,即1s内最多可以通过5个请求,那平均每个请求是的响应时间就是200ms。知道了每个请求需要的时间,就可以计算出一个请求在什么时候结束,或者说下一个请求在什么时候开始。

RateLimiter源码注释中,提到了这样一种思路来实现这种基于时间间隔的限流:

记录上一个请求开始的时间戳,下一个请求必须在1/QPS时间后才可以通过。假如QPS为5,新请求进来的时候,如果距上一个请求不到200ms,则新请求必须等待。等到什么时候?等到距上一个请求有200ms。这种方式有一个问题,如果某个请求过后一段时间内都没有请求,则这段时间系统资源被浪费了,为什么不能在这段时间内生成几个令牌供下次使用呢?

RateLimiter没有使用这种方式,它采用的是一种先消费,后买单的方式。什么意思?

不记录上一次请求的时间,只计算下一次请求对应的时间(nextTime)。当请求过来的时候,判断如果now大于nextTime,则计算nownextTime之间的时间差,然后根据1/QPS计算这段时间内应用生成几个令牌并放到令牌桶里。然后根据你请求的令牌数和桶里面的令牌数对比。假如请求令牌数>桶里令牌数,它会让这次请求通过。然后你预消费的令牌数(请求令牌数-桶里令牌数)计算出一个时间,即:预消费令牌数*(1/QPS)s。然后将该时间加到nextTime上。这样下一个请求到来的时候,首先它会计算出下下个请求发现的nextTime,然后根据请求阻塞等待或返回false。

RateLimiter是一个接口,SmoothRateLimiter实现了该接口,是一个抽象类,它有两个实现类,基于模板模式

RateLimiter

SmoothBurstySmoothWarmingUp 不同的地方在于计算nextTime阶段,更具体一点来说应该是计算从令牌桶里取令牌所需要时间逻辑不一样

  • SmoothBursty:从令牌桶里取令牌不需要时间
  • SmoothWarmingUp:从令牌桶里取令牌需要时间,就是有个预热的过程。这个时间的就是那个图形的面积(长方形面积 + 梯形面积)

  /**
   * This implements the following function where coldInterval = coldFactor * stableInterval.
   *
   *          ^ throttling
   *          |
   *    cold  +                  /
   * interval |                 /.
   *          |                / .
   *          |               /  .   <-- "warmup period" is the area of the trapezoid between
   *          |              /   .       thresholdPermits and maxPermits
   *          |             /    .
   *          |            /     .
   *          |           /      .
   *   stable +----------/  WARM .
   * interval |          .   UP  .
   *          |          . PERIOD.
   *          |          .       .
   *        0 +----------+-------+--------------> storedPermits
   *          0 thresholdPermits maxPermits
  1. X 轴:代表 storedPermits 的数量,即桶里面的令牌数量
  2. Y 轴:不太好描述。当x>thresholdPermits时,Y轴表示梯形的下底,梯形的面积=(上底+下底)*高/2;否则,Y轴代表获取一个令牌需要的时间
  3. 当请求比较少的时候,此时桶里面的令牌必然越堆越多。此时X轴向右边移动,梯形的面积越来越大,代表获取一个请求需要的时间比较长
  4. 当请求越来越多的时候,桶里的令牌肯定被消费的越来越少,此时,X轴向左移动,梯形面积越来越小,代表获取一个请求需要的时间比较短
  5. 当X轴在0 - thresholdPermits之间时,梯形面积为0,此时获取一个请求所需要的时间stableinterval。这说明预热过程完成,之后获取一个令牌所需要的时间比较短,并且是一个固定的时间
  6. 当系统的请求又下降时,此时桶里的令牌越来越多,情况又像上面分析的一样

Sentinel

  1. sentinel的中的QPS统计基于滑动窗口算法
  2. sentinel中的又分了几种限流策略:直接拒绝(直接基于QPS来比较)、匀速排队(类似根据平均响应时间和最大缓冲时间来判断,计算某个请求能不能先不直接拒绝,而是排队等一会)、WarmUP(解决了guava的令牌桶算法)
  3. sentinel中的WarmUP借鉴了guava的令牌桶算法。从上面对RateLimiter的分析我们可以发现,RateLimiter的限流基于时间间隔。在sentinel中,这一秒的QPS已经通过时间窗口算法统计出来了,那我能不能设计这样一种算法,让你允许通过的QPS值缓慢增大?就类似于RateLimiter的一个预热过程?WarmUP就是这样一种算法,我不管你的时间间隔,我只根据令牌桶的令牌数量调整我允许通过的QPS值,听巧妙的,膜拜大佬。怪不得刚在GitHub看sentinel和WarmUP相关的文档时完全看不懂,后面去了解了一下RateLimiter,总算知道作者这段话是什么意思了