RateLimiter主要用于作限流,对于限流,现在的主要几种算法参考:帮助你理解熔断、降级和限流
RateLimiter便是基于令牌桶实现的流量限制
那么就让我们开始把!
一个官方文档中的例子:假设现在有一系列任务需要执行,并且你希望每秒钟被执行的任务不能超过2个
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List tasks, Executor executor) {
for (Runnable task : tasks) {
rateLimiter.acquire(); // 可能需要等待
executor.execute(task);
}
}
就由这个例子入手,首先看看RateLimiter的create方法,permitsPerSecond保证在任意选定的一秒内均能保证其不超过,这是计数器算法不具有的
public static RateLimiter create(double permitsPerSecond) {
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
这里创建的是一个平稳突发启动的RateLimiter,SleepingStopwatch是一个基础计时器,用于度量经过的时间和必要的休眠时间
- 平稳突发启动
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
- 冷启动
比上面的多一个参数,表示冷启动的时候,即:多少时间之内达到permitsPerSecond,单位时间:NANOSECONDS
public static RateLimiter create(double permitsPerSecond, Duration warmupPeriod) {
return create(permitsPerSecond, toNanosSaturated(warmupPeriod), TimeUnit.NANOSECONDS);
}
这里创建的是一个冷启动的RateLimiter
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());
}
到这里需要理解一个概念,预消费,前一个请求需求的令牌数超过了当前持有的令牌数,那么这个请求可以预消费,直接取走自己所需要的的量,而这个量会由后一个请求来偿还(通过等待时间来偿还)
对于QPS的设置可以通过以下俩个方法进行,通过setRate设置的最新值,对于当前正在睡眠的线程是透明的,即他们不会感知到Rate的变化,只有新进来的Request才能感知到
并且对于setRate的后一个请求,新的Rate也是它感知不到的,这是因为上面提到的预消费,这时候的它已经为上一个请求买单,新的Rate对它而言并没有意义
重新设置Rate如果是冷启动,就还要进行一次冷启动
public final void setRate(double permitsPerSecond) {
checkArgument(
permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized (mutex()) {
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}
public final double getRate() {
synchronized (mutex()) {
return doGetRate();
}
}
再来到这样一个方法,这个方法决定预消费的实时,后一个消费需要为前一个消费买单,那么买单是需要等待多长时间呢,就是通过这个方法来
final long reserve(int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
进入实现查看,最后找到的reserveEarliestAvailable方法是一个抽象类,需要子类实现具体的算法
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
abstract long reserveEarliestAvailable(int permits, long nowMicros);
不妨来到它的其中一个子类,实现平稳突发启动的SmoothBursty的实现,先看看几个变量
- double storedPermits; 当前持有令牌数
- double maxPermits; 最大持有令牌数
- 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;
void resync(long nowMicros) {
// 如果当前时间已经过了下次发放令牌时间,计算相差时间内所能产生的令牌数,将令牌加入令牌桶
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros(); //这里的coolDownIntervalMicros = stableIntervalMicros
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros; //将这次放牌的时间记录更新
}
}
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros); //误差修正令牌数和发牌时间
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits); //storedPermitsToSpend:如果需要的令牌数小于桶内令牌数,就是需要的令牌数,这没啥,但是如果需要的比较大,那么这个值就是当前桶内数量
double freshPermits = requiredPermits - storedPermitsToSpend; //freshPermits就是还差多少个令牌
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros); //通过计算获取预消费的时间
//storedPermitsToWaitTime 在这里空实现 返回0L
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros); //将预消费时间加入到 下次获取令牌时间,就是因为这里,所以导致下次请求需要为前一个请求买单
this.storedPermits -= storedPermitsToSpend; //重新计算桶内令牌
return returnValue; //返回上次消费的时间,即:返回的时候本次请求刚进来的时间,而不是本次请求进来后,预消费后的时间
}
上面的处理,返回的本次请求刚进来的时间,而不是返回本次请求进来后,再经过它预消费的时间,就是为了预消费作处理,如果不需要预消费,请在这里返回最新的nextFreeTicketMicros
来了解一下冷启动
SmoothRateLimiter源码,请到line145找这张图
认识几个变量:
- thresholdPermits 阙值,如果桶内令牌数大于这个值,即变为冷启动,增大产生令牌间隔
它的值是 预热的时候内所能产生的令牌数的一半,0.5 * warmupPeriodMicros / stableIntervalMicros
- coldFactor 冷却系数,默认为3.0
- slope 表示斜率,用正常的斜率公式计算就是了(coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
对于冷启动的实现,其实主要是在3个方法上重写方式不同
一个是doSetRate在俩处的实现是不一样的
直接启动是通过直接重新计算产生令牌的间隔,然后重新设置间隔,最大令牌数,以及当前持有令牌数
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
maxPermits = maxBurstSeconds * permitsPerSecond;
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;
}
}
冷启动的doSetRate:
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = maxPermits;
//冷启动状态下,时间间隔为正常时间间隔的coldFactor倍数,coldFactor默认为3.0
double coldIntervalMicros = stableIntervalMicros * coldFactor;
//计算thresholdPermits,它的值是 预热的时候内所能产生的令牌数的一半
thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;
//计算maxPermits,通过梯形公式反推,面积是预热时间,上底正常间隔,下底冷启动间隔,高=max-阙值,反推max
maxPermits =
thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);
//表示斜率
slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
storedPermits = 0.0;
} else {
storedPermits =
(oldMaxPermits == 0.0)
? maxPermits // initial state is cold
: storedPermits * maxPermits / oldMaxPermits;
}
}
一个是coolDownIntervalMicros,他是用来计算延迟的时间内产生的令牌数,直接启动其实就是返回stableIntervalMicros间隔时间
而冷启动:warmupPeriodMicros / maxPermits,冷启动时间/最大令牌数(warmupPeriodMicros / maxPermits),即计算 冷启动时间内生产令牌到达最大令牌数的间隔
一个是reserveEarliestAvailable中的storedPermitsToWaitTime,就是在计算获取预消费时间的时候多了一处计算,直接启动返回0L,而冷启动的代码如下:
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) {
//查看超过阈值的桶内的数量和当前桶内需要耗费的谁小,其实就是获取 当前桶内令牌数超过阈值的,并且需要被消耗的数量(作 高)
double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
// TODO(cpovirk): Figure out a good name for this variable.
//加起来 其实就是 上底 + 下底 ,求一个小梯形
double length =
permitsToTime(availablePermitsAboveThreshold) //这一行求的是当前间隔,里面是stableIntervalMicros + permits * slope; 其实是: 平缓的时间间隔 + 当前超过阈值令牌数 * 斜率 (理解成 初速度 + 加速度 * 时间),下同
+ permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
//求面积,其实就是求具体的冷启动时间
micros = (long) (permitsAboveThresholdToTake * length / 2.0);
//计算取走后这些冷启动阶段获取的令牌,还有多少令牌需要在平缓启动的时候获取
permitsToTake -= permitsAboveThresholdToTake;
}
// measuring the integral on the left part of the function (the horizontal line)
//正常情况下的令牌时间也要加上
micros += (long) (stableIntervalMicros * permitsToTake);
//获得到了正方形+三角形的面积
return micros;
}