距離上次總結Sentinel的滑動窗口算法已經有些時間了,原本想着一口氣將它的core模塊全部總結完,但是中間一懶就又松懈下來了,這幾天在工作之余又重新整理了一下,在這里做一個學習總結。
上篇滑動窗口算法總結鏈接:https://www.cnblogs.com/mrxiaobai-wen/p/14212637.html
今天主要總結了一下Sentinel的快速失敗和勻速排隊的漏桶算法。因為它的WarmUpController和WarmUpRateLimiterController對應的令牌桶算法的數學計算原理有一點點復雜,所以我准備在后面單獨用一篇來總結。所以今天涉及到的主要就是DefaultController和RateLimiterController。
限流策略入口
首先進入到FlowRuleUtil類中,方法generateRater就是對應策略的創建,邏輯比較簡單,代碼如下:
private static TrafficShapingController generateRater(FlowRule rule) {
if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
switch (rule.getControlBehavior()) {
case RuleConstant.CONTROL_BEHAVIOR_WARM_UP:
// WarmUp-令牌桶算法
return new WarmUpController(rule.getCount(), rule.getWarmUpPeriodSec(),
ColdFactorProperty.coldFactor);
case RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER:
// 排隊等待-漏桶算法
return new RateLimiterController(rule.getMaxQueueingTimeMs(), rule.getCount());
case RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER:
// 預熱和勻速排隊結合
return new WarmUpRateLimiterController(rule.getCount(), rule.getWarmUpPeriodSec(),
rule.getMaxQueueingTimeMs(), ColdFactorProperty.coldFactor);
case RuleConstant.CONTROL_BEHAVIOR_DEFAULT:
default:
// Default mode or unknown mode: default traffic shaping controller (fast-reject).
}
}
// 快速失敗
return new DefaultController(rule.getCount(), rule.getGrade());
}
快速失敗DefaultController
默認流控算法代碼如下:
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
int curCount = avgUsedTokens(node);
// 當前閾值 + acquireCount 是否大於規則設定的count,小於等於則表示符合閾值設定直接返回true
if (curCount + acquireCount > count) {
// 在大於的情況下,針對QPS的情況會對先進來的請求進行特殊處理
if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
long currentTime;
long waitInMs;
currentTime = TimeUtil.currentTimeMillis();
// 如果策略是QPS,那么對於優先請求嘗試去占用下一個時間窗口中的令牌
waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
sleep(waitInMs);
// PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
throw new PriorityWaitException(waitInMs);
}
}
return false;
}
return true;
}
先看一下涉及到的avgUsedTokens方法:
private int avgUsedTokens(Node node) {
if (node == null) {
return DEFAULT_AVG_USED_TOKENS;
}
// 獲取當前qps或者當前線程數
return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}
主要是獲取已使用的令牌數,如果設置的閾值類型為線程數,那么返回當前統計節點中保存的線程數,如果設置的閾值類型為QPS,那么返回已經通過的QPS數。
然后回到上面的canPass方法,其主要邏輯就是在獲取到目前節點的統計數據后,將已占用的令牌數與請求的令牌數相加,如果小於設定的閾值,那么直接放行。
如果大於設置的閾值,那么在閾值類型為QPS且允許優先處理先到的請求的情況下進行特殊處理,否則返回false不放行。
上面特殊處理就是:首先嘗試去占用后面的時間窗口的令牌,獲取到等待時間,如果等待時間小於設置的最長等待時長,那么就進行等待,當等待到指定時間后返回。否則直接返回false不放行。
由代碼可以看出,在等待指定時長后,拋出PriorityWaitException進行放行,對應實現的地方在StatisticSlot中,對應entry方法代碼如下:
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
try {
// Do some checking.
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// 說明:省略了執行通過的處理邏輯
} catch (PriorityWaitException ex) {
node.increaseThreadNum();
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseThreadNum();
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseThreadNum();
}
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (BlockException e) {
// 說明:省略了阻塞異常處理邏輯
throw e;
} catch (Throwable e) {
context.getCurEntry().setError(e);
throw e;
}
}
對這個方法去除了其它多余代碼,可以看出在PriorityWaitException異常捕捉的代碼中沒有繼續拋出,所以對該請求進行了放行。
勻速排隊-漏桶算法RateLimiterController
對於漏桶算法,首先在網上盜用一張圖如下:
圖片來源:https://blog.csdn.net/tianyaleixiaowu/article/details/74942405
其思路是:水流(請求)先進入到漏桶里,漏桶以一定的速率勻速流出,當流入量過大的時候,多余水流(請求)直接溢出,從而達到對系統容量的保護。
對應Sentinel使用漏桶算法進行流量整形的效果就如下圖所示:
來看RateLimiterController的canPass方法:
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) {
return true;
}
if (count <= 0) {
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
// 計算此次令牌頒發所需要的時間,其中: (1.0 / count * 1000)代表每個令牌生成的耗時,然后乘以acquireCount得到此次所需令牌生成耗時
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
// 在上次通過時間的基礎上加上本次的耗時,得到期望通過的時間點
long expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {
// 如果期望時間小於當前時間,那么說明當前令牌充足,可以放行,同時將當前時間設置為上次通過時間
latestPassedTime.set(currentTime);
return true;
} else {
// 當期望時間大於當前時間,那么說明令牌不夠,需要等待
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
// 如果需要等待時間大於設置的最大等待時長,那么直接丟棄,不用等待,下面同理
return false;
} else {
long oldTime = latestPassedTime.addAndGet(costTime);
try {
// 再次檢查等待時長
waitTime = oldTime - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
// in race condition waitTime may <= 0
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
Sentinel的令牌桶算法和漏桶算法都參考了Guava RateLimiter的設計。
上面的邏輯很清晰,其思路就是根據當前令牌請求數量acquireCount乘以令牌生成速率得到本次所需令牌的生成時間,然后加上上次通過時間得到一個本次請求的期望通過時間,如果期望通過時間小於當前時間那么說明容量足夠直接通過,如果期望通過時間大於當前時間那么說明系統容量不夠需要等待,然后結合設置的等待時間判斷是繼續等待還是直接放棄。
需要特別注意的是,勻速模式具有局限性,它只支持1000以內的QPS。我們可以看對應的語句:
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
long expectedTime = costTime + latestPassedTime.get();
很容易得到如下結果,每種閾值對應的令牌生成時間(單位:毫秒):
count | costTime |
---|---|
100 | 10 |
1000 | 1 |
2000 | 1 |
3000 | 0 |
所以當閾值count大於2000后,每個令牌生成的時間間隔計算為0,那么后面的判斷就沒有意義了。所以Sentinel的勻速器只支持QPS在1000以內的請求。