Sentinel 使用
同時發布:http://fantasylion.github.io/java/2020-07-29-Sentinel-Source-code-analysis/
在分析源碼之前首先看下,Sentinel 如何使用
建立規則
1 |
// 建立規則 |
使用規則
1 |
Entry entry = null; |
從上面的代碼中大致可以看出,sentinel 通過 SphU.entry
驗證規則並開始統計,如果其中某條規則不通過將會拋出對應的異常, 通過 entry.exit()
結束統計。
下面進入到源碼中分析具體的實現原理
1 |
public static final Sph sph = new CtSph(); |
責任鏈模式
以上 entryWithPriority
源碼中可以 sentinel 用到了責任鏈模式,通過責任鏈創建節點、統計指標、驗證規則…。
接下看下 Sentinel 是如何實現責任鏈模式又是如何統計指標和驗證規則的。
1 |
// 在沒有調用鏈,並且調用鏈沒有超過最大允許數時,初始化一個 |
1 |
// 獲取到一個默認的slot調用鏈構建器,並開始構建 |
1 |
public ProcessorSlotChain build() { |
1 |
public static <T> List<T> loadPrototypeInstanceListSorted(Class<T> clazz) { |
- @1 SPI 發現並加載ProcessorSlot接口對象集合。通過[META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot]找到所有的調用鏈節點
- @2 每個實現類上都有一個注解
@SpiOrder
取出注解上的值,用於后續的排序 - @3 按
@SpiOrder
從小到大冒泡排序,將spi
插入到orderWrappers
中 - @4 創建一個新的集合並將
spi
按順序存入
在完成以上步驟后,調用鏈將被初始化成
順序 | 節點 | 作用 | 下一個節點 |
---|---|---|---|
1 | DefaultProcessorSlotChain | 第一個節點 | NodeSelectorSlot |
2 | NodeSelectorSlot | 創建當前Node | ClusterBuilderSlot |
3 | ClusterBuilderSlot | 創建全局Cluster節點 | LogSlot |
4 | LogSlot | 記錄日志 | StatisticSlot |
5 | StatisticSlot | 統計各項指標 | AuthoritySlot |
6 | AuthoritySlot | 驗證認證規則 | SystemSlot |
7 | SystemSlot | 驗證系統指標(CPU等指標) | FlowSlot |
8 | FlowSlot | 驗證限流指標 | DegradeSlot |
9 | DegradeSlot | 驗證熔斷指標 | Null |
責任鏈調用
NodeSelectorSlot 源碼分析
1 |
DefaultNode node = map.get(context.getName()); |
NodeSelectorSlot
源碼比較簡單,主要邏輯就是根據 context
名找到一個對應的 Node
如果沒有就創建一個,並標記為 context
的
當前 node
ClusterBuilderSlot 源碼分析
1 |
if (clusterNode == null) { |
- clusterNode 是相對資源唯一
- 因為一個資源只會有一個責任鏈,只有在初始化的時候需要進行緩存,所以這里只需要用 HashMap 用來存儲這個 clusterNode, 並且在初始化的時候加上鎖就可以了(后續只會讀)。
LogSlot 源碼分析
1 |
try { |
- @1 先調用后面的責任鏈節點
- @2 當后面的責任鏈節點觸發 BlockException 異常后記錄 Block 次數到鷹眼
- @3 當后面的責任鏈觸發其他異常后打出警告日志
StatisticSlot 源碼分析
StatisticSlot
是 Sentinel
核心的一個類,統計各項指標用於后續的限流、熔斷、系統保護等策略,接下來看下 Sentinel
是如何通過 StatisticSlot
進行指標統計的
1 |
// ...省略部分代碼 |
- @1 觸發后面的責任鏈節點
- @2 記錄通過的線程數
+1
和通過請求+count
這里的node
就是第二個責任鏈節點NodeSelectorSlot
創建的DefaultNode
在分析源碼前可以先簡單了解下Context
、Entry
、DefaultNode
、ClusterNode
的關系 Context
每個線程是獨享的,但是不同線程的Context
可以使用同一個名字EntranceNode
是根據Context
名共享的,也就是說一個Context.name
對應一個EntranceNode
。每次調用的時候都會創建,用於記錄Entry
是相對於每個Context
獨享的即是同一個Context.name
,包含了資源名、curNode(當前統計節點)、originNode(來源統計節點)等信息DefaultNode
一個Context.name
對應一個統計某資源調用鏈路上的指標ClusterNode
一個資源對應一個,統計一個資源維度的指標DefaultNode
和ClusterNode
都繼承至StatisticNode
都包含兩個ArrayMetric
類型的字段rollingCounterInSecond
、rollingCounterInMinute
分別用於存儲秒級和分鍾級統計指標- 而
ArrayMetric
類包含了一個LeapArray<MetricBucket>
類型字段data
,data
中存放了一個WindowWrap<MetricBucket>
元素的數組(滑動窗口), 而這個數組就是各項指標最終存儲的位置
1 |
node.increaseThreadNum(); |
這行代碼其實就是對 StatisticNode.curThreadNum
進行自增操作
1 |
public void addPassRequest(int count) { |
添加通過的數量, 除了 DefaultNode
記錄一次外,在 ClusterNode
上也需要記錄一次【注意:ClusterNode
是按照資源維度統計的,這里指向的 ClusterNode
與同一資源不同 Context
指向的 ClusterNode
是同一個】。一個 Node
在調用了 addPassRequest
后發生了什么呢?
1 |
public void addPassRequest(int count) { |
在以上代碼可以看到 rollingCounterInSecond
、rollingCounterInMinute
兩個字段,它們分別用來統計秒級指標和分鍾級指標。而實際上這兩個字段使用了滑動時間窗口數據結構用於存儲指標。接下來看下 Sentinel
滑動窗口的設計:
時間滑動窗口主要用到的幾個類有:
- ArrayMetric: 負責初始化時間滑動窗口和維護
- LeapArray: 一個滑動時間窗口主體
- WindowWrap: 一個時間窗口主體
- LongAdder:指標統計的計數類
ArrayMetric
構造器:
1 |
public ArrayMetric(int sampleCount, int intervalInMs) { |
ArrayMetric
主要有三種構造器,最后一種只是用來跑單元測試使用,而前兩種構造器主要為了初始化 data
字段。
從代碼中我們可以看到 LeapArray
有兩種實現方式 OccupiableBucketLeapArray
和 BucketLeapArray
,而兩種都繼承至 LeapArray
。
LeapArray 類圖
LeapArray類圖LeapArray
類主要包含以下幾個字段:
int windowLengthInMs
一個時間窗口的長度,用毫秒表示int sampleCount
表示用幾個時間窗口統計int intervalInMs
輪回時間,也就是所有時間窗口加起來的總時長AtomicReferenceArray<WindowWrap<T>> array
時間窗口實例集合,數組的長度等於sampleCount
那么我們在回頭看下 rollingCounterInSecond
、rollingCounterInMinute
用到了哪種 LeapArray
1 |
/** |
從上述代碼中我們可以看到秒級統計初始化了一個 OccupiableBucketLeapArray
輪回時間為 1000ms 也就是 1s,分兩個時間窗口每個各 500ms,而分鍾級統計初始化了 BucketLeapArray
輪回時間為 60000ms 也就是 1Min ,分 60 個時間窗口每個窗口 1s。
1 |
// ArrayMetric.addPass |
在添加通過指標前先獲取到當前的時間窗口,再將通過數量統計到窗口對應的 MetricBucket
中,那么如何獲取當前窗口呢?
1 |
public WindowWrap<T> currentWindow() { |
第一步首先獲取到當前的時間戳毫秒,通過時間戳計算出時間窗口數組的下標。在計算下標時首先將當前時間戳除以單個窗口時長,計算出當前所在從0ms開始到現在的第幾個窗,再對窗口數取模得出當前窗口的在數組中所在下標。從這里我們大概可以看出,這里數組中的時間窗口對象是反復使用的只是代表的時間不同了。
我們以秒級統計為例模擬計算下,當前時間戳為:1595495124658
,按照 timeMillis / windowLengthInMs
可以得出 timeId
為 3190990249
。 (int)(timeId % array.length())
就是 3190990249 % 2
算出結果為 1
,也就是說 1
下標位置的時間窗口是當前時間窗口。
第二步在計算出當前窗口所在下標后,需要計算出當前窗口的開始時間 timeMillis - timeMillis % windowLengthInMs
,timeMillis % windowLengthInMs
表示當前窗口開始時間到當前時間的時長,所有當前時間減去時長即可得出當前窗口的開始時間,按上面的例子算出的結果就是 1595495124500
1 |
WindowWrap<T> old = array.get(idx); |
第三步根據下標取出我們的當前窗口的實例,如果實例還沒有被創建過新建一個窗口實例並初始化同時通過 CAS
的方式更新到窗口數組中,如果更新失敗讓出 CPU
等待下次 CPU
執行本線程。
第四步如果下標位置已經存在一個窗口實例,並且窗口的開始時間跟本次窗口開始時間一致(同一個窗口),直接返回下標中的窗口
第五步如果當前窗口的開始時間大於下標窗口的開始時間,說明下標窗口已過期,需要重置數組下標中的窗口(把下標窗口的開始時間改完當前窗口時間,並將指標計數都置成 0 )
第六步當前窗口時間小於下標窗口時間,重新實例化一個窗口(不太有這個可能,sentinel
內部實現了自己的時間戳)
在拿到當前時間所在窗口后,將當前的指標累加記錄到 MetriBucket
中
- MetriBucket 累加通過指標 *
1 |
public void addPass(int n) { |
counters
是一個LongAdder
類型的數組MetricEvent
是指標類型,分別有:PASS 通過、BLOCK 阻塞、 EXCEPTION 異常、 SUCCESS 成功、 RT 平均響應時間、 OCCUPIED_PASS 通過未來的配額counters[event.ordinal()].add(n)
在指定的指標計數器上累加計數
看到這里我們知道了 pass
指標是在資源通過 StatisticSlot
后幾個節點的驗證后立即進行指標計數,那么剩下的 BLOCK
、 EXCEPTION
、 SUCCESS
、 RT
、 OCCUPIED_PASS
這幾個是在什么時候做記錄的呢?
BLOCK 統計
1 |
...省略部分代碼... |
在后續的責任鏈節點中(StatisticSlot
之后的節點),如果捕獲到了阻塞異常,將對 DefaultNode
、OriginNode
、ENTRY_NODE
幾個 node
進行指標累計。同樣也是添加到當前窗口 MetricBucket
中不再進行過多描述
EXCEPTION 統計
1 |
try { |
類似的 exception
統計在后續的責任鏈節點中(StatisticSlot
之后的節點),如果捕獲到了異常,將對 DefaultNode
、OriginNode
、ENTRY_NODE
幾個 node
進行指標累計。
除了 StatisticSlot
自動捕獲異常外,在資源調用過程中如果出現了異常將通過調用 Tracer.trace(e)
手動統計異常指標
1 |
public static void trace(Throwable e, int count) { |
首先從線程變量中出去當前線程的 Context
在從中取出 DefaultNode 和 ClusterNode 並進行異常指標累計
SUCCESS
、 RT
統計
平均響應時間和成功次數的統計是在資源退出的時候調用 entry.exit()
進行統計,代碼如下:
1 |
// StatisticSlot#exit() |
退出也是責任鏈調用退出每個節點,這里直接跳過了大部分代碼。退出統計大致流程如下:
- 獲取得到當前時間戳和資源調用的時間,相減算出這次整個資源調用所花費的總時間
- 將總時間記錄和成功次數累加記錄當前窗口,本次總時間如果超過最大統計時間以最大統計時間作為本次統計時間
- 對 Node 扣減一次當前線程數
- 觸發下一個責任鏈節點退出
LongAdder 源碼分析
1 |
public void add(long x) { |
LongAdder 中有一個Cell數組用於存儲數值,當高並發時對數組中某個值進行加法運算減少同一個數值並發。(+1) 或者 (+ -1)
1 |
public long sum() { |
取值時把 Cell 數組中所有元素的取出算總數
熔點判斷
1 |
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count); |
熔點的判斷是由 DegradeRuleManager
管理。 DegradeRuleManager
會根據資源名取出所有的熔斷規則,然后檢查所有的規則如果觸發其中一個直接拋出 DegradeException
異常觸發熔斷機制。
- RT *
1 |
double rt = clusterNode.avgRt(); |
- 從
clusterNode
中計算出平均響應時間 - 如果平均響應時間小於規則設置時間,將統計連續超時計數器重置為
0
- 如果平均響應時間大於規則設置時間,並且連續超時計數器超過了規則設置的大小,判為到達熔斷點拋出熔斷異常
統計平均 RT 的方法(秒級):
- 取出所有窗口(秒級只定義了兩個時間窗口)的 RT,並求總和
- 取出所有窗口(秒級只定義了兩個時間窗口)的 success,並求總和
- 所有窗口的 RT 總和 除以 success 總和 得出平均RT
異常比例熔斷也是類似的邏輯(秒級)
- 取出所有窗口的 exception 數求和,並除以一個間隔時間(秒為單位)【每秒總異常數】
- 取出所有窗口的 success 數求總和,並除以一個間隔時間(秒為單位)【每秒總退出成功數,包含了異常數】
- 取出所有窗口的 pass 總和 加上所有窗口 block 總數,並除以一個間隔時間(秒為單位)【算每秒總調用量】
- 如果每秒總調用量小於 minRequestAmount 判為未到達熔斷點
- 如果每秒總異常數沒有超過 minRequestAmount 判為未到達熔斷點
- 每秒總退出成功數 / 每秒總異常數(異常比例)如果超過規則指定比例,判為到達熔斷點拋出熔斷異常
異常數就比例(分鍾級)
- 取出所有窗口的 exception 數總和,判斷如果超過規則配置數,拋出熔斷異常
總結
Sentinel 通過責任鏈,觸發節點創建、監控統計、日志、認證、系統限流、限流、熔斷,因為Sentinl 是由 SPI 創建的責任鏈所以我們可以自定義鏈節點拿到指標根據自己的業務邏輯定義。
Sentinel 通過將所有的指標統計到時間窗口中,記錄在 MetricBucket 類實例中