[译]guava RateLimiter原理解析(updating)

原文:
https://github.com/google/guava/blob/HEAD/guava/src/com/google/common/util/concurrent/SmoothRateLimiter.java

RateLimiter是怎样设计的?为什么要这样设计?
guava提供的限流器RateLimiter的主要特点就是它能提供稳定的速率,在通常情况下,按照指定的最大速率处理请求。为了实现这个功能,需要按照配置的速率限制外部请求。比如,对每个外部请求,会计算出一个合适的阻塞时间,让请求线程等待一段时间后才能接受处理。
为了保持稳定的QPS,最简单的方法就是记录最后一次请求的时间戳,确保经过(1/QPS)秒后,才处理下一个请求。例如,指定QPS=5(每秒发放5个令牌),那么,只要保证发放令牌的时间间隔不小于200ms,就能实现指定的处理速度。如果第一个令牌发放100ms后,新的请求进入,那么让这个新的请求等待100ms。按照这个处理速度,假如15个请求进入,就需要3s处理完成。
但是要注意,像这样实现的限流器有一个问题,它对系统之前处理请求的记录是不完整的,它只记录了之前最后一次请求的信息。如果之前很长一段时间没有外部请求进入,突然进来个请求,是不是一定要立即处理这个请求(授予一个令牌)呢?这种限流器不知道之前请求的处理速度,可能现在系统处理能力是不饱和的,可以接受新的请求,也可能系统已经满载了,不能接受新的请求,这取决于现实中系统对于这种不稳定的突变流量的处理能力。
如果系统处理能力没有充分利用,也就意味着系统有剩余的资源,可以处理新的请求。那么,对外部请求的处理速度可以加快一些,这样可以充分利用系统资源。特别是对于有带宽限制的网络请求,未充分利用的系统资源在这种场景可以理解为空的缓冲区,可以立即提供给新的网络请求。
另一方面,如果系统资源使用已经饱和,也就意味着系统目前没有准备好接受下一次请求,比如缓存已经过期,还没来得及更新,处理新的请求可能引起大量的系统开销(一个极端的例子,系统刚刚启动,资源没有加载完成,没有准备好处理外部请求)。
为了处理以上这些情况,我们的限流器需要增加一个新的维度:系统使用率,这个维度使用存储的令牌数storedPermits来量化,如果令牌数等于0,就表示系统忙,没有空余资源处理新的请求,反之,如果令牌数增长到最大值maxStoredPermits,就意味着系统有充足的资源处理请求。调用acquire(permits)获取令牌,可以从以下获取:

  • 保存的闲置令牌
  • 新生成的令牌

举例来说:
限流器每秒生成一个令牌,每经过一秒,如果没有请求要处理,那么storedPermits会增加1,如果过去10s都没有请求进来,那么闲置令牌数storedPermits就会增长到10个(假设最大令牌数maxStoredPermits>=10),假设在这个时间点,有一个请求要求获取3个令牌(acquire(3)),我们从闲置的令牌中取出3个就可以满足,闲置令牌数storedPermits减少到7个,紧接着,另一个请求需要获取10个令牌(acquire(10)),先从闲置令牌中取出余下的7个,同时还要新生成3个令牌。
通过上面的例子可以得出,如果生成令牌的速度是每秒一个,那么获取3个令牌需要等待3秒,如果此时需要7个令牌呢?那么答案就不是唯一了,如果我们仅仅关注系统资源没有充分利用,那么就应该优先立即使用闲置的令牌,因为新生成令牌需要时间,导致系统资源闲置了。但是如果我们优先考虑系统资源是不是饱和了,能不能应对突发的大量请求,这种情况下,就应该优先使用新生成的令牌,等待的时间长了,但是可以保证系统平缓的加压。综上所述,我们需要一个不同于以上两种极端的方案,兼顾响应速度和系统压力,需要将闲置令牌数storedPermits转换成等待时间。

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @sxzhou Oct 11, 2017

奉献爱心