Alibaba Sentinel 骨架源碼閱讀

Sentinel 的核心骨架,將不同的 Slot 按照順序串在一起(責任鏈模式),從而將不同的功能(限流、降級、系統保護)組合在一起。slot chain 其實可以分爲兩部分:統計數據構建部分(statistic)和判斷部分(rule checking)。核心結構:

業務埋點示例

// 資源的唯一標識
String resourceName = "testSentinel";
Entry entry = null;
String retVal;
try {
    entry = SphU.entry(resourceName, EntryType.IN);
    // TODO 業務邏輯 
    retVal = "passed";
} catch (BlockException e) {
    // TODO 降級邏輯
    retVal = "blocked";
} catch (Exception e) {
    // 異常數統計埋點
    Tracer.trace(e);
    throw new RuntimeException(e);
} finally {
    if (entry != null) {
        entry.exit();
    }
}

這段代碼是Sentinel業務埋點示例,通過示例我們可以看出Sentinel對資源的控制入口是SphU.entry(resourceName, EntryType.IN);,源碼如下:

public static Entry entry(String name, EntryType type) throws BlockException {
    return Env.sph.entry(name, type, 1, OBJECTS0);
}

這裏第一個參數是受保護資源的唯一名稱;第二個參數表示流量類型:

  • EntryType.IN:是指進入我們系統的入口流量,比如 http 請求或者是其他的 rpc 之類的請求,設置爲IN主要是爲了保護自己系統。
  • EntryType.OUT:是指我們系統調用其他第三方服務的出口流量,設置爲OUT是爲了保護第三方系統。

這段代碼沒什麼邏輯,只是轉發了下,跟進源碼可以發現最終邏輯實在CtSph#entryWithPriority(ResourceWrapper, int, boolean, Object...)方法中。

Sentinel 骨架代碼

Sentinel的核心是資源,這裏的資源可以是任何東西,服務,服務裏的方法,甚至是一段代碼。而SphU.entry(resourceName);這段代碼的主要作用是 :

  1. 定義一個Sentinel資源
  2. 檢驗資源所對應的規則是否生效

核心代碼如下:

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 獲取當前線程上下文,Context是通過ThreadLocal維護,每一個Context都會有一個EntranceNode實例,它是dashboard【簇點鏈路】中的根節點,主要是用來區分調用鏈路的
    Context context = ContextUtil.getContext();
    if (context instanceof NullContext) {
        // 如果是 NullContext,表示 Context 個數超過了閾值,這個時候 Sentinel 不會應用規則,即不會觸發限流降級等規則,也不會觸發QPS等數據統計。
        // 閾值大小 =Constants.MAX_CONTEXT_NAME_SIZE = 2000,具體可以查看 ContextUtil#trueEnter。
        return new CtEntry(resourceWrapper, null, context);
    }

    if (context == null) {
        // 如果沒有設置上下文,即使用默認上下文,默認上下文的名稱是  sentinel_default_context
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }

    if (!Constants.ON) {
        // Sentinel 的全局控制開關,一旦關閉則不進行任何檢查
        return new CtEntry(resourceWrapper, null, context);
    }

    // 通過Sentinel的官方文檔我們可以知道,Sentinel的核心功能是基於一系列的功能插槽來實現的,而組織這些功能插槽使用的是責任鏈模式。
    // 這裏是通過資源(每個資源是唯一的),獲取第一個功能插,即該資源對應的規則入口。
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

    /*
     * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
     * so no rule checking will be done.
     */
    // 如果一個服務中,資源數量操過閾值(最大的插槽鏈),則返回null,即不會再應用規則,直接返回。
    // 閾值大小 = Constants.MAX_SLOT_CHAIN_SIZE = 6000
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }

    // 構建Sentinel調用鏈入口
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        // 開始執行插槽鏈,如果某個插槽匹配上了某個規則,如限流規則,就會拋出BlockException異常,這時表示請求被拒絕了。
        // 業務層面會去捕獲這個異常,然後做熔斷,降級操作。
        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;
}

核心邏輯如下:

  1. 通過當前線程的上下文,獲取到當前線程的【簇點鏈路】入口。
  2. 判斷全局開關是否關閉。
  3. 通過唯一的資源標識獲取到對應的功能插槽鏈(ProcessorSlot)的第一個插槽。
  4. 構建Sentinel調用鏈入口,並執行調用鏈
  5. 如果拋出BlockException表示觸發了資源限制規則,需要進行熔斷降級。

