Sentinel源碼解析一(流程總覽)


引言

Sentinel作為ali開源的一款輕量級流控框架,主要以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度來幫助用戶保護服務的穩定性。相比於HystrixSentinel的設計更加簡單,在 Sentinel中資源定義和規則配置是分離的,也就是說用戶可以先通過Sentinel API給對應的業務邏輯定義資源(埋點),然后在需要的時候再配置規則,通過這種組合方式,極大的增加了Sentinel流控的靈活性。

引入Sentinel帶來的性能損耗非常小。只有在業務單機量級超過25W QPS的時候才會有一些顯著的影響(5% - 10% 左右),單機QPS不太大的時候損耗幾乎可以忽略不計。

Sentinel提供兩種埋點方式:

  • try-catch 方式(通過 SphU.entry(...)),用戶在 catch 塊中執行異常處理 / fallback
  • if-else 方式(通過 SphO.entry(...)),當返回 false 時執行異常處理 / fallback

寫在前面

在此之前,需要先了解一下Sentinel的工作流程
Sentinel 里面,所有的資源都對應一個資源名稱(resourceName),每次資源調用都會創建一個 Entry 對象。Entry 可以通過對主流框架的適配自動創建,也可以通過注解的方式或調用 SphU API 顯式創建。Entry 創建的時候,同時也會創建一系列功能插槽(slot chain),這些插槽有不同的職責,例如默認情況下會創建一下7個插槽:

  • NodeSelectorSlot 負責收集資源的路徑,並將這些資源的調用路徑,以樹狀結構存儲起來,用於根據調用路徑來限流降級;
  • ClusterBuilderSlot 則用於存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count 等等,這些信息將用作為多維度限流,降級的依據;
  • StatisticSlot 則用於記錄、統計不同緯度的 runtime 指標監控信息;
  • FlowSlot 則用於根據預設的限流規則以及前面 slot 統計的狀態,來進行流量控制;
  • AuthoritySlot 則根據配置的黑白名單和調用來源信息,來做黑白名單控制;
  • DegradeSlot 則通過統計信息以及預設的規則,來做熔斷降級;
  • SystemSlot 則通過系統的狀態,例如 load1 等,來控制總的入口流量

注意:這里的插槽鏈都是一一對應資源名稱的

上面的所介紹的插槽(slot chain)是Sentinel非常重要的概念。同時還有一個非常重要的概念那就是Node,為了幫助理解,盡我所能畫了下面這張圖,可以看到整個結構非常的像一棵樹:

簡單解釋下上圖:

  • 頂部藍色的node節點為根節點,全局唯一
  • 下面黃色的節點為入口節點,每個CentextName(上下文名稱)一一對應一個
    • 可以有多個子節點(對應多種資源)
  • 中間綠色框框中的節點都是屬於同一個資源的(相同的ResourceName)
  • 最底下紫色的節點是集群節點,可以理解成綠色框框中Node資源的整合
  • 最右邊的指的是不同的來源(origin)流量,同一個EntranceNode可以有多個來源

以上2個概念務必要理清楚,之后再一步一步看源碼會比較清晰

下面我們將從入口源碼開始一步一步分析整個調用過程:

源碼分析

下面的是一個Sentinel使用的示例代碼,我們就從這里切入開始分析

// 創建一個名稱為entrance1,來源為appA 的上下文Context
ContextUtil.enter("entrance1", "appA");
// 創建一個資源名稱nodeA的Entry
 Entry nodeA = SphU.entry("nodeA");
 if (nodeA != null) {
    nodeA.exit();
 }
 // 清除上下文
 ContextUtil.exit();

ContextUtil.enter("entrance1", "appA")

public static Context enter(String name, String origin) {
	  // 判斷上下文名稱是否為默認的名稱(sentinel_default_context) 是的話直接拋出異常
    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);
}

