流量效果控制,经典限流算法
限流算法用以控制通过的流量始终在限流阈值内,不同限流算法还可以控制流量达到某种效果,如控制超阈值流量快速失败的计数器算法、允许一定数量的请求等待通过的漏桶算法(Leaky Bucket),控制流量匀速通过且允许流量在一定程度上突增的令牌桶算法(TokenBucket)。
1. 计数器算法
Sentinel中默认实现的QPS限流算法和Threads限流算法都属于计数器算法。QPS限流算法是根据当前时间窗口(1秒)的pass(被放行的请求数)指标数据判断的,当通过一个请求时,当前时间窗口的pass指标计数加1,如果pass总数已经大于或等于限流的QPS阈值,则直接拒绝当前请求。Threads限流算法是根据当前资源并行占用的线程数是否已经达到阈值判断的,若是,则直接拒绝当前请求,并且每通过一个请求,Threads计数加1,每完成一个请求,Threads计数减1。
2. 漏桶算法
漏桶就像在一个桶的底部开一个洞,不控制水被放入桶的速率,而通过底部漏洞的大小控制水流失的速率。当水被放入桶的速率小于或等于水通过底部漏洞流出的速率时,桶中没有剩余的水;而当水被放入桶的速率大于水通过底部漏洞流出的速率时,水就会逐渐在桶中积累,当桶装满水时,若再向桶中放入水,则放入的水就会溢出。
如果把水换成请求,当处理请求的速率大于或等于请求到来的速率时放行请求,否则当请求堆积到一定程度时,溢出的请求将被拒绝,如图5.3所示。
图5.3 漏桶算法
3. 令牌桶算法
令牌桶不存放请求,而是存放为请求生成的令牌(Token),只有拿到令牌的请求才能通过。令牌桶算法的原理是以固定速率向桶中放入令牌,每当有请求过来时,都尝试从桶中获取令牌,如果能拿到令牌,请求就能通过。当桶中放满令牌时,多余的令牌就会被丢弃,而当桶中的令牌被用完时,请求拿不到令牌就无法通过,如图5-4所示。
图5.4 令牌桶算法
流量效果控制器
Sentinel支持采取流量效果控制器控制超过限流阈值的流量。流量效果控制器支持的功能包括直接拒绝、冷启动、匀速排队。
FlowRule的controlBehavior字段用于配置采用何种流量效果控制器。在调用FlowRuleManager#loadRules方法时,FlowRuleManager会将限流规则配置的controlBehavior转换为对应的TrafficShapingController实例。
TrafficShapingController接口的源码如下。
•node:根据limitApp与strategy选出来的Node(可为StatisticNode、DefaultNode或ClusterNode)。
•acquireCount:与并发编程方法AQS#tryAcquire的参数作用一样,acquireCount的值一般为1。当限流规则配置的限流阈值类型为Threads时,表示需要申请一个线程;当限流规则配置的限流阈值类型为QPS时,表示需要申请放行一个请求。
• prioritized:表示是否对请求进行优先级排序,SphU#entry方法传递过来的值是false。
controlBehavior的取值与使用的TrafficShapingController的对应关系如表5.1所示。
表5.1 controlBehavior的取值与使用的TrafficShapingController的对应关系
快速失败流量效果控制器
DefaultController是默认使用的流量效果控制器,实现的效果是直接拒绝超过阈值的请求。当QPS超过限流规则配置的阈值时,新的请求就会被立即拒绝,并抛出FlowException。
DefaultController适用于明确知道系统处理能力的情况,如通过压测确定阈值。实际上我们很难测出这个阈值,因为一个服务可能被部署在硬件配置不同的服务器上,并且随时都可能调整部署计划。
DefaultController#canPass方法的源码如下。
① avgUsedTokens方法:如果规则的限流阈值类型为QPS,则此方法返回Node统计的当前时间窗口已经放行的请求总数;如果规则的限流阈值类型为Threads,则此方法返回Node统计的当前并行占用的线程数。
② 如果将当前请求放行会超过限流阈值且不满足条件③,则直接拒绝当前请求。
③ 如果prioritized为true且规则的限流阈值类型为QPS,则表示具有优先级的请求可以占用未来时间窗口的统计指标。
④ 如果可以占用未来时间窗口的统计指标,则tryOccupyNext会返回当前请求需要等待的时间,单位为毫秒。
⑤ 如果休眠时间在限制可占用的最大时间范围内,则挂起当前请求,令当前线程休眠waitInMs毫秒,在休眠结束后抛出PriorityWaitException,标志当前请求是等待了waitInMs毫秒之后通过的。在一般情况下,prioritized参数的值为false。如果prioritized在ProcessorSlotChain传递的过程中,排在FlowSlot之前的ProcessorSlot都没有修改过,则条件③不会被满足,因此这个canPass方法实现的流量效果就是直接拒绝。
匀速限流效果控制器
Sentinel基于漏桶算法并结合虚拟队列等待机制实现了匀速限流效果。可将其理解为存在一个虚拟的队列,使请求在队列中排队通过,每count / 1000毫秒可通过一个请求。
使用虚拟队列的好处在于队列并非真实存在,当多核CPU并行处理请求时,可能会出现这些请求并排占用一个位置的现象。也因为如此,实际通过的QPS会超过限流阈值的QPS,但不会超过很多。
若要使用匀速限流效果控制器RateLimiterController配置限流规则,则必须配置限流阈值类型为GRADE_QPS,并且阈值要小于或等于1000。
配置使用匀速限流效果控制器的代码如下。
RateLimiterController类的字段和构造方法的源码如下。
• maxQueueingTimeMs:请求在虚拟队列中的最大等待时间,默认为500毫秒。
• count:限流阈值的QPS。
• latestPassedTime:最近一个请求通过的时间,用于计算下一个请求的预期通过时间。
RateLimiterController类实现的canPass方法的源码如下。
① 计算队列中连续两个请求通过的时间间隔,如果限流阈值为200QPS,则costTime等于5,即每5毫秒只允许通过一个请求,而每5毫秒通过一个请求就是固定速率。
② 计算当前请求的期望通过时间,等于时间间隔加上最近一个请求通过的时间。
③ 如果期望通过时间少于或等于当前时间,则当前请求可立即通过。
④ 如果期望通过时间超过当前时间,则需要休眠等待,需要等待的时间等于预期通过时间减去当前时间。
⑤ 如果等待时间超过队列允许的最大等待时间,则会拒绝当前请求。
⑥ 如果更新latestPassedTime为期望通过时间后,需要等待的时间还是少于最大等待时间,则说明排队有效,否则说明在一瞬间被某个请求占位了,需要拒绝当前请求,将当前请求移出队列并回退一个时间间隔。
⑦ 休眠等待waitTime毫秒。
latestPassedTime永远存储当前请求的期望通过时间,后续的请求将排在该请求的后面,这就是虚拟队列的核心实现,按期望通过时间排队。在虚拟队列中,将latestPassedTime回退一个时间间隔,相当于将虚拟队列中的一个元素移除。
在理想情况下,每个请求在队列中排队通过,则每个请求都在固定的不重叠的时间通过。但在多核CPU的硬件条件下,可能出现多个请求并行通过的情况,即队列中的一个位置被多个请求同时占用,这就是为什么说实际通过的QPS会超过限流阈值的QPS的原因。但是没有关系,因并行导致超出的请求数不会超过阈值太多,所以影响不大。
匀速限流效果控制器适合用于请求突发性增长后剧降的场景。例如,对于一个有定时任务调用的接口,在定时任务执行时请求量会一下“飙高”,但随后又没有了请求,这时为了避免把系统压垮,我们不希望让所有请求一下都通过,但也不想直接拒绝超过阈值的请求。在这种场景下,使用匀速限流效果控制器可以将突增的请求排队到低峰时执行,起到“削峰填谷”的效果。
在分析完源码后,我们再来看GitHub上Sentinel的一个Issue,如图5.5所示。
图5.5 QPS阈值超过1000后不生效的Issue
为什么将QPS限流阈值配置为超过1000后限流就不生效了呢?
计算请求通过的时间间隔的代码如下。
假设count=1200(限流QPS阈值),当acquireCount=1时,costTime=1000/1200,这个结果是小于1毫秒的,使用Math.round方法取整后值为1;而当QPS阈值增大,costTime的计算结果小于0.5时,使用Math.round方法取整后值就变为0。
Sentinel支持的最小等待时间的单位是毫秒,这可能是出于性能的考虑。当限流阈值超过1000后,如果costTime的计算结果不小于0.5,那么时间间隔都是1毫秒,这相当于仍然限流1000QPS;而当costTime的计算结果小于0.5时,使用Math.round方法取整后值为0,即请求通过的时间间隔为0毫秒,也就是不排队等待,此时限流规则就完全无效了。
冷启动限流效果控制器
Warm Up,即冷启动。在应用升级重启时,应用自身需要一个预热的过程,因为只有预热之后才能到达稳定的性能状态。在接口预热阶段可以完成JIT即时编译,完成一些单例对象的创建、线程池的创建,完成各种连接池的初始化并执行首次需要加锁执行的代码块。
冷启动并非只在应用重启时需要,以下这些场景也需要冷启动:在一段时间内没有访问的情况下,连接池中存在大量过期连接需要待下次使用才移除并创建新的连接、一些热点数据缓存过期需要重新查找数据库并写入缓存,等等。
WarmUpController支持设置冷启动周期,即冷启动的时长,默认为10秒。
WarmUpController控制流量在冷启动周期内平缓地增长到限流阈值。
例如,某个接口限流为200QPS,预热时间为10秒,那么在这10秒内,相当于每秒的限流阈值分别为5QPS、15QPS、35QPS、70QPS、90QPS、115QPS、145QPS、170QPS、190QPS、200QPS。当然,这组数据只是假设的。
如果要使用WarmUpController,则必须将限流规则阈值类型配置为GRADE_QPS,代码如下。
Sentinel的冷启动限流算法参考了Guava的SmoothRateLimiter的冷启动限流算法,但两者在实现上有很大的区别。Sentinel主要用于控制QPS,不会控制每个请求的时间间隔。正因为与Guava有所不同,官方文档目前没有很详细地介绍算法的具体实现,单看源码很难揣摩作者的思路,因此我们也不过深地去分析WarmUpController的实现源码,只是结合Guava的实现算法进行简单介绍。
Guava的SmoothRateLimiter基于令牌桶(TokenBucket)算法实现冷启动。
我们先看图5.6,从而了解SmoothRateLimiter中的一些基础知识。
如图5.6所示,横坐标(storedPermits)代表存令牌桶中的令牌数,纵坐标(throttling)代表获取一个令牌需要的时间,即请求通过的时间间隔。
• stableInterval:稳定产生令牌的时间间隔。
• coldInterval:冷启动产生令牌的最大时间间隔,等于稳定产生令牌的时间间隔乘以冷启动系数(即stableInterval×coldFactor)。
• thresholdPermits:令牌桶中剩余令牌数的阈值,介于以正常速率生产令牌还是以冷启动速率生产令牌的阈值,是判断是否需要进入冷启动阶段的依据。
• maxPermits:允许令牌桶中存放的最大令牌数。
• slope:直线的斜率。
• warmupPeriod:预热时长,即冷启动周期,对应图5.6中梯形的面积。
图5.6 冷启动算法
假设设置冷启动周期(warmupPeriod)为10s,限流为每秒钟生产200个令牌。
那么,用1秒(转换为微秒)除以每秒需要生产的令牌数,计算出生产令牌的时间间隔(stableInterval)为5000μs,对应SmoothRateLimiter的源码如下。
在SmoothRateLimiter中,冷启动系数(coldFactor)的值固定为3,所以冷启动阶段生产令牌的最长时间间隔(coldInterval)等于稳定速率下生产令牌的时间间隔(stableInterval)乘以3,即15000μs。
由于coldFactor等于3,且coldInterval等于stableInterval乘以coldFactor,故(coldIntervalstableInterval)是stableInterval的两倍,因此从thresholdPermits到0的时间是从maxPermits到thresholdPermits时间的一半,也就是warmupPeriod的一半。
因为梯形的面积等于warmupPeriod,所以点stableInterval与点thresholdPermits所在的长方形面积是梯形面积的一半,即长方形面积为warmupPeriod/2。
根据长方形的面积计算公式:面积=长×宽。
可得:stableInterval×thresholdPermits=0.5×warmupPeriod。
所以:thresholdPermits=0.5×
warmupPeriod/stableInterval。
对应SmoothRateLimiter的源码如下。
因此,在本例中,thresholdPermits=0.5×10s/5000μs,计算结果为1000(个)。
根据梯形面积公式:(上底+下底)×高/2。
可得:warmupPeriod=((stableInterval+coldInterval)×(
maxPermits-thresholdPermits))/2。
所以:maxPermits=thresholdPermits+2×warmupPeriod/(stableInterval+coldInterval)。
对应SmoothRateLimiter的源码如下。
因此,在本例中,maxPermits=1000+2.0×10s/(20000μs),计算结果为2000(个)。
根据直线的斜率计算公式:斜率=(y2-y1)/(x2-x1)。
可得:slope=(coldInterval - stableInterval)/(maxPermits - thresholdPermits)。
因此,在本例中,slope=10000μs/1000个,计算结果为101s/个。
在正常情况下,令牌以稳定时间间隔stableInterval生产令牌,1秒内能生产的令牌就刚好是限流的阈值。
假设当初始化令牌数为maxPermits时,系统直接进入冷启动阶段,此时生产令牌的时间间隔最长,等于coldInterval。如果此时以稳定的速率消费令牌桶中的令牌,由于消费速率大于生产速率,那么令牌桶中的令牌将会慢慢减少;当令牌桶中的令牌数慢慢下降到thresholdPermits时,冷启动周期结束,接下来将会以稳定的时间间隔stableInterval生产令牌。当消费速率等于生产速率时,令牌数稳定在限流阈值;而当消费速率远远小于生产速率时,令牌桶中的令牌就会堆积,如果堆积的令牌数超过thresholdPermits,又会是一轮新的冷启动。
在SmoothRateLimiter中,当每个请求获取令牌时,根据当前时间与上一次生产令牌时间(nextFreeTicketMicros)的时间间隔计算需要的新令牌数并将令牌加入令牌桶中。在应用重启时或接口很久没有被访问时,nextFreeTicketMicros的值要么为0,要么远远小于当前时间,所以当前时间与nextFreeTicketMicros的时间间隔非常大,导致第一次生产的令牌数就达到了maxPermits,直接进入冷启动阶段。
SmoothRateLimiter#resync方法的源码如下。
在了解了Guava的SmoothRateLimiter实现后,我们再来看一下Sentinel的WarmUpController,其源码如下。
• warningToken:等同于thresholdPermits,在稳定的令牌生产速率下,令牌桶中存储的令牌数。
• maxToken:等同于maxPermits,令牌桶的最大容量。
• storedTokens:令牌桶当前存储的令牌数。
• lastFilledTime:上一次生产令牌的时间。
• coldFactor:冷启动系数,默认为3。
• slope:斜率,每秒放行请求数的增长速率。
• count:限流阈值的QPS。
提示:warningToken、maxToken和slope的计算可参考Guava的SmoothRateLimiter。
WarmUpController#canPass方法的源码如下。
如源码所示,在canPass方法中,首先获取当前令牌桶中的令牌数,若大于warningToken,则控制QPS。
根据当前令牌桶中存储的令牌数超出warningToken的令牌数,计算当前秒需要控制的QPS的阈值,下面这两行代码是关键。
我们可以通过图5.7来理解这个公式。
结合图5.7可以看出以下两点。
图5.7 Sentinel的冷启动算法
• 图中的x1虚线的长度等于aboveToken。
• 生产令牌的间隔时间等于y1的长度加上stableInterval,其在Sentinel中的单位为秒。
根据斜率和x1可以计算出y1的值。
y1=slope×aboveToken。
而1.0/count计算出来的值是正常情况下每隔多少毫秒生产一个令牌,即stableInterval。
所以,计算warningQps的公式等同于下面这行代码。
当前生产令牌的时间间隔:aboveToken×slope+stableInterval=stableInterval+y1。
当前秒所能生产的令牌数:1.0/(stableInterval+y1)。
所以,warningQps等于当前秒所能生产的令牌数。
Sentinel中的resync方法与SmoothRateLimiter的resync方法不同,在Sentinel中每秒只生产一个令牌。WarmUpController的syncToken方法的源码如下。
Sentinel并不是每通过一个请求就从令牌桶中移除一个令牌,而是在每秒更新令牌桶的令牌数时再扣除上一秒消耗的令牌数,上一秒消耗的令牌数等于上一秒通过的请求数,这就是官方文档所写的每秒会自动掉落令牌。这种做法避免了每次放行请求都需要使用CAS更新令牌桶中的令牌数,可以降低Sentinel对应用性能的影响,是一种非常巧妙的做法。
更新令牌桶中的令牌数=当前令牌桶中剩余的令牌数+当前秒需要生产的令牌数。
coolDownTokens方法的源码如下。
其中,
currentTime-lastFilledTime.get()为当前时间与上一次生产令牌时间的时间间隔,虽然单位为毫秒,但是已经去掉了毫秒的部分(毫秒部分全为0)。
如果
currentTime-lastFilledTime.get()等于1秒,则根据1秒等于1000毫秒,新生产的令牌数等于限流阈值(count),新的令牌桶中的剩余令牌数(newValue)等于当前剩余令牌数(oldValue)加新生产的令牌数。
newValue=oldValue+1000×count/1000=oldValue+count。
如果是在很久没有访问的情况下,lastFilledTime远远小于currentTime,则第一次生产的令牌数将等于maxToken。
小结
本篇主要分析了Sentinel限流功能的实现原理、can pass check全过程,以及流量效果控制的实现原理,同时详细地介绍了Sentinel实现匀速限流与冷启动效果所采用的算法。