距離上次總結Sentinel的滑動窗口算法已經有些時間了,本來想着一口氣將它的core模塊所有總結完,可是中間一懶就又鬆懈下來了,這幾天在工做之餘又從新整理了一下,在這裏作一個學習總結。html
上篇滑動窗口算法總結連接:https://www.cnblogs.com/mrxiaobai-wen/p/14212637.htmljava
今天主要總結了一下Sentinel的快速失敗和勻速排隊的漏桶算法。由於它的WarmUpController和WarmUpRateLimiterController對應的令牌桶算法的數學計算原理有一點點複雜,因此我準備在後面單獨用一篇來總結。因此今天涉及到的主要就是DefaultController和RateLimiterController。node
首先進入到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()); }
默認流控算法代碼以下:api
@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方法:app
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數。ide
而後回到上面的canPass方法,其主要邏輯就是在獲取到目前節點的統計數據後,將已佔用的令牌數與請求的令牌數相加,若是小於設定的閾值,那麼直接放行。學習
若是大於設置的閾值,那麼在閾值類型爲QPS且容許優先處理先到的請求的狀況下進行特殊處理,不然返回false不放行。ui
上面特殊處理就是:首先嚐試去佔用後面的時間窗口的令牌,獲取到等待時間,若是等待時間小於設置的最長等待時長,那麼就進行等待,當等待到指定時間後返回。不然直接返回false不放行。.net
由代碼能夠看出,在等待指定時長後,拋出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異常捕捉的代碼中沒有繼續拋出,因此對該請求進行了放行。
對於漏桶算法,首先在網上盜用一張圖以下:
圖片來源: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之內的請求。