尋找一把進入 Alibaba Sentinel 的鑰匙(文末附流程圖)

通過前面幾篇文章的鋪墊,咱們正式來探討 Sentinel 的 entry 方法的實現流程。即探究進入 Alibaba Sentinel 核心的一把鑰匙。java

@設計模式


不管是從 Sentinel 適配 Dubbo 也好,仍是 SphU 源碼中的註釋中能看出,對一個資源進行限流或熔斷,一般須要調用 SphU 的 entry 方法,例如以下示例代碼。

public void foo() {
	Entry entry = null;
	try {
		entry = SphU.entry("abc");
	} catch (BlockException blockException) {
		// when goes there, it is blocked
		// add blocked handle logic here
	} catch (Throwable bizException) {
		// business exception
		Tracer.trace(bizException);
	} finally {
		// ensure finally be executed
		if (entry != null){
			entry.exit();
		}
	}
}

那本文未來探討 SphU.entry 的實現原理。SphU 類定義了不少 entry 重載方法,咱們就如下面這個方法爲例來探究其實現原理。緩存

一、SphU.entry 流程分析

public static Entry entry(String name, EntryType type, int count, Object... args) throws  BlockException {  // @1
	return Env.sph.entry(name, type, count, args);  // @2
}

代碼@1:咱們先來簡單介紹其核心參數的含義:架構

  • String name
    資源的名稱。
  • EntryType type
    進入資源的方式,主要包含 EntryType.IN、EntryType.OUT。
  • int count
    能夠理解爲本次進入須要消耗的「令牌數」。
  • Object... args
    其餘參數。

代碼@2:調用 Env.sph.entry 的方法,其最終會調用 CtSph 的 entry 方法。併發

接下來咱們將重點查看 CtSph 的 entry 方法。app

public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
    StringResourceWrapper resource = new StringResourceWrapper(name, type); // @1
    return entry(resource, count, args);  // @2
}

代碼@1:因爲該方法用來表示資源的方式爲一個字符串,故建立一個 StringResourceWrapper 對象來表示一個 Sentinel 中的資源,另一個實現爲 MethodResourceWrapper,用來表示方法類的資源。分佈式

代碼@2:繼續調用 CtSph 的另一個 entry 重載方法,最終會調用 entryWithPriority 方法。ide

CtSph#entryWithPriority高併發

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) // @1
        throws BlockException {
    Context context = ContextUtil.getContext();  // @2
    if (context instanceof NullContext) {
      return new CtEntry(resourceWrapper, null, context); 
    }
    if (context == null) {
        // Using default context.
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }
   if (!Constants.ON) {   // @3
        return new CtEntry(resourceWrapper, null, context);
    }
   
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);   // @4
    if (chain == null) {
	    return new CtEntry(resourceWrapper, null, context);
    }
    Entry e = new CtEntry(resourceWrapper, chain, context);     // @5
    try {
	    chain.entry(context, resourceWrapper, null, count, prioritized, args);   // @6
    } catch (BlockException e1) {                                                                    // @7
	    e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

代碼@1:咱們先來介紹一下該方法的參數:源碼分析

  • ResourceWrapper resourceWrapper
    資源的包裝類型,能夠是字符串類型的資源描述,也能夠是方法類的。
  • int count
    這次須要消耗的令牌。
  • boolean prioritized
    是否注重優先級。
  • Object... args
    額外參數。

代碼@2:獲取方法調用的上下文環境,上下環境對象存儲在線程本地變量:ThreadLocal 中,這裏先「劇透」一下,上下文環境中存儲的是整個調用鏈,後續文章會重點介紹。

代碼@3:Sentinel 提供一個全局關閉的開關,若是關閉,返回的 CtEntry 中的 chain 爲空,從這裏能夠看出,若是 chain 爲空,則不會觸發 Sentinel 流控相關的邏輯,從側面也反應了該屬性的重要性。

代碼@4:爲該資源加載處理鏈鏈,這裏是最最重要的方法,將在下文詳細介紹。

代碼@5:根據資源ID、處理器鏈、上下文環境構建 CtEntry 對象。

代碼@6:調用 chain 的 entry 方法。

代碼@7:若是出現 BlockException ,調用 CtEntry 的 exit 方法。

二、Sentienl ProcessorSlot 處理鏈

咱們接下來重點看一下 lookProcessChain 方法的實現細節。
CtSph#lookProcessChain

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);  // @1
    if (chain == null) {
        synchronized (LOCK) {
	    chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // Entry size limit.
                if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {        // @2
		    return null;
                }
                chain = SlotChainProvider.newSlotChain();                                      // @3
                Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                        chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    return chain;
}

代碼@1:chainMap 一個全局的緩存表,即同一個資源 ResourceWrapper (同一個資源名稱) 會共同使用同一個 ProcessorSlotChain ,即不一樣的線程在訪問同一個資源保護的代碼時,這些線程將共同使用 ProcessorSlotChain 中的各個 ProcessorSlot 。注意留意 ResourceWrapper 的 equals 方法與 hashCode 方法。

代碼@2:這裏重點想突出,若是同時在進入的資源個數超過 MAX_SLOT_CHAIN_SIZE,默認爲 6000,會返回 null,則不對本次請求執行限流,熔斷計算,而是直接跳過,這個點仍是值得咱們注意的。

代碼@3:經過 SlotChainProvider 建立對應的處理鏈。

SlotChainProvider#newSlotChain

public static ProcessorSlotChain newSlotChain() {
	if (slotChainBuilder != null) {     // @1
		return slotChainBuilder.build();
        }
	slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);   // @2
	if (slotChainBuilder == null) {                                                                                                                                        // @3
		RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
                slotChainBuilder = new DefaultSlotChainBuilder();
         } else {
		RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: "
                + slotChainBuilder.getClass().getCanonicalName());
         }
         return slotChainBuilder.build();                                                                                                                                   // @4
}