protected static Context trueEnter(String name, String origin) {
	  // 先從ThreadLocal中嘗試獲取,獲取到則直接返回
    Context context = contextHolder.get();
    if (context == null) {
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        // 嘗試從緩存中獲取該上下文名稱對應的 入口節點
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
      		 // 判斷緩存中入口節點數量是否大於2000
            if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                setNullContext();
                return NULL_CONTEXT;
            } else {
                try {
                		// 加鎖
                    LOCK.lock();
                    // 雙重檢查鎖
                    node = contextNameNodeMap.get(name);
                    if (node == null) {
                    	 // 判斷緩存中入口節點數量是否大於2000
                        if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                            setNullContext();
                            return NULL_CONTEXT;
                        } else {
                            // 根據上下文名稱生成入口節點(entranceNode)
                            node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                            // 加入至全局根節點下
                            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;
}

主要做了2件事情

  1. 根據ContextName生成entranceNode,並加入緩存,每個ContextName對應一個入口節點entranceNode
  2. 根據ContextNameentranceNode初始化上下文對象,並將上下文對象設置到當前線程中

這里有幾點需要注意:

  1. 入口節點數量不能大於2000,大於會直接拋異常
  2. 每個ContextName對應一個入口節點entranceNode
  3. 每個entranceNode都有共同的父節點。也就是根節點

Entry nodeA = SphU.entry("nodeA")

// SphU.class
public static Entry entry(String name) throws BlockException {
    // 默認為 出口流量類型,單位統計數為1
    return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}

// CtSph.class
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
    // 生成資源對象
    StringResourceWrapper resource = new StringResourceWrapper(name, type);
    return entry(resource, count, args);
}
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
    return entryWithPriority(resourceWrapper, count, false, args);
}

上面的代碼比較簡單,不指定EntryType的話,則默認為出口流量類型,最終會調用entryWithPriority方法,主要業務邏輯也都在這個方法中

  • entryWithPriority方法
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 獲取當前線程上下文對象
    Context context = ContextUtil.getContext();
    // 上下文名稱對應的入口節點是否已經超過閾值2000,超過則會返回空 CtEntry
    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);

    /*
     * 表示資源(插槽鏈)超過6000,因此不會進行規則檢查。
     */
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }
    // 生成 Entry 對象
    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) {
        // 除非Sentinel內部存在錯誤,否則不應發生這種情況。
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

這個方法可以說是涵蓋了整個Sentinel的核心邏輯

  1. 獲取上下文對象,如果上下文對象還未初始化,則使用默認名稱初始化。初始化邏輯在上文已經分析過
  2. 判斷全局開關
  3. 根據給定的資源生成插槽鏈,插槽鏈是跟資源相關的,Sentinel最關鍵的邏輯也都在各個插槽中。初始化的邏輯在lookProcessChain(resourceWrapper);中,下文會分析
  4. 依順序執行每個插槽邏輯

lookProcessChain(resourceWrapper)方法

lookProcessChain方法為指定資源生成插槽鏈,下面我們來看下它的初始化邏輯

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    // 根據資源嘗試從全局緩存中獲取
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);
    if (chain == null) {
        // 非常常見的雙重檢查鎖
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // 判斷資源數是否大於6000
                if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                    return null;
                }
                // 初始化插槽鏈
                chain = SlotChainProvider.newSlotChain();
                Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                    chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    return chain;
}
  1. 根據資源嘗試從全局緩存中獲取插槽鏈。每個資源對應一個插槽鏈(資源嘴多只能定義6000個)
  2. 初始化插槽鏈上的插槽(SlotChainProvider.newSlotChain()方法中)

下面我們看下初始化插槽鏈上的插槽的邏輯

SlotChainProvider.newSlotChain()

public static ProcessorSlotChain newSlotChain() {
    // 判斷是否已經初始化過
    if (builder != null) {
        return builder.build();
    }
	  // 加載 SlotChain 
    resolveSlotChainBuilder();
    // 加載失敗則使用默認 插槽鏈 
    if (builder == null) {
        RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
        builder = new DefaultSlotChainBuilder();
    }
    // 構建完成
    return builder.build();
}

/**
 * java自帶 SPI機制 加載 slotChain
 */
private static void resolveSlotChainBuilder() {
    List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>();
    boolean hasOther = false;
    // 嘗試獲取自定義SlotChainBuilder,通過JAVA SPI機制擴展
    for (SlotChainBuilder builder : LOADER) {
        if (builder.getClass() != DefaultSlotChainBuilder.class) {
            hasOther = true;
            list.add(builder);
        }
    }
    if (hasOther) {
        builder = list.get(0);
    } else {
        // 未獲取到自定義 SlotChainBuilder 則使用默認的
        builder = new DefaultSlotChainBuilder();
    }

    RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
        + builder.getClass().getCanonicalName());
}
  1. 首先會嘗試獲取自定義的SlotChainBuilder來構建插槽鏈,自定義的SlotChainBuilder可以通過JAVA SPI機制來擴展
  2. 如果未配置自定義的SlotChainBuilder,則會使用默認的DefaultSlotChainBuilder來構建插槽鏈,DefaultSlotChainBuilder所構建的插槽就是文章開頭我們提到的7種Slot。每個插槽都有其對應的職責,各司其職,后面我們會詳細分析這幾個插槽的源碼,及所承擔的職責。

總結

文章開頭的提到的兩個點(插槽鏈和Node),這是Sentinel的重點,理解這兩點對於閱讀源碼來說事半功倍

Sentinel系列


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM