目錄 上一篇《Spring Cloud Alibaba | Sentinel: 服務(wù)限流基礎(chǔ)篇》我們介紹了資源和規(guī)則,幾種主流框架的默認(rèn)適配,我們接著聊一下熔斷降級和幾種其他的限流方式。 1. 熔斷降級除了流量控制以外,對調(diào)用鏈路中不穩(wěn)定的資源進(jìn)行熔斷降級也是保障高可用的重要措施之一。由于調(diào)用關(guān)系的復(fù)雜性,如果調(diào)用鏈路中的某個資源不穩(wěn)定,最終會導(dǎo)致請求發(fā)生堆積。Sentinel 熔斷降級會在調(diào)用鏈路中某個資源出現(xiàn)不穩(wěn)定狀態(tài)時(例如調(diào)用超時或異常比例升高),對這個資源的調(diào)用進(jìn)行限制,讓請求快速失敗,避免影響到其它的資源而導(dǎo)致級聯(lián)錯誤。當(dāng)資源被降級后,在接下來的降級時間窗口之內(nèi),對該資源的調(diào)用都自動熔斷(默認(rèn)行為是拋出 DegradeException)。 1.1 降級策略我們通常用以下幾種方式來衡量資源是否處于穩(wěn)定的狀態(tài):
注意:異常降級僅針對業(yè)務(wù)異常,對 Entry entry = null; try { entry = SphU.entry(key, EntryType.IN, key); // Write your biz code here. // <<BIZ CODE>> } catch (Throwable t) { if (!BlockException.isBlockException(t)) { Tracer.trace(t); } } finally { if (entry != null) { entry.exit(); } } 開源整合模塊,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 2. 熱點參數(shù)限流何為熱點?熱點即經(jīng)常訪問的數(shù)據(jù)。很多時候我們希望統(tǒng)計某個熱點數(shù)據(jù)中訪問頻次最高的 Top K 數(shù)據(jù),并對其訪問進(jìn)行限制。比如:
熱點參數(shù)限流會統(tǒng)計傳入?yún)?shù)中的熱點參數(shù),并根據(jù)配置的限流閾值與模式,對包含熱點參數(shù)的資源調(diào)用進(jìn)行限流。熱點參數(shù)限流可以看做是一種特殊的流量控制,僅對包含熱點參數(shù)的資源調(diào)用生效。 Sentinel 利用 LRU 策略統(tǒng)計最近最常訪問的熱點參數(shù),結(jié)合令牌桶算法來進(jìn)行參數(shù)級別的流控。 2.1 項目依賴<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-parameter-flow-control</artifactId> <version>x.y.z</version> </dependency> 然后為對應(yīng)的資源配置熱點參數(shù)限流規(guī)則,并在 entry 的時候傳入相應(yīng)的參數(shù),即可使熱點參數(shù)限流生效。
那么如何傳入對應(yīng)的參數(shù)以便 Sentinel 統(tǒng)計呢?我們可以通過 SphU 類里面幾個 entry 重載方法來傳入: public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException 其中最后的一串 // paramA in index 0, paramB in index 1. // 若需要配置例外項或者使用集群維度流控,則傳入的參數(shù)只支持基本類型。 SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB); 注意 :若 entry 的時候傳入了熱點參數(shù),那么 exit 的時候也一定要帶上對應(yīng)的參數(shù)(exit(count, args)),否則可能會有統(tǒng)計錯誤。正確的示例: Entry entry = null; try { entry = SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB); // Your logic here. } catch (BlockException ex) { // Handle request rejection. } finally { if (entry != null) { entry.exit(1, paramA, paramB); } } 對于 @SentinelResource("myMethod") public Result doSomething(String uid, int type) { // some logic here... } 2.2 熱點參數(shù)規(guī)則熱點參數(shù)規(guī)則(
我們可以通過 ParamFlowRule rule = new ParamFlowRule(resourceName) .setParamIdx(0) .setCount(5); // 針對 int 類型的參數(shù) PARAM_B,單獨設(shè)置限流 QPS 閾值為 10,而不是全局的閾值 5. ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B)) .setClassType(int.class.getName()) .setCount(10); rule.setParamFlowItemList(Collections.singletonList(item)); ParamFlowRuleManager.loadRules(Collections.singletonList(rule)); 3. 系統(tǒng)自適應(yīng)限流Sentinel 系統(tǒng)自適應(yīng)限流從整體維度對應(yīng)用入口流量進(jìn)行控制,結(jié)合應(yīng)用的 Load、總體平均 RT、入口 QPS 和線程數(shù)等幾個維度的監(jiān)控指標(biāo),讓系統(tǒng)的入口流量和系統(tǒng)的負(fù)載達(dá)到一個平衡,讓系統(tǒng)盡可能跑在最大吞吐量的同時保證系統(tǒng)整體的穩(wěn)定性。 在開始之前,先回顧一下 Sentinel 做系統(tǒng)自適應(yīng)限流的目的:
3.1 背景長期以來,系統(tǒng)自適應(yīng)保護的思路是根據(jù)硬指標(biāo),即系統(tǒng)的負(fù)載 (load1) 來做系統(tǒng)過載保護。當(dāng)系統(tǒng)負(fù)載高于某個閾值,就禁止或者減少流量的進(jìn)入;當(dāng) load 開始好轉(zhuǎn),則恢復(fù)流量的進(jìn)入。這個思路給我們帶來了不可避免的兩個問題:
TCP BBR 的思想給了我們一個很大的啟發(fā)。我們應(yīng)該根據(jù)系統(tǒng)能夠處理的請求,和允許進(jìn)來的請求,來做平衡,而不是根據(jù)一個間接的指標(biāo)(系統(tǒng) load)來做限流。最終我們追求的目標(biāo)是 在系統(tǒng)不被拖垮的情況下,提高系統(tǒng)的吞吐率,而不是 load 一定要到低于某個閾值。如果我們還是按照固有的思維,超過特定的 load 就禁止流量進(jìn)入,系統(tǒng) load 恢復(fù)就放開流量,這樣做的結(jié)果是無論我們怎么調(diào)參數(shù),調(diào)比例,都是按照果來調(diào)節(jié)因,都無法取得良好的效果。 Sentinel 在系統(tǒng)自適應(yīng)保護的做法是,用 load1 作為啟動控制流量的值,而允許通過的流量由處理請求的能力,即請求的響應(yīng)時間以及當(dāng)前系統(tǒng)正在處理的請求速率來決定。 3.2 系統(tǒng)規(guī)則系統(tǒng)保護規(guī)則是從應(yīng)用級別的入口流量進(jìn)行控制,從單臺機器的總體 Load、RT、入口 QPS 和線程數(shù)四個維度監(jiān)控應(yīng)用數(shù)據(jù),讓系統(tǒng)盡可能跑在最大吞吐量的同時保證系統(tǒng)整體的穩(wěn)定性。 系統(tǒng)保護規(guī)則是應(yīng)用整體維度的,而不是資源維度的,并且僅對入口流量生效。入口流量指的是進(jìn)入應(yīng)用的流量(EntryType.IN),比如 Web 服務(wù)或 Dubbo 服務(wù)端接收的請求,都屬于入口流量。 系統(tǒng)規(guī)則支持四種閾值類型:
3.3 原理先用經(jīng)典圖來鎮(zhèn)樓: 我們把系統(tǒng)處理請求的過程想象為一個水管,到來的請求是往這個水管灌水,當(dāng)系統(tǒng)處理順暢的時候,請求不需要排隊,直接從水管中穿過,這個請求的RT是最短的;反之,當(dāng)請求堆積的時候,那么處理請求的時間則會變?yōu)椋号抨爼r間 + 最短處理時間。
我們用 T 來表示(水管內(nèi)部的水量),用RT來表示請求的處理時間,用P來表示進(jìn)來的請求數(shù),那么一個請求從進(jìn)入水管道到從水管出來,這個水管會存在 P * RT 個請求。換一句話來說,當(dāng) T ≈ QPS * Avg(RT) 的時候,我們可以認(rèn)為系統(tǒng)的處理能力和允許進(jìn)入的請求個數(shù)達(dá)到了平衡,系統(tǒng)的負(fù)載不會進(jìn)一步惡化。 接下來的問題是,水管的水位是可以達(dá)到了一個平衡點,但是這個平衡點只能保證水管的水位不再繼續(xù)增高,但是還面臨一個問題,就是在達(dá)到平衡點之前,這個水管里已經(jīng)堆積了多少水。如果之前水管的水已經(jīng)在一個量級了,那么這個時候系統(tǒng)允許通過的水量可能只能緩慢通過,RT會大,之前堆積在水管里的水會滯留;反之,如果之前的水管水位偏低,那么又會浪費了系統(tǒng)的處理能力。
然而,和 TCP BBR 的不一樣的地方在于,還需要用一個系統(tǒng)負(fù)載的值(load1)來激發(fā)這套機制啟動。
3.4 示例public class SystemGuardDemo { private static AtomicInteger pass = new AtomicInteger(); private static AtomicInteger block = new AtomicInteger(); private static AtomicInteger total = new AtomicInteger(); private static volatile boolean stop = false; private static final int threadCount = 100; private static int seconds = 60 + 40; public static void main(String[] args) throws Exception { tick(); initSystemRule(); for (int i = 0; i < threadCount; i++) { Thread entryThread = new Thread(new Runnable() { @Override public void run() { while (true) { Entry entry = null; try { entry = SphU.entry("methodA", EntryType.IN); pass.incrementAndGet(); try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { // ignore } } catch (BlockException e1) { block.incrementAndGet(); try { TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) { // ignore } } catch (Exception e2) { // biz exception } finally { total.incrementAndGet(); if (entry != null) { entry.exit(); } } } } }); entryThread.setName("working-thread"); entryThread.start(); } } private static void initSystemRule() { List<SystemRule> rules = new ArrayList<SystemRule>(); SystemRule rule = new SystemRule(); // max load is 3 rule.setHighestSystemLoad(3.0); // max cpu usage is 60% rule.setHighestCpuUsage(0.6); // max avg rt of all request is 10 ms rule.setAvgRt(10); // max total qps is 20 rule.setQps(20); // max parallel working thread is 10 rule.setMaxThread(10); rules.add(rule); SystemRuleManager.loadRules(Collections.singletonList(rule)); } private static void tick() { Thread timer = new Thread(new TimerTask()); timer.setName("sentinel-timer-task"); timer.start(); } static class TimerTask implements Runnable { @Override public void run() { System.out.println("begin to statistic!!!"); long oldTotal = 0; long oldPass = 0; long oldBlock = 0; while (!stop) { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } long globalTotal = total.get(); long oneSecondTotal = globalTotal - oldTotal; oldTotal = globalTotal; long globalPass = pass.get(); long oneSecondPass = globalPass - oldPass; oldPass = globalPass; long globalBlock = block.get(); long oneSecondBlock = globalBlock - oldBlock; oldBlock = globalBlock; System.out.println(seconds + ", " + TimeUtil.currentTimeMillis() + ", total:" + oneSecondTotal + ", pass:" + oneSecondPass + ", block:" + oneSecondBlock); if (seconds-- <= 0) { stop = true; } } System.exit(0); } } } 4. 黑白名單控制很多時候,我們需要根據(jù)調(diào)用方來限制資源是否通過,這時候可以使用 Sentinel 的黑白名單控制的功能。黑白名單根據(jù)資源的請求來源(origin)限制資源是否通過,若配置白名單則只有請求來源位于白名單內(nèi)時才可通過;若配置黑名單則請求來源位于黑名單時不通過,其余的請求通過。
4.1 規(guī)則配置黑白名單規(guī)則(AuthorityRule)非常簡單,主要有以下配置項:
4.2 示例比如我們希望控制對資源 test 的訪問設(shè)置白名單,只有來源為 appA 和 appB 的請求才可通過,則可以配置如下白名單規(guī)則: AuthorityRule rule = new AuthorityRule(); rule.setResource("test"); rule.setStrategy(RuleConstant.AUTHORITY_WHITE); rule.setLimitApp("appA,appB"); AuthorityRuleManager.loadRules(Collections.singletonList(rule)); |
|