我们首先讲一个场景,假如一个接口只能支持每秒5次访问速率,假如要实现这个功能要如何实现?通用的方法我们知道有计数器,漏斗,令牌桶等方法,本文主要介绍Guava如何是如何实现限流的
Guava是使用令牌桶算法来实现限流功能的,但也有自己的一些特点:
- 在每次有请求来获取token的时候,会同时添加令牌到令牌桶中,并不是通过异步任务固定速率添加令牌到令牌桶中,是一个惰性的方法。
- 超额信用消费,即用将来时间产生的令牌,来满足当前的请求,这样能应对一定的瞬间流量
可以带着这两个特点来继续阅读下文,首先看下Guava是如何使用的,简单代码如下(创建每秒5次的限流器):
RateLimiter limiter = RateLimiter.create(5.0); //创建每秒一次的限流器
double timeWaited = limiter.acquire(); //获取令牌,返回值为获取令牌等待的时间 System.out.println("time waited: " + timeWaited);
上面的代码使用时很简单的,下面来剖析下代码实现,Guava 中的限流器有两种实现,
- SmoothBursty,顾名思义,该实现主要通过令牌桶方法实现,能够应对突发流量
- SmoothWarmingUp,实现也是通过令牌桶方法,但在应对突发流量与流量整形中取了折中,能应对一定的突发流量,同时具有一定的流量整形功能
首先我们先看下整体的类结构,下图是类图:
首先介绍下基类SmoothRateLimiter的关键属性:
属性 | 作用 | 描述 |
---|---|---|
storedPermits | 当前限流器可用的令牌总数 | |
maxPermits | 当前限流器最大的可用令牌数总数 | storedPermits<= maxPermits |
stableIntervalMicros | 请求的平均时间间隔 | 如假设当前限流器是每秒5次,所以两次请求的平均间隔是200ms,也表示每隔stableIntervalMicros,会产生一个token |
nextFreeTicketMicros | 下一次可以产生令牌的时间 |
###SmoothBursty限流实现分析
SmoothBursty类中有一个自己的属性:maxBurstSeconds,值默认为1,表示流量最大持续时间,maxPermits = maxBurstSeconds * permitsPerSecond,permitsPerSecond表示每秒qps
此时一次请求token的请求会首先到acquire()方法中,该方法会顺序执行以下三个步骤
- 补充token到令牌桶中,若当前时间nowMicros大于nextFreeTicketMicros,说明有新的token可以产生,并可以加入到令牌桶中的,具体方法在SmoothRateLimiter#resync
- 获取token,并计算获取这些token需要等待的时间,具体方法在SmoothRateLimiter#reserveEarliestAvailable
- 根据获取token时返回的等待时间,阻塞当前线程,直到指定的等待时间,具体执行入口是在acquire()中的stopwatch.sleepMicrosUninterruptibly(microsToWait)触发
抛开代码实现流程,从token的数据处理流程得到下图:
上面的各个属性可以参考一开始的表格,如上图,当在nowMicros时间点请求requiredPermits个token时,会先判断nowMicros是否大于nextFreeTicketMicros,
-
若大于则会多走一步step1,计算出在[nextFreeTicketMicros,nowMicros]产生的tokens,并将这些token加到令牌桶总数中storedPermits,同时将nextFreeTicketMicros设置为当前时间,其中coolDownIntervalMicros这个方法是获取产生一个token所需要的时间间隔
-
step2不管怎样都会被执行,在步骤中会计算获取token所需要的等待时间,同时更新nextFreeTicketMicros时间,即下一次可以获取token的时间点:
-
storedPermitsToSpend表示可以从令牌桶中获取的token数量
-
freshPermits表示需要新生成的token数量
-
waitMicros表示获取requiredPermits个token所需要的消耗时间,该等待时间是由两部分组成,一个是新生成freshPermits个token需要的时间,以及消耗令牌桶已经存在的storedPermitsToSpend个token所需要的时间,即方法storedPermitsToWaitTime(这个地方可能会有人产生疑问,为啥令牌都已经产生了,直接使用还需要消耗时间,这个会在后面介绍)
现在重点从代码分析介绍:
acquire方法,获取token的入口
public double acquire(int permits) {
//更新token同时返回本次请求需要等待的时间,即 nowMicros-nextFreeTicketMicros(注意该时间为step1后的值)
long microsToWait = reserve(permits);
//返回本次请求阻塞等待的时间
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
reserve方法会调用reserveAndGetWaitLength方法,
final long reserveAndGetWaitLength(int permits, long nowMicros) {
//表示当前时间开始,要等到哪个时间点,即为(momentAvailable),才获取permits个toke,
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0); }
reserve方法,最终会进入到reserveEarliestAvailable方法中,注意storedPermitsToWaitTime方法在SmoothBursty中是默认返回0
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros); //执行step1步骤,更新令牌桶数据
long returnValue = nextFreeTicketMicros;//保存此刻的nextFreeTicketMicros值
//获取本次请求中可以从令牌桶中获取的token数量
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
//本次请求中,需要新生成的token数量,即令牌桶的令牌不够用了
double freshPermits = requiredPermits - storedPermitsToSpend;
long waitMicros = //计算获取requiredPermits所消耗的总时间
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits *
stableIntervalMicros);
//更新nextFreeTicketMicros,表示下次可以产生token的时间点
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
this.storedPermits -= storedPermitsToSpend; //更新令牌桶,减去被消耗的令牌数据
return returnValue;
}
resync方法,更新令牌桶数量和nextFreeTicketMicros,coolDownIntervalMicros用来获取产生令牌的时间间隔,在SmoothBursty中,该方法默认返回stableIntervalMicros
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;
}
}
以上就是SmoothBursty限流的实现方法,从上面我们可以看出nextFreeTicketMicros可能是大于当前时间的,即用将来的时间产生token来满足当前的token需求
###SmoothWarmingUp 限流实现分析
以上其实还有一个未解问题,为啥会有storedPermitsToWaitTime方法(虽然在SmoothBursty中是默认返回0,所以在SmoothBursty实现中是没有任何意义的),为啥使用令牌桶中已经存在的token还要消耗时间?
首先引入一个问题:SmoothBursty实现能够很好的应对突发流量,但瞬时流量来了,后端服务是否准备好了?服务中的数据是否“热起来了”,会不会造成缓存击穿等问题。所以瞬时流量对服务还是有一定隐患存在的
为了解决这个问题,Guava提供了另外一个限流实现,SmoothWarmingUp,如名字一样,请求会被缓慢放行,即使使用已经存在存在的令牌,也是要消耗时间的,假设令牌桶的token是满的,说明在当前时间访问服务的请求是很少的,此时服务是处于一个“冷状态”,所以更不能一下放行大量请求进来,SmoothWarmingUp实现的主流程是和SmoothBursty一样的,只不过针对storedPermitsToWaitTime和coolDownIntervalMicros有不同的实现
SmoothWarmingUp关键属性:
- warmupPeriodMicros,表示处于平滑过渡的时间长度
- slope ,在平滑过渡时间段是,coldInterval随着storedPermits变化的变化率
- thresholdPermits,平滑过渡时间段与正常时间段的分界点
- coldFactor,coldInterval = coldFactor * stableInterval 表示coldInterval与stableInterval倍数关系
先看下上面这个图,图中横坐标表示令牌桶中可用数量,纵坐标表示使用当前令牌所消耗的时间,所以storedPermitsToWaitTime(storedPermits, storedPermitsToSpend)方法返回的是从令牌桶中使用storedPermitsToSpend个token所需要的时间,而该时间可以表示为上图中横坐标为[storedPermits-storedPermitsToSpend,storedPermits]对应的面积。
storedPermitsToWaitTime方法具体源码:
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
long micros = 0;
// 假如permitsToTake大于thresholdPermits,则计算【thresholdPermits,storedPermits】的梯形面积
if (availablePermitsAboveThreshold > 0.0) {
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);
permitsToTake -= permitsAboveThresholdToTake;
}
// 计算【storedPermits - thresholdPermits,thresholdPermits】的面积
micros += (long) (stableIntervalMicros * permitsToTake);
return micros;
}
在SmoothWarmingUp限流实现中会在doSetRate方法中进行属性的初始化
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = maxPermits;
double coldIntervalMicros = stableIntervalMicros * coldFactor;
thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;
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;
}
}
doSetRate方法解释:
- thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros; 我们默认为warmupPeriodMicros平滑过渡的时间长度是正常时间长度的2倍,而如图所示,在正常阶段,thresholdPermits个令牌与与stableInterval的乘积就是产生thresholdPermits令牌的总时间
- slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits),表示在平滑过渡时间段是,coldInterval随着storedPermits变化的变化率
参考文档:
来源:oschina
链接:https://my.oschina.net/u/4201840/blog/4921067