代碼@1:若是 slotChainBuilder 不爲空,則直接調用其 build 方法構建處理器鏈。

代碼@2:若是爲空,首先經過 JAVA 的 SPI 機制,嘗試加載自定義的 Slot Chain 構建器實現類。若是須要實現自定義的 Chain 構建器,只需實現 SlotChainBuilder 接口,而後將其放在 classpath 下便可,若是存在多個,以找到的第一個爲準。

代碼@3:若是從 SPI 機制中加載失敗,則使用默認的構建器:DefaultSlotChainBuilder。

代碼@4:調用其 build 方法構造 Slot Chain。

那接下來咱們先來看看 Sentinel 的 SlotChainBuilder 類體系,而後看看 DefaultSlotChainBuilder 的 build 方法。

2.1 SlotChainBuilder 類體系

在這裏插入圖片描述
主要有三個實現類,對應熱點、接口網關以及普通場景。咱們接下來將重點介紹 DefaultSlotChainBuilder ,關於熱點限流與網關限流將在後面的文章中詳細探討。

2.2 DefaultSlotChainBuilder build 方法

DefaultSlotChainBuilder#build

public class DefaultSlotChainBuilder implements SlotChainBuilder {
    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;
    }
}

就問你們激不激動,開不開心,從這些 Slot 的名字基本就能得出其含義。

  • NodeSelectorSlot
    主要用於構建調用鏈。
  • ClusterBuilderSlot
    用於集羣限流、熔斷。
  • LogSlot
    用於記錄日誌。
  • StatisticSlot
    用於實時收集實時消息。
  • AuthoritySlot
    用於權限校驗的。
  • SystemSlot
    用於驗證系統級別的規則。
  • FlowSlot
    實現限流機制。
  • DegradeSlot
    實現熔斷機制。

通過上面的方法,就構建一條 Slot 處理鏈。其實到這裏咱們就不難發現,調用 ProcessorSlotChain 的 entry 方法,就是依次調用這些 slot 的方法。關於 ProcessorSlotChain 的類層次結構就再也不多說明了,其實現比較簡單,你們若是有興趣的話,能夠關注這部分的實現,這裏表明一類場景:一對多、責任鏈的設計模式。

三、Sentinel SphU.entry 處理流程圖

通過上面的探索,咱們其實已經找到了 Sentinel 的關於限流、熔斷核心處理邏輯的入口,就是 FlowSlot、DegradeSlot。接下來咱們以一張流程圖來結束本文的講解。
在這裏插入圖片描述

本文的目的就是打開 Sentinel 的大門,即尋找實時數據收集、限流、熔斷實現機制的入口,從而正式探尋 Sentienl 的核心實現原理,更多精彩請繼續期待該專欄的後續內容。

點贊是一種美德,若是以爲本文寫的不錯的話,還請幫忙點個贊,您的承認是我持續創造的最大動力,謝謝。

推薦閱讀:源碼分析 Alibaba Sentinel 專欄。
一、Alibaba Sentinel 限流與熔斷初探(技巧篇)
二、源碼分析 Sentinel 之 Dubbo 適配原理
三、源碼分析 Alibaba sentinel 滑動窗口實現原理(文末附原理圖)


做者信息:丁威,《RocketMQ技術內幕》做者,目前擔任中通科技技術平臺部資深架構師,維護 中間件興趣圈公衆號,目前主要發表了源碼閱讀java集合、JUC(java併發包)、Netty、ElasticJob、Mycat、Dubbo、RocketMQ、mybaits等系列源碼。點擊連接:加入筆者的知識星球,一塊兒探討高併發、分佈式服務架構,分享閱讀源碼心得。

相關文章
相關標籤/搜索