Sentinel 調用上下文環境實現原理(含原理圖)
- 2020 年 2 月 17 日
- 筆記

本節將詳細介紹 Sentienl 的上下文環境管理機制。
1、Sentinel Context 調用上下文環境管理
我們從 sentinel-apache-dubbo-adapter 模塊的 SentinelDubboProviderFilter 的實現中不難看出,在其入口處會首先調用 ContextUtil.enter(resourceName, application) 。那我們就從該方法開始來探究上下文環境管理機制。
說到 Sentinel 的調用上下文環境,那調用上下文環境中會保存哪些信息呢?我們先來看看 Context。
1.1 Context 詳解
Context 類圖如下:

- Context 其核心屬性與核心方法如下:
- String name Sentinel 調用上下文環境的名稱。
- DefaultNode entranceNode 調用鏈的入口節點信息。
- Entry curEntry 調用鏈中當前節點的信息。
- boolean async 是否是異步調用上下文環境。
- Entry 保存當前的調用信息,其主要核心屬性:
- private long createTime 資源調用的時間戳。
- private Node curNode 該資源所對應的實時採集信息。
- protected ResourceWrapper resourceWrapper 資源對象。
- CtEntry 同步調用調用信息封裝對象。
- AsyncEntry 異步調用調用信息的封裝對象。
對應的核心方法將在下文具體用到時再詳細介紹。
1.2 創建調用上下文環境
ContextUtil#enter
public static Context enter(String name, String origin) { // @1 if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) { throw new ContextNameDefineException( "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!"); } return trueEnter(name, origin); // @2 }
代碼@1:首先我們來看一下其參數:
- String name 上下文環境 Context 的名稱。
- String origin 該參數的含義在介紹集群限流時會詳細介紹,從 dubbo 模塊的適配來看,通常該值會傳入當前應用的 application 名稱。
代碼@2:通過調用內部的 trueEnter 方法。
在進入 trueEnter 方法之前,我們先來看一下 ContextUtil 中兩個最核心的屬性:

首先使用 ThreadLocal 對象來存儲線程上下文環境對象 Context。Map contextNameNodeMap ,其鍵為 context 的名稱,用來緩存其對應的 EntranceNode 。
ContextUtil#trueEnter
protected static Context trueEnter(String name, String origin) { Context context = contextHolder.get(); // @1 if (context == null) { Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap; DefaultNode node = localCacheNameMap.get(name); // @2 if (node == null) { if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { // @3 setNullContext(); return NULL_CONTEXT; } else { try { LOCK.lock(); node = contextNameNodeMap.get(name); // @4 if (node == null) { if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else { node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); // @5 // Add entrance node. Constant.ROOT.addChild(node); // @6 Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + ); newMap.putAll(contextNameNodeMap); newMap.put(name, node); contextNameNodeMap = newMap; } } } finally { LOCK.unlock(); } } } context = new Context(node, name); // @7 context.setOrigin(origin); contextHolder.set(context); // @8 } return context; }
代碼@1:從 threadLocal 中獲取 Context 對象,線程首次獲取時為空。
代碼@2:根據 context 的名稱嘗試從緩存中去找對應的 Node,通常是 EntranceNode。即用來表示入口的節點Node 為 EntranceNode。
代碼@3:如果 localCacheNameMap 已緩存的對象容量默認超過2000,則不納入 Sentinel 限流,熔斷等機制中來,即一個應用,默認不能定義 2000個 資源統計入口,以 一個 Dubbo 服務為例,一個 Dubbo 服務應用,如果超過2000個服務,則超過的部分不會應用 Sentinel 限流與熔斷機制。
代碼@4:鎖應用的經典場景,dubbo check。
代碼@5:為該 context name 創建一個對應的 EntranceNode。
代碼@6:將創建的 EntranceNode 加入到根節點的子節點中,稍後重點討論一下。
代碼@7:創建 Context 對象,將 Context 對象中的入口節點設置為 新創建的 EntranceNode。
代碼@8:將新創建的 Context 對象存入當前線程本地環境變量中(ThreadLocal)。
接下來先來探討代碼@6 Constants.ROOT.addChild(node)。
在 Sentinel 中,會定義一個固定根節點,其定義如下:

其資源名稱為:machine-root。addChild 方法就是將節點添加到如下數據結構中:

1.3 移除調用上下文環境
public static void exit() { Context context = contextHolder.get(); if (context != null && context.getCurEntry() == null) { contextHolder.set(null); } }
退出當前上下文環境,這裡有一個條件就是當前的上下文環境的當前調用節點已經退出,否則無法移除,故使用建議:ContextUtil . exit 一定要在持有的 Entry 退出之後再調用。
1.4 異步上下文環境切換
public static void runOnContext(Context context, Runnable f) { Context curContext = replaceContext(context); // @1 try { f.run(); // @2 } finally { replaceContext(curContext); // @3 } }
這裡是異步調用上下文環境切換的實現原理,我們知道存在 ThreadLocal 中的數據是無法跨線程訪問的,故一個線程中啟動另外一個線程,上下文環境是無法直接被傳遞的,Sentinel 的思想是為先創建的線程再創建一個 Context,在運行子線程時,調用 runOnContext 來切換上下文環境。
Context 就介紹到這裡了,我們接下來再來看一個與上下文環境管理密切相關的 Sentinel Slot 處理器:NodeSelectorSlot,通常也是 Sentinel Slot 處理鏈的第一個節點。
2、NodeSelectorSlot
2.1 NodeSelectorSlot 調用鏈概述
從該類的注釋可以得出如下的結論:該類的作用是構建一顆虛擬調用樹,我們接下來以一個Dubbo調用示例來說明。

正如上圖所示:應用 A 嚮應用 order-servie 服務發起一個 RPC 服務,下訂單,order-service 應用引入了 sentinel-apache-dubbo-adapter 相關依懶,會執行 SentinelDubboProviderFilter 過濾器,調用 Sentinel 相關的方法,對資源進行保護,然後下單服務中,首先會操作數據庫,將本次數據庫操作定義為資源:insertOrderSQL,然後再操作 redis,redis 的操作命名為資源 setRedisOp。其對應在內存中會生成如下調用鏈的結構圖。

那上面這個調用鏈保存在線程上下文環境中,即 ThreadLocal 中。在 Sentinel 中使用 Node 來表示一個一個調用節點,其中 EntranceNode 表示調用鏈的入口,DefaultNode 表示普通節點,ClusterNode 表示集群節點,即同一個資源會統計整個集群中的信息。
從該類的注釋我們可以得出上述的結論,接下來我們從源碼的角度對其進行分析與理解。
2.2 源碼分析 NodeSelectorSlot
NodeSelectorSlot 中只聲明了一個唯一的成員變量,其聲明如下:
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>();
定義一個 Map,其鍵為上下文環境 Context 的名稱,通常是進入節點的名稱,例如上面提到的 EntranceNode( dubbo:provider:com.a.b.OrderService:saveOrder(java.lang.String))。
注意:一個 NodeSelectorSlot 對象會被多個線程使用,其共享的維度為資源,即多個線程進入同一個資源保護的代碼時,執行的是同一個 NodeSelectorSlot 對象。詳細實現請參考上文中 CtSph # lookProcessChain 部分詳解。
接下來重點看一下 NodeSelectorSlot 的核心方法 entry。
NodeSelectorSlot#entry
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) // @1 throws Throwable { DefaultNode node = map.get(context.getName()); // @2 if (node == null) { // @3 synchronized (this) { // @4 node = map.get(context.getName()); if (node == null) { node = new DefaultNode(resourceWrapper, null); // @5 HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size()); cacheMap.putAll(map); cacheMap.put(context.getName(), node); map = cacheMap; // Build invocation tree ((DefaultNode) context.getLastNode()).addChild(node); // @6 } } } context.setCurNode(node); // @7 fireEntry(context, resourceWrapper, node, count, prioritized, args); }
代碼@1:我們先來看看其參數:
- Context context 調用上下文環境,該對象存儲在 ThreadLocal,其名稱在調用鏈的入口處設置。
- ResourceWrapper resourceWrapper 資源的包裝類,注意留意其 equals 與 hashCode 方法,判斷兩個對象是否相等的依據是資源名 稱是否相同。
- Object obj 參數。
- int count 本次需要消耗的令牌數量。
- boolean prioritized 請求是否按優先級排列。
- Object… args 額外參數。
代碼@2:如果緩存中存在對應該上下文環境的節點,則直接使用,並將其節點設置當前調用上下文的當前節點中(Context)。
代碼@3:如果節點為空,則進入到節點創建流程,此過程需要加鎖,見代碼@4。
代碼@5:創建一個新的 DefaultNode 。
代碼@6:構建調用鏈,由於 NodeSelectorSlot 是第一個進入的處理器,故此時 Context 的 curEntry 為 null ,故這裡就是創建與的上下文環境名稱對應的節點會被添加到 ContextUtil 的 entry 創建的調用鏈入口節點(EntranceNode),然後順便更新 Context 中的 Entry curEntry 屬性,即再次驗證了上面的圖。
我們來總結一下 NodeSelectorSlot 作用:從官方的注釋來看:構建一條調用鏈,更直接一點就是設置 Context 的 curEntry 屬性。
關於 Sentinel 調用上下文環境實現原理就介紹到這裡了。
如果您喜歡這篇文章,點【在看】與轉發是一種美德,期待您的認可與鼓勵,越努力越幸運。
思考題:首先在這裡先「劇透」一下,Node 在 Sentinel 中的作用是持有資源的實時統計信息,將在下一篇文章介紹 StatisticSlot 時詳細介紹。NodeSelectorSlot 中的 Map 中的鍵為什麼是 Context 的 名稱呢?這樣設計的目的是什麼,能有什麼好處?
歡迎加入我的知識星球,一起交流源碼,探討架構,打造高質量的技術交流圈,長按如下二維碼
中間件興趣圈 知識星球 正在對如下話題展開如火如荼的討論:
1、【讓天下沒有難學的Netty-網絡通道篇】
1、Netty4 Channel概述(已發表)
2、Netty4 ChannelHandler概述(已發表)
3、Netty4事件處理傳播機制(已發表)
4、Netty4服務端啟動流程(已發表)
5、Netty4 NIO 客戶端啟動流程
6、Netty4 NIO線程模型分析
7、Netty4編碼器、解碼器實現原理
8、Netty4 讀事件處理流程
9、Netty4 寫事件處理流程
10、Netty4 NIO Channel其他方法詳解
2、Java 並發框架(JUC) 探討【面試神器】 3、源碼分析Alibaba Sentienl 專欄背後的寫作與學習技巧。