這裏有兩個需要注意的地方:

  1. 【簇點鏈路】入口Context的數量是有限制的,最大2000個,通常情況下,我們都不需要顯示設置 context,使用默認的就好了,這樣Context數量限制基本上不會觸發。
  2. SphU.entry(resourceName, EntryType.IN),這裏的資源的唯一標識resourceName也是有限制的,最大是6000。當Sentinel與 Servlet 的整合後,CommonFilter會將所有的對外接口定義成Sentinel的資源,資源名稱就是接口地址,所以要控制好服務接口數量。

ContextUtil#enter

ContextUtil#enter(String name, String origin)的主要作用就是創建當前線程的上下文Context,每個上下文會對應一個EntranceNode(入口節點)實例,通常情況下我們不需要顯示調用該方法。

  • name:上下文的唯一標識,也是入口節點的資源名稱。
  • orgin:表示來源,通常是服務消費者或調用者的應用名稱,當我們需要對不同來源的消費者或調用者進行限制時就會用到這個參數。

源碼如下:

public static Context enter(String name, String origin) {
    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) {
            // 判斷是否超過最大長度限制(樂觀鎖機制)
            if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                setNullContext();
                return NULL_CONTEXT;
            } else {
                try {
                    LOCK.lock();
                    // 雙重判斷
                    node = contextNameNodeMap.get(name);
                    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);
                            // 將入口節點添加到全局根節點下(machine-root)
                            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;
}

如果我們再代碼中顯示調用這個方法:

ContextUtil.enter("context1", "service-1"); 
...
ContextUtil.exit();

ContextUtil.enter("context2", "service-1"); 
...
ContextUtil.exit();

那麼會創建如下一個樹結構圖:

這裏有兩點需要注意:

  1. 也就是上面說的數量限制,2000。
  2. ContextUtil是通過ThreadLocal來維護當前線程的上下文的,所以當遇到異步線程時需要手動調用ContextUtil.runOnContext(context, f)方法來完成父線程和子線程的上下文切換。

文檔中的Demo:

public void someAsync() {
    try {
        AsyncEntry entry = SphU.asyncEntry(resourceName);

        // Asynchronous invocation.
        doAsync(userId, result -> {
            // 在異步回調中進行上下文變換,通過 AsyncEntry 的 getAsyncContext 方法獲取異步 Context
            ContextUtil.runOnContext(entry.getAsyncContext(), () -> {
                try {
                    // 此處嵌套正常的資源調用.
                    handleResult(result);
                } finally {
                    entry.exit();
                }
            });
        });
    } catch (BlockException ex) {
        // Request blocked.
        // Handle the exception (e.g. retry or fallback).
    }
}

lookProcessChain

Sentinel的核心功能是使用的是責任鏈模式實現,lookProcessChain(resourceWrapper)的主要作用就是用來構造責任鏈,源碼如下:

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    // 根據資源的唯一標識來做本地緩存
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);
    if (chain == null) {
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // 限制資源資對應調用鏈的總數,一個資源對應一條調用鏈
                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;
}

進一步跟進方法會發現,責任鏈是由SlotChainBuilder#build()````去構建的,默認實現類是DefaultSlotChainBuilder```,源碼如下:

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

        // 找到ProcessorSlot所有的實現類,並排序
        List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
        for (ProcessorSlot slot : sortedSlotList) {
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
                continue;
            }
            // 將功能槽放到責任鏈最後
            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }
}

老版本直接是硬編碼方式:

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        chain.addLast(new NodeSelectorSlot());
        chain.addLast(new ClusterBuilderSlot());
        chain.addLast(new LogSlot());
        chain.addLast(new StatisticSlot());
        chain.addLast(new AuthoritySlot());
        chain.addLast(new SystemSlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());

        return chain;
    }
}

以下內容來自文檔

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

總結

  1. Sentinel 通過責任鏈模式,將各功能塊隔離,即清晰劃分出了各功能塊的職責邊界,也非常方便擴展。新增功能直接新增功能插槽就行了,不需要改以前代碼。
  2. Sentinel 的本地緩存使用的是HashMap,通過加鎖和寫複製的思想來解決HashMap的線程安全性問題,在讀遠大於寫的場景這種方式非常非常值得借鑑。

參考

https://github.com/alibaba/Sentinel/wiki/Sentinel-%E6%A0%B8%E5%BF%83%E7%B1%BB%E8%A7%A3%E6%9E%90

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章