Sentinel 調用上下文環境實現原理(含原理圖)

作積極的人,越努力越幸運!
Sentinel 調用上下文環境實現原理(含原理圖)
本節將詳細介紹 Sentienl 的上下文環境管理機制。java

一、Sentinel Context 調用上下文環境管理


咱們從 sentinel-apache-dubbo-adapter 模塊的 SentinelDubboProviderFilter 的實現中不難看出,在其入口處會首先調用 ContextUtil.enter(resourceName, application) 。那咱們就從該方法開始來探究上下文環境管理機制。node

說到 Sentinel 的調用上下文環境,那調用上下文環境中會保存哪些信息呢?咱們先來看看 Context。面試

1.1 Context 詳解

  • Context 類圖以下:
    Sentinel 調用上下文環境實現原理(含原理圖)

Context
其核心屬性與核心方法以下:redis

  • String name
    Sentinel 調用上下文環境的名稱。
  • DefaultNode entranceNode
    調用鏈的入口節點信息。
  • Entry curEntry
    調用鏈中當前節點的信息。
  • boolean async
    是不是異步調用上下文環境。
  • Entry
    保存當前的調用信息,其主要核心屬性:
  • private long createTime
    資源調用的時間戳。
  • private Node curNode
    該資源所對應的實時採集信息。
  • protected ResourceWrapper resourceWrapper
    資源對象。
  • CtEntry
    同步調用調用信息封裝對象。數據庫

  • AsyncEntry
    異步調用調用信息的封裝對象。

對應的核心方法將在下文具體用到時再詳細介紹。apache

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 中兩個最核心的屬性:
Sentinel 調用上下文環境實現原理(含原理圖)數據結構

首先使用 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() + 1);
                                        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 中,會定義一個固定根節點,其定義以下:

Sentinel 調用上下文環境實現原理(含原理圖)
其資源名稱爲:machine-root。addChild 方法就是將節點添加到以下數據結構中:
Sentinel 調用上下文環境實現原理(含原理圖)

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 處理鏈的第一個節點。

二、NodeSelectorSlot


2.1 NodeSelectorSlot 調用鏈概述

從該類的註釋能夠得出以下的結論:該類的做用是構建一顆虛擬調用樹,咱們接下來以一個Dubbo調用示例來講明。

Sentinel 調用上下文環境實現原理(含原理圖)

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

那上面這個調用鏈保存在線程上下文環境中,即 ThreadLocal 中。在 Sentinel 中使用 Node 來表示一個一個調用節點,其中 EntranceNode 表示調用鏈的入口,DefaultNode 表示普通節點,ClusterNode 表示集羣節點,即同一個資源會統計整個集羣中的信息。
從該類的註釋咱們能夠得出上述的結論,接下來咱們從源碼的角度對其進行分析與理解。

2.2 源碼分析 NodeSelectorSlot

NodeSelectorSlot 中只聲明瞭一個惟一的成員變量,其聲明以下:

private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

定義一個 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 的 名稱呢?這樣設計的目的是什麼,能有什麼好處?

歡迎加入個人知識星球,一塊兒交流源碼,探討架構,打造高質量的技術交流圈,長按以下二維碼
Sentinel 調用上下文環境實現原理(含原理圖)

中間件興趣圈 知識星球 正在對以下話題展開如火如荼的討論:

一、【讓天下沒有難學的Netty-網絡通道篇】一、Netty4 Channel概述(已發表)二、Netty4 ChannelHandler概述(已發表)三、Netty4事件處理傳播機制(已發表)四、Netty4服務端啓動流程(已發表)五、Netty4 NIO 客戶端啓動流程六、Netty4 NIO線程模型分析七、Netty4編碼器、解碼器實現原理八、Netty4 讀事件處理流程九、Netty4 寫事件處理流程十、Netty4 NIO Channel其餘方法詳解二、Java 併發框架(JUC) 探討【面試神器】三、源碼分析Alibaba Sentienl 專欄背後的寫做與學習技巧。

相關文章
相關標籤/搜索