目录
前言
互联网高并发、高流量业务特性使大家都关注可保障系统稳定的漏桶算法和漏斗算法(两个算法原理读者自行查阅资料)来解决秒杀、双11促销类业务流量的冲击。Google的Guava组件对漏斗算法做了两个版本的实现,分别是SmoothBursty和SmoothWarmingUp。需要对系统做预热处理的建议使用SmoothWarmingUp,使用场景如:系统启动、耗时较长的资源初始化需要10秒,在这10秒内不希望承受流量冲击。SmoothWarmingUp称这10秒为warming up,在warming up阶段流量保能很缓慢的进来,当warming up结束后流控恢复到正常限速水平(stable interval 阶段,stable interval阶段实际效果和SmoothBursty没有区别)。
原理
业务场景:限制单个JVM进程10 qps的下单能力。
方案一:
后台有一个线程池或者单个线程以特定的速率生成带有有效期的Permit并入到池中,每次用户请求过来从池中获取Permit。
备注:一般喜欢叫Token,RateLimiter源码中使用的是permit,这里与源码保持一致
这个方案的优点是代码实现简单、易维护、可读性强,缺点也很明显,单独的线程池不间断生成Permit对CPU的消耗,如果一个JVM中有多个RateLimiter实例,那是一种灾难。一些小项目可以考虑使用,这肯定是不符合Guava这类大神之作的方案要求。
RateLimiter原理
RateLimiter基于时间轴变化在每次请求时计算是否有可用Permit。总体思路:记录start、next两个时间点,每次申请Permit时,若current-next>stable interval可以申请Permit。
注:图中一个刻度表示100ms
名字解释:
start:计时器启动时间
next:下次可生成Permit时间(current>next时,默认为上次申请Permit时间)
current:当前时间
stable interval:生成一个Permit时间间隔
步骤:
1、记录启动时间,记start=System.currentTimeMillis()
2、计算生成一个Permit的时间间隔,继上面例子1秒10qps,1000/10=100,即100ms生成一个Permit,记stable interval 为100ms
3、
request1:申请一个Permit,next=0,current=start+1
a. current>next设置next=current
b. 计算request1等待时间,next<=current,request1不需要等待
c. 计算可用Permit,(current-next)/statble interval=0,没有可用Permit。怎么办?向未来借用Permit,next需要往后推迟一个stable interval。即:next=next+stable interval=start+2
request2:申请一个Permit,next=start+2,current=start+1.2
a. current<next,什么也不做
b. 计算request2等待时间,sleep time = next-current=0.8,request2需要等待sleep time(还request1借的时间)
c. 计算可用Permit,current<next,说明时间上次请求借用的额度还没有还完,所以本次请求只能再次向未来借用Permit,next需要往后推迟一个stable interval。即:next=next+stable interval=start+3
request3:申请一个Permit,next=start+3,current=start+8
a. current>next设置next=current
b. 计算request3等待时间,next<=current,request3不需要等待
c. 计算可用Permit,(current-next)/statble interval=5,所以本次请求不需要借用额度,注意额度最多只能存储用户设置的qps数量,这个例子是10
SmoothBursty
关键属性
// RateLimiter当前可用的Permits数量
double storedPermits;
// SmoothBursty等于用户设置的QPS,SmoothWarmingUp通过复杂逻辑计算出来,小于用户设置的QPS
double maxPermits;
// 每个Permit时间间隔,上面例子此值为100
double stableIntervalMicros;
// 下次可以生成Permit的时间,这个时间可是过去或者未来
long nextFreeTicketMicros = 0L;
关键方法
doSetRate
构建RateLimiter设置
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
//maxBurstSeconds硬编码为1
maxPermits = maxBurstSeconds * permitsPerSecond;
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
storedPermits = maxPermits;
} else {
// 启动时走的都是else,storePermits为0
storedPermits = (oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits
}
//断上面的例子,storePermits=0、maxPermits=10
}
reserveEarliestAvailable
核心方法,更新next时间、计算需要等待时间、计算是否需要向未来借用额度
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
//更新next时间、storePermits
resync(nowMicros);
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
//借用的额度
double freshPermits = requiredPermits - storedPermitsToSpend;
//计算本次借用额度下次需要等待的时间
//SmoothBursty实现storePermitsToWaitTime永远为0,
long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
try {
//未来借用额度产生的时间添加给next
this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);
} catch (ArithmeticException e) {
this.nextFreeTicketMicros = Long.MAX_VALUE;
}
//扣减此次请求的额度
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
SmoothWarmingUp
你可能希望系统接受的流量从0到目标QPS是一个缓慢的过程,而不是突然达到目标QPS。SmoothWarmingUp可以控制流量缓慢的从0到目标QPS,保护系统避免出现流量冲击,这个过程称作WarmingUp(预热),预热结束后SmoothWarmingUp和SmoothBursty没有区别,SmoothWarmingUp怎么控制这个过程呢?
WarmingUp原理
问题描述:假设WarmingUpPeriod为500ms,目标QPS为1000 QPS,如何做到在500ms内把QPS缓慢增长到1000QPS ? 当增长到1000QPS后以恒定速率1ms可获取一个Permit。
关键问题:
1、在WarmingUp阶段 500ms内可发放多少个permit
2、发放多少个permits后以恒定的时间间隔发放permit
3、在WarmingUp阶段可获取一个permit时间间隔?WarmingUp阶段QPS的是缓慢增长,可获取permit的时间间隔是缓慢减少。如:从最初的10ms、9ms、8ms、7ms可获取一个permit,一直到最后以恒定1ms可获取一个permit,此时QPS为1000
(注意文章下面的用词,可获取permit时间间隔和可发放permit时间间隔,在SmoothBursty中两个概念可以说是等价的,均为stable interval。但在SmoothWarmingUp中概念是不一样的。在SmoothWarmingUp中可发放的时间间隔是基于假设计算的且值是恒定不变的,在预热阶段可获取permit时间间隔正是我们一直求解的问题,预热阶段过后可获取permit时间间隔也变为恒定不变的stable interval。预执阶段过后可获取permit时间间隔和发放pertmit时间间隔也不相等)
假设:
1、记每秒目标QPS为permitsPerSecond,记预热周期为WarmingUpPeriod
2、可获取的permits最大值为maxPermtis(如:WarmingUp阶段最多可发放500个permit)
3、可获取的permtis值为thresholdPermits后可以恒定的时间间隔获取permit,此时表明预热结束(如:当thresholdPermits为250时,每1ms可获取一个permit)
4、预热结束后可以恒定的时间间隔stable interval获取一个permti(1秒/permitsPerSecond,即:1000/1000=1ms)
5、在WarmingUp阶段以cold interval时间间隔可获取一个permit(10ms、7ms、4ms)
6、以恒定的时间间隔发放permit,(这个假设一定注意,这是指未使用情况下发放permit的时间间隔,SmoothBursty是stable interval)
数学建模:
以系统拥有可获取的Pertmits为X轴,记storePermits为X轴。可获取permit时间间隔为Y轴,记throttling为Y轴
X轴有两个关键点:
maxPermits:可获取的permits最大数值
thresoldPermits:[0,thresoldPermits]时以恒定时间间隔获取permit,(thresholdPermits,maxPermits]以cold interval 时间间隔获取permit
Y轴有两个关键点:
stable interval:以恒定的时间间隔stable interval获取一个permit,1秒/1000QPS=1 ms,即1ms可发送一个permit
cold interval:系统预热阶段可获取每个permit的时间间隔,在WarmingUp阶段,可获取一个permit的cold interval都会减少一个恒定比率,为了方便说明,我们暂且记这个比率为slope(每次获取permit,cold interval 都是变化的,slope为变化因子)
我们把stable interval、cold interval、thresholdPermits、maxPermits 四个点连接到一起,组成一个梯形。由边A、B、C、maxpermits-thresholdPermits组成
我们知道当storePermits从maxPermits--->thresholdPermits且throttling从cold interfal --->stable interval 时预热结束,那么也表示耗时为预热周期。转换为数学模型就是点a、b、threshodlPermits、maxPermits构建的梯形的面积等于WarmingUpPeriod(例子中的500ms)。所以问题可转换在预热阶段每获取一个permit时,所构建的梯形面积就是额外要等待的时间。
思路已经有了,假设上图F为maxPermits-1时组成的梯形图,怎么求出它的面积?在计算前要定几个基准原则,否则这个梯形面积无法计算出来
基准原则:
1、当RateLimiter不使用的时候,RateLimiter以一个常量比率发送permit,这个常量比率选择为(这条原则在前面已经在前面已经说过了),这样是为了确保permits从0--->maxPermits有消耗的时间为warmingPeriod
2、当使用RateLimiter时,它所消耗时间介于x permits与x-k permits之间组成的面积(上面推导数学模型的时候已经说过,比如梯形F)
3、coldFactor=3,,这是人为定义的,这点很关键,上面也有提到。
4、maxPermtis--->thresholdPermits所组成的梯形面积等于warmingPeriod(前面有说明)
5、基于基准原则1、3、4可以推导出 ,这点有些难,我们推导下。
a. stableInterval * maxPermits = (stableInterval + 3* stableInterval )* (maxPermits-thresholdPermits)/2
====>2*stableInterval * maxPermtis=4*stableInterval * (maxPermits-thresholdPermits)
====>记maxPermits-thresholdPermits= H,梯形的高
====>2*stableInterval * (thresholdPermits+ H) = 4* stableInterval * H
====>2*stableInterval *thresholdPermits+ 2*stableInterval* H = 4* stableInterval *H
====>thresholdPermits=H
====>thresholdPermits = maxPermits-thresholdPermits
6、因此可以推导出 ,即,thresholdPermits加上梯形的高
基于上面的基准原则我们可以算出来thresholdPermits和maxPermits了,但梯形的上边和下边怎么计算呢?这里用了一个前面说的slope概念来计算,slope是一个三角形的∠BCA的正弦,,因此边B=stableInterval + (maxPermits-thresholdPermits) * slope可以推导出B=stableInterval + maxPermits-thresholdPermits-permits(每次获取的permit数量)* slope
核心方法
原理讲完了,核心方法就简单了,就是对原理的实现
doSetRate
@Override
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;
}
}
storedPermitsToWaitTime
@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) {
double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
//计算此次获取Permits组成的梯形面积
micros = (long) (permitsAboveThresholdToTake
* (permitsToTime(availablePermitsAboveThreshold)
+ permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)) / 2.0);
permitsToTake -= permitsAboveThresholdToTake;
}
// measuring the integral on the left part of the function (the horizontal line)
//常量比率等待+额外等待之和为此次等待时间
micros += (stableIntervalMicros * permitsToTake);
return micros;
}