目录
简介
限流即流量限制,目的是在遇到流量高峰期或者流量突增时,通过对流量进行限制,以把流量控制在系统所能接受的合理范围之内,不至于让系统被高流量击垮。当达到限制速率时,可以拒绝服务(定向到错误页或告知资源没有了)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)等。
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流。令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不到,该请求就要被限流,即丢弃或在缓冲区等待。
RateLimiter是一个抽象类,子类SmoothRateLimiter派生了平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)两种实现。继承关系:
核心参数:
//当前存储的permits
double storedPermits;
//最大可存储permits
double maxPermits;
//两个请求的时间间隔,e.g 当permitsPerSecond=5时,那么请求间隔为200ms
double stableIntervalMicros;
/*下一次生成令牌的开始时间
由于RateLimiter允许预消费,上次请求预消费令牌后
下次请求需要等待相应的时间到nextFreeTicketMicros时刻才可以获取令牌*/
private long nextFreeTicketMicros = 0L;
平滑预热限流(SmoothWarmingUp)
启动后会有一段预热期,逐步将分发频率提升到配置的速率。表现形式为令牌刷新的时间间隔由长逐渐变短。等存储令牌数从maxPermits到达thresholdPermits时,发放令牌的时间价格也由coldInterval降低到了正常的stableInterval。
主要原理是分发令牌的速率会随时间和令牌数而改变,由于涉及数学公式推导,先不研究。(想学习的参考这篇 https://cloud.tencent.com/developer/article/1607090)
平滑突发限流(SmoothBursty)
-
Demo
将qps设置为10,每0.1s左右获取一个令牌。
如果设置完qps后没有请求,此时会存储令牌,请求过来后直接拿到令牌不需要等待。比如休眠10s按10qps可以生成100个令牌,但qps10决定了最大存储10个令牌。
-
create过程
//默认使用平滑突发限流,permitsPerSecond为每秒生成的令牌数
public static RateLimiter create(double permitsPerSecond) {
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
//SleepingStopwatch主要用于计时和休眠
@VisibleForTesting
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond); //RateLimiter的setRate方法
return rateLimiter;
}
public final void setRate(double permitsPerSecond) {
checkArgument(
permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized (mutex()) {
//调用SmoothRateLimiter的setRate方法
doSetRate(permitsPerSecond, stopwatch.readMicros()); //
}
}
//nowMicros为计时器当前时间
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
//更新令牌数和获取令牌时间-
resync(nowMicros);
//更新请求的时间间隔
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
//第一次调用oldMaxPermits为0,所以storedPermits(桶中令牌个数)也为0
doSetRate(permitsPerSecond, stableIntervalMicros);
}
void resync(long nowMicros) {
//若当前时间 > 下次生成令牌开始时间
if (nowMicros > nextFreeTicketMicros) {
//计算出这段时间内,一共可以生产多少令牌。coolDownIntervalMicros()返回stableIntervalMicros,创建时还未赋值,所以是初始0,newPermits计算结果为无穷大
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
//更新存储令牌数
storedPermits = min(maxPermits, storedPermits + newPermits);
//更新下次生成令牌开始时间
nextFreeTicketMicros = nowMicros;
}
}
//SmoothBursty的coolDownIntervalMicros方法直接返回时间间隔
@Override
double coolDownIntervalMicros() {
return stableIntervalMicros;
}
@Override
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;
}
}
创建完成后,结果如图所示:当前存储令牌0,最大存储令牌为2,两个请求的时间间隔500ms,更新了下次请求时间,maxBurstSeconds是SmoothBursty的属性,表示存储多少s的请求。
-
acquire过程
RateLimiter
会累积令牌,所以可以应对突发流量。在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受。
获取令牌时可以分为三种情况:1、令牌数足够,直接获取令牌并更新存储令牌数、生成令牌开始时间 2、当前时间 > 开始生成令牌时间,但令牌数不够,此时会借用令牌,将存储置为0,并计算借用的令牌所需要偿还的时间,保存到下一次开始生成令牌时间 3、当前时间 < 开始生成令牌时间,即上一个请求借用的令牌还没还完时间,此时需要休眠到偿还完时间为止,并再次借用令牌。
public double acquire() {
return acquire(1);
}
public double acquire(int permits) {
//需要等待的时间,当令牌足够或者不够导致第一次借用时,返回为0;
//当上次借的令牌花费的时间还没有还完,则一直休眠到还完为止,并再次借用
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);
//之所以要减去当前时间,是为了让第一次借用令牌的请求不需要休眠,所以后面的方法对nextFreeTicketMicros的处理比较绕
return max(momentAvailable - nowMicros, 0);
}
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros);
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
double freshPermits = requiredPermits - storedPermitsToSpend;
//当令牌数不够,需要借用时,计算借用的令牌需要花费的时间
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
//更新令牌数
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
void resync(long nowMicros) {
//如果当前时间>令牌开始生成时间,更新令牌数,更新下次令牌开始生成时间
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}
与信号量对比:
一旦从 RateLimiter 获得许可,不需要释放。信号量分配后必须释放。
RateLimiter 控制的是速率,以配置的速率分发许可,速率不变时单位时间内分发的许可量是恒定的。信号量控制的是并发访问的数量,单位时间内分配的次数跟使用许可的时长有关,每次申请使用的时间越短,则单位时间内能分配的次数就越多。