前言
之前写了一篇文章讲了两种限流算法连ChatGPT都会搞错限流算法,而Guava中的RateLimiter是一种基于令牌桶算法实现的,在行业内使用非常多的限流工具实现,所以我对RateLimiter的源码进行了一定的研究后与大家分享
RateLimiter
在上一篇文章提到了,令牌桶算法有四个重要的属性:容量、速率、时间、产生的令牌数量,RateLimiter是一种令牌桶算法的实现所以也离不开这四个重要属性
- 容量: maxPermits
- 产生的令牌数量: storedPermits
- 产生令牌的时间间隔(速率):stableIntervalMicros
- 下一次可以处理请求的时间(时间):nextFreeTicketMicros
注意:
使用时间间隔代表速率是为了方便计算产生令牌,因为RateLimiter支持系统WarmUp功能,所以使用时间间隔表示速率可以更方便计算warmup期间产生的令牌数量(后面详解)
使用下一次可以处理请求的时间因为RateLimiter是预消费的:可以允许提前消费令牌,先消费令牌,再补充令牌,但只允许一次提前消费,下一次请求需要等待上一次透支的令牌补充完成后才可以消费
/**
* The currently stored permits.
*/
double storedPermits;
/**
* The maximum number of stored permits.
*/
double maxPermits;
/**
* The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
* per second has a stable interval of 200ms.
*/
double stableIntervalMicros;
/**
* 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.
*/
private long nextFreeTicketMicros = 0L; // could be either in the past or future
RateLimiter两种实现
Ratelimiter有两种实现,SmoothBursty(突发限流器)和SmoothWarmingUp(预热限流器),两者的区别在于当系统闲置一段时间后桶内堆积了大量令牌的情况下突然面对大量的请求如何放行
SmoothBursty(突发限流器):
SmoothBursty是只要桶内有令牌就直接放行,也就是说系统会直接面对这突发的大量请求
SmoothWarmingUp(预热限流器):
SmoothWarmingUp则是认为系统在闲置一段时间后可能会存在缓存大量失效的情况,把这种情况下的系统的状态称为cool冷却状态,这种情况下系统不能达到最大的性能,如果直接面对大量的请求可能存在系统崩溃的状态,所以应该存在一个预热状态,也就是逐渐
增多放行的请求数量,给系统一个缓冲时间
下面分别介绍具体实现
SmoothBursty
创建create
RateLimiter提供了一个静态的创建方法create(double permitsPerSecond),需要传入参数permitsPerSecond(每秒产生的令牌数量)来计算速率,这种创建方式采用的实现是SmoothBursty
创建的实现过程:
- 创建SmoothBursty对象,传入的第二个参数是maxBurstSeconds(达到最大容量需要的时间)结合permitsPerSecond(每秒产生的令牌数量)则可以计算最大容量
public static RateLimiter create(double permitsPerSecond) {
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
@VisibleForTesting
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */); // maxBurstSeconds 用于计算 maxPermits
rateLimiter.setRate(permitsPerSecond); // 设置生成令牌的速率
return rateLimiter;
}
-
通过setRate给RateLimiter设置速率
- 计算stableIntervalMicros(一块令牌所需的时间间隔代表速率)
- 初始化storedPermits(当前存储的令牌数量)为最大值
注意:
为什么doSetRate需要在一开始调用resync惰性计算当前限流器内的令牌数呢,是因为RateLimiter支持在使用过程中调整速率的情况
比如我们之前permitsPerSecond是2,现在我们系统升级了需要调整permitsPerSecond为5,他则会按照已有令牌数量以及新旧速率的比例来计算storedPermits(当前的令牌数)=storedPermits*(newRate/oldRate),因为达到最大容量的时间都是一样的所以最大容量的比例则为速率的比例
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
resync(nowMicros); // 惰性计算当前限流器内的令牌数,为什么这个地方要调用resync下面会说
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond; // 通过permitsPerSecond计算产生一块令牌所需的时间间隔
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros); //调用SmoothBursty实现类中的具体实现
}
/ SmoothBursty 中对 doSetRate的实现
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
maxPermits = maxBurstSeconds * permitsPerSecond;
// 未初始化过走该分支,初始化storedPermits为最大值
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
storedPermits = maxPermits;
} else {
// 使用过程中调整速率走该分支
storedPermits =
(oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}
}
惰性计算令牌resync
令牌的产生可以采用一个定时调度的线程,但是这种方式太重量级了,RateLimiter采用的是惰性计算令牌的方式。在每一次请求到来时通过计算当前时间和上一次计算令牌的时间的差值/产生令牌的时间间隔来计算当前桶内的令牌数
注意:
如果没有透支令牌时,nextFreeTicketMicros就是上一次计算令牌的时间
如果已经透支令牌了,nextFreeTicketMicros就是直到把透支的令牌补充回来的时间
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
获取令牌tryAcquire()和acquire()
tryAcquire和acquire都是获取令牌的两种方式,区别在于acquire是无限时等待,tryAcquire可以设置超时时间
tryAcquire
- 通过canAcquire判断在超时时间内能否放行
注意:
因为RateLimiter是预消费的,所以判断在超时时间内能否放行是判断在超时时间内能否将上一次透支的令牌恢复回来,而不是在超时时间内产生足够本次消费的令牌,这里充分体现预消费的特点,我能否消费不在于有没有足够我本次消费的令牌,而在于有没有将上一次的令牌恢复完成
- reserveAndGetWaitLength预定令牌并计算本次请求需要等待的时间microsToWait,线程等待microsToWait的时间后可以获取令牌放行
注意:
因为RateLimiter是预消费的,所以本次请求需要等待的时间microsToWait是将上一次透支的令牌恢复回来所需要的时间,并不是产生足够本次消费的令牌所需要的时间。reserveAndGetWaitLength内具体计算时间的reserveEarliestAvailable稍后会详解
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
long timeoutMicros = max(unit.toMicros(timeout), 0);
checkPermits(permits); // 检查令牌数>=0 如果为负数则说明系统出现异常了,会抛IllegalArgumentException
long microsToWait;
// synchronized 保证获取令牌的线程安全
synchronized (mutex()) {
long nowMicros = stopwatch.readMicros();
if (!canAcquire(nowMicros, timeoutMicros)) { // 首先判断当前超时时间之内请求能否被满足,不能满足的话直接返回失败
return false;
} else {
microsToWait = reserveAndGetWaitLength(permits, nowMicros); // 计算本次请求需要等待的时间,核心方法
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}
acquire
acquire比较简单,他没有超时时间,直接计算等待时间microsToWait,然后等待后被放行,但还是要注意microsToWait是预消费的,等待的时间是上一次请求的令牌得到恢复的时间
public double acquire(int permits) {
long microsToWait = reserve(permits);
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
final long reserve(int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
abstract long reserveEarliestAvailable(int permits, long nowMicros);
计算等待时间reserveEarliestAvailable
- resync计算当前已经产生的令牌数量
- 先确定本次请求可以放行的时间为恢复完上次请求透支令牌的时间
returnValue = nextFreeTicketMicros - 计算补充本次请求的令牌所需要的时间,这个时间闲置下一次请求的放行,与本次请求的放行无关
具体计算本次请求的令牌所需要的时间过程如下:
-
将本次所需的令牌数量requiredPermits 由两个部分组成:storedPermits 和 freshPermits,storedPermits 是令牌桶中已有的令牌freshPermits 是需要新生成的令牌数
-
计算取得这两部分令牌所需等待的时间
- freshPermits(需要新生成的令牌数) = 令牌数*时间间隔
- storedPermits(已经生成令牌):在SmoothBursty这部分可以直接取走不需要消耗时间,但是在SmoothWarmingUp中为了保证warmup的功能需要逐步释放令牌,后面会详解
// 计算本次请求需要等待的时间
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros); // 本次请求和上次请求之间间隔的时间是否应该有新的令牌生成,如果有则更新 storedPermits
long returnValue = nextFreeTicketMicros;
// 本次请求的令牌数 requiredPermits 由两个部分组成:storedPermits 和 freshPermits,storedPermits 是令牌桶中已有的令牌
// freshPermits 是需要新生成的令牌数
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
double freshPermits = requiredPermits - storedPermitsToSpend;
// 分别计算从两个部分拿走的令牌各自需要等待的时间,然后总和作为本次请求需要等待的时间,SmoothBursty 中从 storedPermits 拿走的部分不需要等待时间
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
// 更新 nextFreeTicketMicros,这里更新的其实是下一次请求的时间,是一种“预消费”
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
// 更新 storedPermits
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
/**
* Translates a specified portion of our currently stored permits which we want to spend/acquire,
* into a throttling time. Conceptually, this evaluates the integral of the underlying function we
* use, for the range of [(storedPermits - permitsToTake), storedPermits].
*
* <p>This always holds: {@code 0 <= permitsToTake <= storedPermits}
*/
abstract long storedPermitsToWaitTime(double storedPermits, double permitsToTake);
SmoothWarmingUp
创建
想要创建具有预热功能的限流器实现可以调用另一个重载的create方法
该方法不仅需要传入permitsPerSecond(每秒产生的令牌数),还需要传入warmupPeriod(预热时间)和unit时间单位
SmoothWarmingUp多了几个参数控制warmup分别是:
- thresholdPermits(预热状态的阈值)
- coldFactor(预热因子,预热状态的斜率见下图)
在这种创建方式中
maxPermits = permitsPerSecond * warmupPeriod
thresholdPermits = 0.5 * maxPermits
感兴趣兴趣的可以查看SmoothWarmingUp的doSetRate方法
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {
checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod);
return create(
permitsPerSecond, warmupPeriod, unit, 3.0, SleepingStopwatch.createFromSystemTimer());
}
@VisibleForTesting
static RateLimiter create(
double permitsPerSecond,
long warmupPeriod,
TimeUnit unit,
double coldFactor,
SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
storedPermitsToWaitTime
SmoothWarmingUp中取得已经产生的令牌storedPermits并不是直接获取的,而是通过storedPermitsToWaitTime计算,在SmoothBursty中上述函数直接返回0代表取得已经产生的令牌不需要等待时间,而SmoothWarmingUp中则需要需要等待时间,具体计算方式见下图:
* ^ 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
上图中横坐标是当前令牌桶中的令牌 storedPermits, SmoothWarmingUp 将 storedPermits 分为两个区间:[0, thresholdPermits) 和 [thresholdPermits, maxPermits]。纵坐标是请求的间隔时间,stableInterval 就是 1 / QPS,例如设置的 QPS 为5,则 stableInterval 就是200ms,coldInterval = stableInterval * coldFactor,这里的 coldFactor "hard-code"写死的是3。
取得已经产生的令牌需要等待时间的时间则是上图的面积,由图可知在[0, thresholdPermits) 区间内等待时间稳定为stableInterval*permits,[thresholdPermits, maxPermits]则随令牌的增加等待时间逐渐增大,从而达到warmup的效果
计算过程如下:
-
availablePermitsAboveThreshold(在阈值线上的令牌数量),取得这部分令牌所需要的时间是梯形的面积
-
permitsAboveThresholdToTake计算permitsToTake(需要的令牌)在阈值线上的数量
-
通过梯形面积公式(上底+下底)*高/2计算时间
- 下底: permitsToTime(availablePermitsAboveThreshold)
- 上底:permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)
- 高:permitsAboveThresholdToTake
-
计算还需要阈值线下的令牌数量,计算取得阈值线下的令牌所需的时间
@Override
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
long micros = 0;
// measuring the integral on the right part of the function (the climbing line)
if (availablePermitsAboveThreshold > 0.0) { // 如果当前 storedPermits 超过 availablePermitsAboveThreshold 则计算从 超过部分拿令牌所需要的时间(图中的 WARM UP PERIOD)
// WARM UP PERIOD 部分计算的方法,这部分是一个梯形,梯形的面积计算公式是 “(上底 + 下底) * 高 / 2”
double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
// TODO(cpovirk): Figure out a good name for this variable.
double length = permitsToTime(availablePermitsAboveThreshold)
+ permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
micros = (long) (permitsAboveThresholdToTake * length / 2.0); // 计算出从 WARM UP PERIOD 拿走令牌的时间
permitsToTake -= permitsAboveThresholdToTake; // 剩余的令牌从 stable 部分拿
}
// measuring the integral on the left part of the function (the horizontal line)
micros += (stableIntervalMicros * permitsToTake); // stable 部分令牌获取花费的时间
return micros;
}
// WARM UP PERIOD 部分 获取相应令牌所对应的的时间
private double permitsToTime(double permits) {
return stableIntervalMicros + permits * slope;
}
注意:
在SmoothWarmingUp中产生一块令牌的时间是stableIntervalMicros,但是取得一块已经产生的令牌的时间>=stableIntervalMicros,也就是说取得一块已经产生的令牌还不如重新产生新令牌,这种方式会让请求到达系统的等待时间较长,但因为有了warmup阶段系统更稳定
总结
本文结合自己的理解对RateLimiter的源码进行了浅析,大家可以重点关注每个部分的注意
如有错误请指正!
参考
Guava源码