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

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

@


無論是從 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 重載方法,我們就以下面這個方法爲例來探究其實現原理。

1、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 方法。

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 方法。

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 方法。

2、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 的類層次結構就不再多說明了,其實現比較簡單,大家如果有興趣的話,可以關注這部分的實現,這裏代表一類場景:一對多、責任鏈的設計模式。

3、Sentinel SphU.entry 處理流程圖

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

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

點贊是一種美德,如果覺得本文寫的不錯的話,還請幫忙點個贊,您的認可是我持續創造的最大動力,謝謝。

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


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

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