關(guān)于Sentinel中冷啟動(dòng)限流原理WarmUpController
冷啟動(dòng)
所謂冷啟動(dòng),或預(yù)熱是指,系統(tǒng)長時(shí)間處理低水平請(qǐng)求狀態(tài),當(dāng)大量請(qǐng)求突然到來時(shí),并非所有請(qǐng)求都放行,而是慢慢的增加請(qǐng)求,目的時(shí)防止大量請(qǐng)求沖垮應(yīng)用,達(dá)到保護(hù)應(yīng)用的目的。
Sentinel中冷啟動(dòng)是采用令牌桶算法實(shí)現(xiàn)。
令牌桶算法圖例如下:

預(yù)熱模型
Sentinel中的令牌桶算法,是參照Google Guava中的RateLimiter,在學(xué)習(xí)Sentinel中預(yù)熱算法之前,先了解下整個(gè)預(yù)熱模型,如下圖:

Guava中預(yù)熱是通過控制令牌的生成時(shí)間,而Sentinel中實(shí)現(xiàn)不同:
- 不控制每個(gè)請(qǐng)求通過的時(shí)間間隔,而是控制每秒通過的請(qǐng)求數(shù)。
- 在Guava中,冷卻因子coldFactor固定為3,上圖中②是①的兩倍
- Sentinel增加冷卻因子coldFactor的作用,在Sentinel模型中,②是①的(coldFactor-1)倍,coldFactor默認(rèn)為3,可以通過csp.sentinel.flow.cold.factor參數(shù)修改
原理分析
Sentinel中冷啟動(dòng)對(duì)應(yīng)的FlowRule配置為RuleConstant.CONTROL_BEHAVIOR_WARM_UP,對(duì)應(yīng)的Controller為WarmUpController,首先了解其中的屬性和構(gòu)造方法:
count:FlowRule中設(shè)定的閾值warmUpPeriodSec:系統(tǒng)預(yù)熱時(shí)間,代表上圖中的②coldFactor:冷卻因子,默認(rèn)為3,表示倍數(shù),即系統(tǒng)最"冷"時(shí)(令牌桶飽和時(shí)),令牌生成時(shí)間間隔是正常情況下的多少倍warningToken:預(yù)警值,表示進(jìn)入預(yù)熱或預(yù)熱完畢maxToken:最大可用token值,計(jì)算公式:warningToken+(2*時(shí)間*閾值)/(1+因子),默認(rèn)情況下為warningToken的2倍slope:斜度,(coldFactor-1)/count/(maxToken-warningToken),用于計(jì)算token生成的時(shí)間間隔,進(jìn)而計(jì)算當(dāng)前token生成速度,最終比較token生成速度與消費(fèi)速度,決定是否限流storedTokens:姑且可以理解為令牌桶中令牌的數(shù)量
public class WarmUpController implements TrafficShapingController {
// FlowRule中設(shè)置的閾值
protected double count;
// 冷卻因子,默認(rèn)為3,通過SentinelConfig加載,可以修改
private int coldFactor;
// 預(yù)警token數(shù)量
protected int warningToken = 0;
// 最大token數(shù)量
private int maxToken;
// 斜率,用于計(jì)算當(dāng)前生成token的時(shí)間間隔,即生成速率
protected double slope;
// 令牌桶中剩余令牌數(shù)
protected AtomicLong storedTokens = new AtomicLong(0);
// 最后一次添加令牌的時(shí)間戳
protected AtomicLong lastFilledTime = new AtomicLong(0);
public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
construct(count, warmUpPeriodInSec, coldFactor);
}
public WarmUpController(double count, int warmUpPeriodInSec) {
construct(count, warmUpPeriodInSec, 3);
}
private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
if (coldFactor <= 1) {
throw new IllegalArgumentException("Cold factor should be larger than 1");
}
this.count = count;
// 默認(rèn)為3
this.coldFactor = coldFactor;
// thresholdPermits = 0.5 * warmupPeriod / stableInterval.
// warningToken = 100;
// 計(jì)算預(yù)警token數(shù)量
// 例如 count=5,warmUpPeriodInSec=10,coldFactor=3,則waringToken=5*10/2=25
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
// / maxPermits = thresholdPermits + 2 * warmupPeriod / (stableInterval + coldInterval)
// maxToken = 200
// 最大token數(shù)量=25+2*10*5/4=50
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
// slope
// slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits- thresholdPermits);
// 傾斜度=(3-1)/5/(50-25) = 0.016
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
}
}
舉例說明:
FlowRule設(shè)定閾值count=5,即1s內(nèi)QPS閾值為5,設(shè)置的預(yù)熱時(shí)間默認(rèn)為10s,即warmUpPeriodSec=10,冷卻因子coldFactor默認(rèn)為3,即count = 5,coldFactor=3,warmUpPeriodSec=10,則
stableInterval=1/count=200ms,coldInterval=coldFactor*stableInterval=600ms warningToken=warmUpPeriodSec/(coldFactor-1)/stableInterval=(warmUpPeriodSec*count)/(coldFactor-1)=25 maxToken=2warmUpPeriodSec/(stableInterval+coldInterval)+warningToken=warningToken+2warmUpPeriodSeccount/(coldFactor+1)=50 slope=(coldInterval-stableInterval)/(maxToken-warningToken)=(coldFactor-1)/count/(maxToken-warningToken)=0.016
接下來學(xué)習(xí),WarmUpController是如何進(jìn)行限流的,進(jìn)入canPass()方法:
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
// 獲取當(dāng)前1s的QPS
long passQps = (long) node.passQps();
// 獲取上一窗口通過的qps
long previousQps = (long) node.previousPassQps();
// 生成和滑落token
syncToken(previousQps);
// 如果進(jìn)入了警戒線,開始調(diào)整他的qps
long restToken = storedTokens.get();
// 如果令牌桶中的token數(shù)量大于警戒值,說明還未預(yù)熱結(jié)束,需要判斷token的生成速度和消費(fèi)速度
if (restToken >= warningToken) {
long aboveToken = restToken - warningToken;
// 消耗的速度要比warning快,但是要比慢
// y軸,當(dāng)前token生成時(shí)間 current interval = restToken*slope+stableInterval
// 計(jì)算此時(shí)1s內(nèi)能夠生成token的數(shù)量
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
// 判斷token消費(fèi)速度是否小于生成速度,如果是則正常請(qǐng)求,否則限流
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
// 預(yù)熱結(jié)束,直接判斷是否超過設(shè)置的閾值
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
canPass()方法分為3個(gè)階段:
syncToken():負(fù)責(zé)令牌的生產(chǎn)和滑落
判斷令牌桶中剩余令牌數(shù)
- 如果剩余令牌數(shù)大于警戒值,說明處于預(yù)熱階段,需要比較令牌的生產(chǎn)速率與令牌的消耗速率。若消耗速率大,則限流;否則請(qǐng)求正常通行
仍然以count=5進(jìn)行舉例,警戒線warningToken=25,maxToken=50
假設(shè)令牌桶中剩余令牌數(shù)storedTokens=30,即在預(yù)熱范圍內(nèi),此時(shí)restToken=30,slope=0.016,則aboveToken=30-25=5
由斜率slope推導(dǎo)當(dāng)前token生成時(shí)間間隔:(restToken-warningToken)*slope+stableInterval=5*0.016+1/5=0.28,即280ms生成一個(gè)token
此時(shí)1s內(nèi)生成token的數(shù)量=1/0.28≈4,即1s內(nèi)生成4個(gè)token
假設(shè)當(dāng)前窗口通過的請(qǐng)求數(shù)量passQps=4,acquiredCount=1,此時(shí)passQps+acquiredCount=5>4,即令牌消耗速度大于生產(chǎn)速度,則限流
- 如果剩余令牌數(shù)小于警戒值,說明系統(tǒng)已經(jīng)處于高水位,請(qǐng)求穩(wěn)定,則直接判斷QPS與閾值,超過閾值則限流
接下來分析Sentinel是如何生產(chǎn)及滑落token的,進(jìn)入到syncToken()方法:
獲取當(dāng)前時(shí)間秒數(shù)currentTime,與lastFilledTime進(jìn)行比較,之所以取秒數(shù),是因?yàn)闀r(shí)間窗口的設(shè)定為1s,若兩個(gè)時(shí)間相等,說明還處于同一秒內(nèi),不進(jìn)行token填充和滑落,避免重復(fù)問題
令牌桶中添加token
- 當(dāng)流量極大,令牌桶中剩余token遠(yuǎn)低于預(yù)警值時(shí),添加token
- 處于預(yù)熱節(jié)點(diǎn),單令牌的消耗速度小于系統(tǒng)最冷時(shí)令牌的生成速度,則添加令牌
通過CAS操作,修改storedToken,并進(jìn)行令牌扣減
protected void syncToken(long passQps) {
long currentTime = TimeUtil.currentTimeMillis();
// 獲取整秒數(shù)
currentTime = currentTime - currentTime % 1000;
// 上一次的操作時(shí)間
long oldLastFillTime = lastFilledTime.get();
// 判斷成立,如果小于,說明可能出現(xiàn)了時(shí)鐘回?fù)?
// 如果等于,說明當(dāng)前請(qǐng)求都處于同一秒內(nèi),則不進(jìn)行token添加和滑落操作,避免的重復(fù)扣減
// 時(shí)間窗口的跨度為1s
if (currentTime <= oldLastFillTime) {
return;
}
// token數(shù)量
long oldValue = storedTokens.get();
long newValue = coolDownTokens(currentTime, passQps);
// 重置token數(shù)量
if (storedTokens.compareAndSet(oldValue, newValue)) {
// token滑落,即token消費(fèi)
// 減去上一個(gè)時(shí)間窗口的通過請(qǐng)求數(shù)
long currentValue = storedTokens.addAndGet(0 - passQps);
if (currentValue < 0) {
storedTokens.set(0L);
}
// 設(shè)置最后添加令牌時(shí)間
lastFilledTime.set(currentTime);
}
}
private long coolDownTokens(long currentTime, long passQps) {
long oldValue = storedTokens.get();
long newValue = oldValue;
// 添加令牌的判斷前提條件:
// 當(dāng)令牌的消耗程度遠(yuǎn)遠(yuǎn)低于警戒線的時(shí)候
if (oldValue < warningToken) {
// 計(jì)算過去一段時(shí)間內(nèi),可以通過的QPS總量
// 初始加載時(shí),令牌數(shù)量達(dá)到maxToken
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
} else if (oldValue > warningToken) {
// 處于預(yù)熱過程,且消費(fèi)速度低于冷卻速度,則補(bǔ)充令牌
if (passQps < (int)count / coldFactor) {
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
}
}
// 當(dāng)令牌桶滿了之后,拋棄多余的令牌
return Math.min(newValue, maxToken);
}
總結(jié)
Sentinel采用令牌桶算法實(shí)現(xiàn)預(yù)熱限流
系統(tǒng)流量突增,令牌消耗從maxPermits(令牌桶容量)到thresholdPermits(警戒線)所需要的時(shí)間,是從警戒線到0的(coldFactor-1)倍,并非其他博客中的2倍。另外,關(guān)于預(yù)熱模型中②和①的關(guān)系,是通過結(jié)果反推而來,并沒有找到模型定義的官方文檔。
Sentinel限流是針對(duì)某時(shí)刻令牌的生成與消耗速度
Sentinel通過比較整秒數(shù),來判斷是否需要進(jìn)行令牌扣減,并通過CAS操作,保證同一時(shí)刻只能由1個(gè)線程成功操作,從而避免多次扣減passQps導(dǎo)致限流失效的問題
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java整合Redis實(shí)現(xiàn)坐標(biāo)附近查詢功能
這篇文章主要介紹了Java整合Redis實(shí)現(xiàn)坐標(biāo)附近查詢,我們可以在redis服務(wù)器使用命令 help xxx 查看指令的具體用法,本文給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2023-11-11
java工具類SendEmailUtil實(shí)現(xiàn)發(fā)送郵件
這篇文章主要為大家詳細(xì)介紹了java工具類SendEmailUtil實(shí)現(xiàn)發(fā)送郵件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02
JVM(Java?Virtual?Machine,Java虛擬機(jī))的作用詳解
JVM是Java語言實(shí)現(xiàn)“一次編寫,到處運(yùn)行”特性的基石,也是Java平臺(tái)的核心組成部分,其主要作用包括平臺(tái)無關(guān)性、內(nèi)存管理、運(yùn)行Java程序、安全性以及性能優(yōu)化,通過這些功能,JVM確保了Java程序的可移植性、高效性和安全性2025-03-03
SpringBoot中Json工具類的實(shí)現(xiàn)
本文介紹在Java項(xiàng)目中實(shí)現(xiàn)一個(gè)JSON工具類,支持對(duì)象與JSON字符串之間的轉(zhuǎn)換,并提供依賴和代碼示例便于直接應(yīng)用,感興趣的可以了解一下2024-10-10
ConcurrentModificationException日志關(guān)鍵字報(bào)警思考分析
本文將記錄和分析日志中的ConcurrentModificationException關(guān)鍵字報(bào)警,還有一些我的思考,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-12-12
MyBatis Plus實(shí)現(xiàn)一對(duì)多的查詢場景的三種方法
MyBatis Plus提供了多種簡便的方式來進(jìn)行一對(duì)多子查詢,本文主要介紹了MyBatis Plus實(shí)現(xiàn)一對(duì)多的查詢場景的三種方法,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07
Java獲取Cookie里的指定值的實(shí)現(xiàn)方法
在Java中,我們經(jīng)常需要從HTTP請(qǐng)求中獲取Cookie,并從中提取特定的值,下面我們將介紹如何通過Java代碼獲取Cookie中的指定值,文章通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09
SpringBoot中Bean生命周期自定義初始化和銷毀方法詳解
這篇文章給大家詳細(xì)介紹了SpringBoot中Bean生命周期自定義初始化和銷毀方法,文中通過代碼示例講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-01-01

