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

引言

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

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

Sentinel提供兩種埋點方式:node

  • 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 等,來控制總的入口流量

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

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

簡單解釋下上圖:ide

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

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

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

源碼分析

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

// 建立一個名稱爲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系列

相關文章
相關標籤/搜索