前言:
sentinel是阿里針對服務流量控制、熔斷降級的框架,如何使用官方都有很詳細的文件,下載它的原始碼包 裡面對各大主流框都做了適配按理,本系列文章目的 主要通過原始碼分析sentinel流量控制和熔斷降級的流程
提前準備好sentinel控制檯 如有下載原始碼啟動sentinel dashboard模組
本文演示的專案通過引入spring-cloud-starter-alibaba-sentinel包來實現接入sentinel功能
入門案例
下面舉一個最簡單的案例埋點來引出流控入口
public String getOrderInfo(String orderNo) {
ContextUtil.enter("getOrderInfo", "application-a"); Entry entry = null; try { // name:資源名 EntryType 流量型別為入口還是出口,系統規則只針對入口流量, batchCount:當前請求流量, args:引數 entry = SphU.entry("getOrderInfo", EntryType.IN, 1, orderNo); getUserInfo(); } catch (BlockException e) { e.printStackTrace(); } finally { entry.exit(); } return "orderInfo = " + orderNo; } public String getUserInfo() { Entry entry = null; try { entry = SphU.entry("getUserInfo", EntryType.OUT, 1); } catch (BlockException e) { e.printStackTrace(); } finally { entry.exit(); } return "userInfo"; }
public static Entry entry(String name, EntryType trafficType, int batchCount, Object... args) throws BlockException
也可以通過註解的方式引入,執行方法時SentinelResourceAspect會做攔截進行流控處理,當然什麼都不配也是可以的,因為引入spring-cloud-starter-alibaba-sentinel包spring mvc和spring webflux做了適配,自動會對每一個請求做埋點
@GetMapping("getOrderInfo") @SentinelResource(value = "/getOrderInfo", entryType = EntryType.IN) public String getOrderInfo(@RequestParam("orderNo") String orderNo) { return "orderInfo = " + orderNo; }
ContextUtil.enter("getOrderInfo", "application-a") 來表示呼叫鏈的入口,可以暫時理解為上下文,一般不做宣告 後面會預設建立
第一個引數為context-name,區分不同的呼叫鏈入口,預設常量值sentinel_default_context,
第二引數為呼叫來源,這個引數可以細分從不同應用來源發出的請求,授權規則白名單和黑名單會根據該引數做限制,
然後通過SphU.entry()埋點進入,下面說下這個方法幾個引數的含義
- name:當前資源名
- trafficType:流量型別 分別為入口流量和出口流量。入口流量和出口流量執行過程都是差不多,只是入口流量會多了一個系統規則攔截,像是上面案例從訂單服務呼叫getUserInfo,getUserInfo是去呼叫使用者服務,它的流量方式是出去的,壓力都在使用者服務那邊,不用考慮當前伺服器的壓力,所以標為出口流量
- batchCount:當前流量數量,一般預設為1
- args:引數,後面做熱點引數規則時用到
BlockException:當某一規則不通過時會丟擲對應異常
SphU.entry(xxx)
需要與 entry.exit()
方法成對出現,匹配呼叫,如有巢狀像上面,需先退出getUserInfo的entry在退出getOrderInfo的entry
開啟打控制檯,此時應該是空白的,sentinel控制檯是懶載入模式,需要呼叫一下相關資源介面就可以看到
可以看到sentinel規則配置主要有流控規則,降級規則,熱點規則,系統規則,授權規則,先簡單介紹下規則作用,其它配置也很簡單 一目瞭然,後面通過結合原始碼來深入分析
- 流控規則:針對資源流量控制
- 熱點規則:針對資源的熱點引數做流量控制
- 降級規則:針對資源的排程情況來做降級處理
- 系統規則:針對當前服務做全域性流量控制
- 授權規則:對訪問資源的特定應用做授權處理
我們隨著SphU.entry()入口來走進原始碼
SphU.entry(xxx)執行過程原始碼分析
流控規則執行前的準備
public static Entry entry(String name, EntryType trafficType, int batchCount, Object... args) throws BlockException { return Env.sph.entry(name, trafficType, batchCount, args); } public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException { // 將資源名稱和流量型別進行包裝 StringResourceWrapper resource = new StringResourceWrapper(name, type); return entry(resource, count, args); } private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // 這裡返回當前執行緒持有的context Context context = ContextUtil.getContext(); if (context instanceof NullContext) { return new CtEntry(resourceWrapper, null, context); } if (context == null) { // 為空建立一個預設 context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME); } // 全域性開關 不進行規則檢查 if (!Constants.ON) { return new CtEntry(resourceWrapper, null, context); } // 新增一個規則檢查呼叫鏈 ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); // 建立一個流量入口,將context curEntry進行指定 Entry e = new CtEntry(resourceWrapper, chain, context); try { // 開始規則檢查 chain.entry(context, resourceWrapper, null, count, prioritized, args); } catch (BlockException e1) { // 發生流控異常進行退出 e.exit(count, args); // 將異常向上拋 throw e1; } catch (Throwable e1) { // This should not happen, unless there are errors existing in Sentinel internal. RecordLog.info("Sentinel unexpected exception", e1); } return e; }
可以看到準備過程主要做了五件事
- 將資源名稱和流量型別進行包裝
- 從當前執行緒得到context,如果之前沒有建立context,則這裡會建立一個context-name為sentinel_default_name、original為""的context
- 新增一個規則檢查呼叫鏈,根據我們配置的規則一層一層進行檢查,只要在某一個規則未通過就提前結束丟擲該規則對應的異常
- 建立一個流量入口entry,它用來儲存本次呼叫的資訊,將context的curEntry進行指定
- 開始執行規則檢查呼叫鏈
context建立過程
來看下context的建立過程,因為裡面還涉及到一個非常重要的類Node,它的作用是統計資源的呼叫資訊,如QPS、rt等資訊
protected static Context trueEnter(String name, String origin) { // 從當前執行緒上下文中拿 Context context = contextHolder.get(); if (context == null) { Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap; // 獲取context-name對應的DefaultNode DefaultNode node = localCacheNameMap.get(name); if (node == null) { // 限制2000,也就是最多申明2000不同名稱的上下文 if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { LOCK.lock(); try { // 防止併發,再次檢查 node = contextNameNodeMap.get(name); if (node == null) { if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { // 建立EntranceNode node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); // Add entrance node. Constants.ROOT.addChild(node); Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1); newMap.putAll(contextNameNodeMap); newMap.put(name, node); contextNameNodeMap = newMap; } } } finally { LOCK.unlock(); } } } context = new Context(node, name); context.setOrigin(origin); contextHolder.set(context); } return context; }
Node之間的樹形結構
在建立context會先建立DefaultNode 實際是它的父類EntranceNode,context可以相同context-name反覆申明建立,但是DefaultNode同一context-name只會建立一次,DefaultNode包含了一個鏈路所有的資源,每一個資源對應一個ClusterNode,ClusterNode再根據來源細分為StatisticNode,它們之間的關係就是一個樹形結構 如下:
EntranceNode:根據context-name來建立,就算同一個context-name多次建立context,entranceNode也只會建立一次, 用來統計該鏈路上所有的資源資訊
DefaultNode:根據context-name + resource-name建立,用來統計某鏈路上的資源資訊
ClusterNode:根據resource-name來建立,用來統計資源資訊
StatisticsNode:根據origin-name+resource-name來建立,針對請求來源統計該來源的資源資訊,上面幾個node都是它的子類,基於它的資料做彙總
讀者一定要搞清楚這幾個node之間的關係和作用,下面重點來看StatisticsNode,它用來完成資訊統計 以供後續的限流規則使用, 它只統計了兩個維度資料,qps和執行緒數
Node中滑動視窗實現
執行緒數統計很簡單,通過LongAdder來完成,比較簡單不過敘述,qps採用滑動視窗演算法完成,但它跟普通的滑動視窗演算法不太一樣,它的資料結構是固定,可以重複利用 減少了記憶體消耗,可以看到預設建立了兩個時間維度的視窗,分別以秒(細分為2個子視窗 500ms為間隔)和分鐘(細分為60個子視窗 1s為間隔)
ArrayMetric 的內部是一個 LeapArray,分鐘維度使用由子類 BucketLeapArray
實現,秒維度由OccpiableBucketLeapArray實現,我們先來看BucketLeapArray
,OccpiableBucketLeapArray會在後面具體使用到時候再進行額外講解
BucketLeapArray比較簡單,內部就實現了LeapArray兩個鉤子方法,newEmptyBucket建立空桶, MetricBuket它視窗用來統計資料的類,裡面是一個陣列LongAdder 依次存放qps、rt等資訊
resetWindoTo 重置視窗資料,再跟到LeapArray類中
public abstract class LeapArray<T> { // 每個視窗長度(佔用時間) protected int windowLengthInMs; // 滑動視窗的個數 protected int sampleCount; // 全部視窗的總間隔時間 (毫秒) protected int intervalInMs; // 全部視窗的總間隔時間 (秒) private double intervalInSecond; // 儲存視窗的陣列,length = sampleCount protected final AtomicReferenceArray<WindowWrap<T>> array; public WindowWrap<T> currentWindow(long timeMillis) { if (timeMillis < 0) { return null; } // 根據時間計算索引 int idx = calculateTimeIdx(timeMillis); // 根據時間計算視窗開始時間 long windowStart = calculateWindowStart(timeMillis); /* * 從array中獲取視窗 * * (1) array中不存在,建立一個視窗 並cas加入其中 * (2) array中視窗開始的時間=當前當前視窗開始時間,說明當前視窗剛不久已經建立過 * (3) array中視窗開始的時間<當前獲取的時間,表示old視窗已過期,重置視窗資料並返回 */ while (true) { WindowWrap<T> old = array.get(idx); if (old == null) { WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); if (array.compareAndSet(idx, null, window)) { return window; } else { Thread.yield(); } } else if (windowStart == old.windowStart()) { return old; } else if (windowStart > old.windowStart()) { if (updateLock.tryLock()) { try { return resetWindowTo(old, windowStart); } finally { updateLock.unlock(); } } else { Thread.yield(); } } else if (windowStart < old.windowStart()) { // Should not go through here, as the provided time is already behind. return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); } } } // 獲取視窗資料 public T getWindowValue(long timeMillis) { if (timeMillis < 0) { return null; } // 根據時間計算索引 int idx = calculateTimeIdx(timeMillis); WindowWrap<T> bucket = array.get(idx); if (bucket == null || !bucket.isTimeInWindow(timeMillis)) { return null; } return bucket.value(); } }
LeapArray主要兩個方法,currentWindow(long timeMillis),根據當前時間獲取當前視窗,getWindowValue(long timeMillis),根據當前時間獲取當前視窗的值
這裡跟一般的滑動視窗演算法不太一樣,一般的滑動視窗演算法 視窗的大小不是固定 可以實時擴容,但這裡它的大小在初始化就決定好了,當第一分鐘中的60個視窗已經全部被建立,後續時間進來獲取視窗會不斷進行覆蓋
sentinel規則呼叫鏈
我們再回到Sphu.entry()方法中來,裡面還有個sentinel規則呼叫鏈的構建,針對當前資源的排程資訊進行規則校驗
ProcessorSlot<Object> chain = this.lookProcessChain(resourceWrapper);
構建後會形成一個這樣的呼叫鏈
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
進入規則呼叫鏈,跟著順序一個一個來看
chain.entry(context, resourceWrapper, null, count, prioritized, args)
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
NodeSelectorSlot:用來建立DefaultNode,前面講解node已經提到過
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
ClusterBuilderSlot: 用來建立ClusterBuilderSlot
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
LogSlot:發生BlockException異常,記錄日誌
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
StatisticSlot:這個類就非常重要了,它用來進行資料統計,它先向後繼續傳遞,等後續slot全部執行後再執行統計
public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> { @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { try { // 向後傳遞 fireEntry(context, resourceWrapper, node, count, prioritized, args); // 對DefaultNode新增執行緒數和qps node.increaseThreadNum(); node.addPassRequest(count); if (context.getCurEntry().getOriginNode() != null) { // Add count for origin node. context.getCurEntry().getOriginNode().increaseThreadNum(); context.getCurEntry().getOriginNode().addPassRequest(count); } if (resourceWrapper.getEntryType() == EntryType.IN) { // Add count for global inbound entry node for global statistics. Constants.ENTRY_NODE.increaseThreadNum(); Constants.ENTRY_NODE.addPassRequest(count); } // Handle pass event with registered entry callback handlers. for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) { handler.onPass(context, resourceWrapper, node, count, args); } } catch (PriorityWaitException ex) { xxx } catch (BlockException e) { xxx throw e; } catch (Throwable e) { context.getCurEntry().setError(e); throw e; } } }
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
ParamFlowSlot:校驗熱點引數規則
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
SystemSlot:系統規則校驗,只對入站流量做校驗,彙總當前入站所有的資源資訊然後進行校驗
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
AuthoritySlot:授權規則校驗,對呼叫方進行白名單或黑名單限制
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
FlowSlot:校驗流控規則,具體有四種規則,由TrafficShapingController的實現類完成,裡面實現比較複雜,在下一章節會進行詳解
NodeSelectorSlot >>> ClusterBuilderSlot >>> LogSlot >>> StatisticSlot >>> ParamFlowSlot >>> SystemSlot >>> AuthoritySlot >>> FlowSlot >>> DegradeSlot
DegradeSlot:校驗熔斷降級規則,分別有三種策略:慢比例呼叫、異常比例、異常數,降級規則校驗跟之前規則校驗流程不太一樣,它是直接對資源進行校驗,內部通過CircuitBreaker(斷路器)來實現,慢比例呼叫由ResponseTimeCircuitBreaker實現,
異常比例、異常數由ExceptionCircuitBreaker實現
public class DegradeSlot extends AbstractLinkedProcessorSlot<DefaultNode> { @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { performChecking(context, resourceWrapper); fireEntry(context, resourceWrapper, node, count, prioritized, args); } void performChecking(Context context, ResourceWrapper r) throws BlockException { List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName()); if (circuitBreakers == null || circuitBreakers.isEmpty()) { return; } for (CircuitBreaker cb : circuitBreakers) { // 嘗試通過 if (!cb.tryPass(context)) { throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule()); } } } }
斷路器有三種狀態,CLOSE:正常通行,HALF_OPEN:允許探測通行,OPEN:拒絕通行,這裡解釋下為啥會有HALF_OPEN狀態出現,比如我們對同一個資源設定了兩個降級規則 R1:熔斷時間為100ms,R2:熔斷時間為200ms,當R1已到恢復點 此時R2還未恢復,
R1狀態會從OPEN變為HALF_OPEN,R1本次校驗通過,由於R2還未恢復 R2校驗不通過,本次資源請求依然是不通過的,但如果R1、R2都已恢復 正常通行,在entry.exit()會將狀態設定為CLOSE後續請求正常通行,這就是HALF_OPEN出現的目的
/**************************************AbstractCircuitBreaker**************************************/ public boolean tryPass(Context context) { // 正常通行 if (currentState.get() == State.CLOSED) { return true; } // 嘗試通行 if (currentState.get() == State.OPEN) { // For half-open state we allow a request for probing. return retryTimeoutArrived() && fromOpenToHalfOpen(context); } return false; } protected boolean fromOpenToHalfOpen(Context context) { // 嘗試將狀態從OPEN設定為HALF_OPEN if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) { // 狀態變化通知 notifyObservers(State.OPEN, State.HALF_OPEN, null); Entry entry = context.getCurEntry(); // 在entry新增一個exitHandler entry.exit()時會呼叫 entry.whenTerminate(new BiConsumer<Context, Entry>() { @Override public void accept(Context context, Entry entry) { // 如果有發生異常,重新將狀態設定為OPEN 請求不同通過 if (entry.getBlockError() != null) { currentState.compareAndSet(State.HALF_OPEN, State.OPEN); notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d); } } }); // 此時狀態已設定為HALF_OPEN正常通行 return true; } return false; } /**************************************CtEntry**************************************/ private void callExitHandlersAndCleanUp(Context ctx) { if (exitHandlers != null && !exitHandlers.isEmpty()) { for (BiConsumer<Context, Entry> handler : this.exitHandlers) { try { handler.accept(ctx, this); } catch (Exception e) { RecordLog.warn("Error occurred when invoking entry exit handler, current entry: " + resourceWrapper.getName(), e); } } exitHandlers = null; } }
上面只看到了狀態從OPEN變為HALF_OPEN,HALF_OPEN變為OPEN,但沒有看到狀態如何從HALF_OPEN變為CLOSE的,它的變化過程是在正常執行完請求後,entry.exit()會呼叫DegradeSlot.exit()方法來改變狀態
/**************************************DegradeSlot**************************************/ public void exit(Context context, ResourceWrapper r, int count, Object... args) { Entry curEntry = context.getCurEntry(); if (curEntry.getBlockError() != null) { fireExit(context, r, count, args); return; } List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName()); if (circuitBreakers == null || circuitBreakers.isEmpty()) { fireExit(context, r, count, args); return; } if (curEntry.getBlockError() == null) { // passed request for (CircuitBreaker circuitBreaker : circuitBreakers) { circuitBreaker.onRequestComplete(context); } } fireExit(context, r, count, args); } /**************************************ExceptionCircuitBreaker**************************************/ public void onRequestComplete(Context context) { Entry entry = context.getCurEntry(); if (entry == null) { return; } Throwable error = entry.getError(); SimpleErrorCounter counter = stat.currentWindow().value(); if (error != null) { // 發生異常 新增異常數 counter.getErrorCount().add(1); } counter.getTotalCount().add(1); handleStateChangeWhenThresholdExceeded(error); } private void handleStateChangeWhenThresholdExceeded(Throwable error) { if (currentState.get() == State.OPEN) { return; } if (currentState.get() == State.HALF_OPEN) { // In detecting request if (error == null) { // 未發生異常 HALF_OPEN >>> CLOSE fromHalfOpenToClose(); } else { // 發生異常 HALF_OPEN >>> OPEN fromHalfOpenToOpen(1.0d); } return; } // 代表此時狀態為CLOSE List<SimpleErrorCounter> counters = stat.values(); long errCount = 0; long totalCount = 0; for (SimpleErrorCounter counter : counters) { errCount += counter.errorCount.sum(); totalCount += counter.totalCount.sum(); } if (totalCount < minRequestAmount) { return; } // 當前異常數 double curCount = errCount; if (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) { // 算出當前的異常比例 curCount = errCount * 1.0d / totalCount; }
// 判斷當前異常數或異常比例是否達到設定的閥值 if (curCount > threshold) { // 超出設定 將狀態設定為OPEN transformToOpen(curCount); } }
到此整個規則呼叫的流程我們都大致過了一遍,除了FlowSlot和DegradeSlot比較複雜,其它的規則校驗原始碼都不算難,讀者自行了解