溫馨提示,如果大家對源碼不感興趣,可以直接跳到本文的總結部分,瞭解一下預熱實現原理的一些實戰建議。
首先先回顧一下 Sentinel 流控效果相關的類圖:
DefaultController 快速失敗已經在上文詳細介紹過,本文將詳細介紹其他兩種策略的實現原理。
首先我們應該知道,一條流控規則(FlowRule)對應一個 TrafficShapingController 對象。
1、RateLimiterController
勻速排隊策略實現類,首先我們先來介紹一下該類的幾個成員變量的含義:
- int maxQueueingTimeMs
排隊等待的最大超時時間,如果等待超過該時間,將會拋出 FlowException。 - double count
流控規則中的闊值,即令牌的總個數,以QPS爲例,如果該值設置爲1000,則表示1s可併發的請求數量。 - AtomicLong latestPassedTime
上一次成功通過的時間戳。
接下來我們詳細來看一下其算法的實現:
RateLimiterController#canPass
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) {
return true;
}
if (count <= 0) {
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
long costTime = Math.round(1.0 * (acquireCount) / count * 1000); // @1
long expectedTime = costTime + latestPassedTime.get(); // @2
if (expectedTime <= currentTime) { // @3
latestPassedTime.set(currentTime);
return true;
} else {
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis(); // @4
if (waitTime > maxQueueingTimeMs) { // @5
return false;
} else {
long oldTime = latestPassedTime.addAndGet(costTime); // @6
try {
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
if (waitTime > 0) { // @7
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
代碼@1:首先算出每一個請求之間最小的間隔,時間單位爲毫秒。例如 cout 設置爲 1000,表示一秒可以通過 1000個請求,勻速排隊,那每個請求的間隔爲 1 / 1000(s),乘以1000將時間單位轉換爲毫秒,如果一次需要2個令牌,則其間隔時間爲2ms,用 costTime 表示。
代碼@2:計算下一個請求的期望達到時間,等於上一次通過的時間戳 + costTime ,用 expectedTime 表示。
代碼@3:如果 expectedTime 小於等於當前時間,說明在期望的時間沒有請求到達,說明沒有按照期望消耗令牌,故本次請求直接通過,並更新上次通過的時間爲當前時間。
代碼@4:如果 expectedTime 大於當前時間,說明還沒到令牌發放時間,當前請求需要等待。首先先計算需要等待是時間,用 waitTime 表示。
代碼@5:如果計算的需要等待的時間大於允許排隊的時間,則返回 false,即本次請求將被限流,返回 FlowException。
代碼@6:進入排隊,默認是本次請求通過,故先將上一次通過流量的時間戳增加 costTime,然後直接調用 Thread 的 sleep 方法,將當前請求先阻塞一會,然後返回 true 表示請求通過。
勻速排隊模式的實現的關鍵:主要是記錄上一次請求通過的時間戳,然後根據流控規則,判斷兩次請求之間最小的間隔,並加入一個排隊時間。
2、WarmUpController
預熱策略的實現,首先我們先來介紹一下該類的幾個成員變量的含義:
- double count
流控規則設定的闊值。 - int coldFactor
冷卻因子。 - int warningToken
告警token,對應 Guava 中的 RateLimiter 中的 - int maxToken
double slope
AtomicLong storedTokens
AtomicLong lastFilledTime
2.1 WarmUpController 構造函數
內部的構造函數,最終將調用 construct 方法。
WarmUpController#construct
private void construct(double count, int warmUpPeriodInSec, int coldFactor) { // @1
if (coldFactor <= 1) {
throw new IllegalArgumentException("Cold factor should be larger than 1");
}
this.count = count;
this.coldFactor = coldFactor;
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1); // @2
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor)); // @3
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
}
要理解該方法,就需要理解 Guava 框架的 SmoothWarmingUp 相關的預熱算法,其算法原理如圖所示:
關於該圖的詳細介紹,請參考筆者的另外一篇博文:源碼分析RateLimiter SmoothWarmingUp 實現原理,對該圖進行了詳細解讀。
代碼@1:首先介紹該方法的參數列表:
- double count
限流規則配置的闊值,例如是按 TPS 類型來限流,如果限制爲100tps,則該值爲100。 - int warmUpPeriodInSec
預熱時間,單位爲秒,通用在限流規則頁面可配置。 - int coldFactor
冷卻因子,這裏默認爲3,與 RateLimiter 中的冷卻因子保持一致,表示的含義爲 coldIntervalMicros 與 stableIntervalMicros 的比值。
代碼@2:計算 warningToken 的值,與 Guava 中的 RateLimiter 中的 thresholdPermits 的計算算法公式相同,thresholdPermits = 0.5 * warmupPeriod / stableInterval,在Sentienl 中,而 stableInteral = 1 / count,thresholdPermits 表達式中的 0.5 就是因爲 codeFactor 爲3,因爲 warm up period與 stable 面積之比等於 (coldIntervalMicros - stableIntervalMicros ) 與 stableIntervalMicros 的比值,這個比值又等於 coldIntervalMicros / stableIntervalMicros - stableIntervalMicros / stableIntervalMicros 等於 coldFactor - 1。
代碼@3:同樣根據 Guava 中的 RateLimiter 關於 maxToken 也能理解。
2.2 canPass 方法詳解
WarmUpController#canPass
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps(); // @1
long previousQps = (long) node.previousPassQps(); // @2
syncToken(previousQps); // @3
// 開始計算它的斜率
// 如果進入了警戒線,開始調整他的qps
long restToken = storedTokens.get();
if (restToken >= warningToken) { // @4
long aboveToken = restToken - warningToken;
// 消耗的速度要比warning快,但是要比慢
// current interval = restToken*slope+1/count
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
return true;
}
} else { // @5
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
代碼@1:先獲取當前節點已通過的QPS。
代碼@2:獲取當前滑動窗口的前一個窗口收集的已通過QPS。
代碼@3:調用 syncToken 更新 storedTokens 與 lastFilledTime 的值,即按照令牌發放速率發送指定令牌,將在下文詳細介紹 syncToken 方法內部的實現細節。
代碼@4:如果當前存儲的許可大於warningToken的處理邏輯,主要是在預熱階段允許通過的速率會比限流規則設定的速率要低,判斷是否通過的依據就是當前通過的TPS與申請的許可數是否小於當前的速率(這個值加入斜率,即在預熱期間,速率是慢慢達到設定速率的。
代碼@5:當前存儲的許可小於warningToken,則按照規則設定的速率進行判定。
不知大家有沒有一個疑問,爲什麼 storedTokens 剩餘許可數越大,限制其通過的速率竟然會越慢,這又怎麼理解呢?大家可以思考一下這個問題,將在本文的總結部分進行解答。
我們先來看一下 syncToken 的實現細節,即更新 storedTokens 的邏輯。
WarmUpController#syncToken
protected void syncToken(long passQps) {
long currentTime = TimeUtil.currentTimeMillis();
currentTime = currentTime - currentTime % 1000; // @1
long oldLastFillTime = lastFilledTime.get();
if (currentTime <= oldLastFillTime) { // @2
return;
}
long oldValue = storedTokens.get();
long newValue = coolDownTokens(currentTime, passQps); // @3
if (storedTokens.compareAndSet(oldValue, newValue)) {
long currentValue = storedTokens.addAndGet(0 - passQps); // @4
if (currentValue < 0) {
storedTokens.set(0L);
}
lastFilledTime.set(currentTime);
}
}
代碼@1:這個是計算出當前時間秒的最開始時間。例如當前是 2020-04-06 08:29:01:056,該方法返回的時間爲 2020-04-06 08:29:01:000。
代碼@2:如果當前時間小於等於上次發放許可的時間,則跳過,無法發放令牌,即每秒發放一次令牌。
代碼@3:具體方法令牌的邏輯,稍後詳細介紹。
代碼@4:更新剩餘令牌,即生成的許可後要減去上一秒通過的令牌。
我們詳細來看一下 coolDownTokens 方法。
WarmUpController#coolDownTokens
private long coolDownTokens(long currentTime, long passQps) {
long oldValue = storedTokens.get();
long newValue = oldValue;
// 添加令牌的判斷前提條件:
// 當令牌的消耗程度遠遠低於警戒線的時候
if (oldValue < warningToken) { // @1
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
} else if (oldValue > warningToken) { // @2
if (passQps < (int)count / coldFactor) {
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
}
}
return Math.min(newValue, maxToken);// @3
}
代碼@1:如果當前剩餘的 token 小於警戒線,可以按照正常速率發放許可。
代碼@2:如果當前剩餘的 token 大於警戒線但前一秒的QPS小於 (count 與 冷卻因子的比),也發放許可(這裏我不是太明白其用意)。
代碼@3:這裏是關鍵點,第一次運行,由於 lastFilledTime 等於0,這裏將返回的是 maxToken,故這裏一開始的許可就會超過 warningToken,啓動預熱機制,進行速率限制。
3、總結
WarmUpController 這個預熱算法還是挺複雜的,接下來我們來總結一下它的特徵。
不知大家有沒有一個疑問,爲什麼 storedTokens 剩餘許可數越大,限制其通過的速率竟然會越慢,這又怎麼理解呢?
這裏感覺有點逆向思維的味道,因爲一開始就會將 storedTokens 的值設置爲 maxToken,即開始就會超過 warningToken,從而一開始進入到預熱階段,此時的速率有一個爬坡的過程,類似於數學中的斜率,達到其他啓動預熱的效果。
實戰指南:注意 warmUpPeriodInSec 與 coldFactor 的設置,將會影響最終的限流效果。
爲了更加直觀的理解,我們舉例如下,warningToken 與 maxToken 的生成公式如下:
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
coldFactor 設定爲 3,例如限流規則中配置每秒允許通過的許可數量爲 10,即 count 值等於 10,我們改變 warmUpPeriodInSec 的值來看一下 warningToken 與 maxToken 的值,以此來探究 Sentinel WarmUpController 的工作機制或工作效果。
warmUpPeriodInSec | warningToken | maxToken |
---|---|---|
1 | 5 | 10 |
2 | 10 | 20 |
3 | 15 | 30 |
4 | 20 | 40 |
根據上面的算法,如果 warningToken 的值小於 count,則限流會變的更嚴厲,即最終的限流TPS會小於設置的TPS。即 warmUpPeriodInSec 設置過大過小都不合適,其標準是要使得 warningToken 的值大於 count。
如果文章對你有所幫助的話,還請點個贊,謝謝。
作者信息:丁威,《RocketMQ技術內幕》作者、CSDN博客專家,原創公衆號『中間件興趣圈』維護者。擅長JAVA編程,對主流中間件 RocketMQ、Dubbo、ElasticJob、Netty、Sentinel、Mybatis、Mycat 等中間件有深入研究。歡迎加入筆者的知識星球,一起探討高併發、分佈式服務架構,分享閱讀源碼心得。