使用 Guava 中 RateLimiter 遇到的一个问题
线上业务使用了 Guava 中的 RateLimiter 来做单机的限流。最近运维同事反馈,在修改了限流的配置之后,请求频率没有超过限流的配置却触发了限流逻辑,在此对该问题进行一下分析与总结。
问题模拟
根据上述问题的现象,初步推测问题可能跟 Guava 中 RateLimiter 的限流算法实现有关,为了方便排查问题,参考线上业务的限流的逻辑写了一个 demo,其核心代码如下:
1 |
|
启动工程,我们模拟一下出现问题时的操作。
- 构造 HTTP 请求调用
/setRate
接口,将 loan 对应的限流的值修改为 5; - 构造 HTTP 请求快速调用
/testLimit
接口 2 次,发现第一调用接口返回 SUCCESS, 第二次接口返回 FAIL,说明第 2 次请求触发了限流; - 构造 HTTP 请求调用
/testLimit
5 次,保证 5 次请求在 1 秒内接收,发现这 5 次请求接口均返回 SUCCESS,说明这 5 次请求均未触发限流; - 构造 HTTP 请求调用
/testLimit
6 次,保证 6 次请求均在 1 秒内接收,发现前 6 次请求接口均返回 SUCCESS,说明这 6 次请求均未触发限流; - 构造 HTTP 请求调用
/testLimit
7 次,保证 7 次请求均在 1 秒内接收,发发现前 6 次请求接口均返回 SUCCESS,第 7 次请求接口返回 FAIL,说明前 6 次请求均未触发限流,第 7 次请求触发限流。
至此,我们模拟的现象与线上一致。步骤 3 是我们预期的结果,步骤 2 中在未超过限流配置的情况下触发了限流,步骤 4、5 中的第 6 次请求在超过了限流配置的情况下,仍然允许通过,我们直观上认为这些是不合理的。
排查与分析
Guava 中 RateLimiter 提供了两种令牌桶的算法实现:
- 平滑突发限流(SmoothBursty)
- 平滑预热限流(SmoothWarmingUp)
我们项目中使用了默认的平滑突发限流(SmoothBursty)算法,这里我们只针对该算法进行分析。
tryAcquire 方法
其源码
1 | public boolean tryAcquire(int permits, long timeout, TimeUnit unit) { |
可以观察到只有在 canAcquire 方法返回 false 的情况下,才会返回 false,通过阅读 canAcquire 方法可知该方法跟 SmoothBursty 中的成员变量 nextFreeTicketMicros 有关,该成员变量的释义如下:
The time when the next request (no matter its size) will be granted. After granting a request, this is pushed further in the future. Large requests push this further than small requests.
该成员变量标识下一次请求获得令牌的最早时间,在创建 SmoothBursty 时初始值为 0,并且每次授权一个请求之后,这个值会向后推移。查找该成员变量引用的地方,发现 SmoothBursty 类中 reserveEarliestAvailable、resync 方法会触发该成员变量的更新,并且观察该方法的调用栈,其中就有 RateLimiter#tryAcquire()
方法。
reserveEarliestAvailable、resync 方法
其源码
1 | /** |
在 reserveEarliestAvailable 方法结尾处打断点,来观察一下修改配置之后,第一次调用该方法时对 nextFreeTicketMicros 成员变量的设值情况。如下图:

通过上图中的执行情况可知,在限流配置修改为 5TPS 的情况下,第一次请求进来之后,令牌桶内所存储的令牌桶还不足 1,发生了预支付令牌的情况,此时 nextFreeTicketMicros 成员变量会被赋值为约 200000 微妙(即 200ms),如果第二个请求是在第一个请求之后 200ms 内请求进来的,一定会触发限流逻辑,如下图:

同样的我们提上面的操作中 1 秒内的连续 6 次请求,第 6 次仍未触发限流,这种现象也是由于预支付令牌产生的。在一段时间内没有请求进来的情况下,令牌桶内累计了 5 个令牌,此时前 5 个令牌会被前 5 个请求消费掉,当第 6 个请求进来的时候,时间仍然在向前推进,只是此时桶内累计的令牌数不足 1 个,会触发预先支付令牌,故这种情况下第 6 个请求,仍然是能够通过的。而当第 7 个请求进来的时候,由于下一次请求获得令牌的最早时间已经大于当前时间了,故取令牌失败,触发限流,如下图:

结论
通过以上源码的分析,Gauava 中 RateLimiter 的平滑突发限流(SmoothBursty)是通过以固定的速率向桶内放置令牌,从而达到平滑输出的效果。由于桶内会进行令牌的积累,可以应对一定程度的流量突发。
对于我们线上遇到的修改配置之后第二个请求就触发了限流的问题,我们在切换配置时应该使用 RateLimiter 的 setRate 方法直接修改其限流配置,这样可以避免重新创建一个新的 RateLimiter 时会创建一个新的跑表(Stopwatch),同时也不会将 nextFreeTicketMicros 重新置为 0,使得在取令牌时对桶内令牌数的计算不会像重新创建一个那么极端。
发散
大家也可以看一下 Guava 中平滑预热限流(SmoothWarmingUp)在实现上与的平滑突发限流(SmoothBursty)有什么区别,此外常见的其他限流算法如漏桶算法、计数法分别是怎么实现的。