1. 程式人生 > 程式設計 >Sentinel原始碼分析(第二篇):NodeSelectorSlot和ClusterBuilderSlot分析

Sentinel原始碼分析(第二篇):NodeSelectorSlot和ClusterBuilderSlot分析

1. 前言

上一篇介紹了Sentinel的Context、Entry、Node相關的資訊。在建立Node時,涉及到了NodeSelectorSlot和ClusterBuilderSlot,建立Entry的時候,會建立一個chain。本文會通過原始碼分析這些物件的作用。

2. 功能插槽

2.1. 功能插槽的作用

當使用SphU.entry()獲取資源建立Entry物件的時候,會建立一系列的功能插槽,這些插槽以責任鏈的方式組合在一起,每個插槽分別處理不同的功能。

在CtSph#entryWithPriority()方法中會嘗試去獲取chain,如果沒有,則建立:

 ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
複製程式碼

具體的獲取流程:

原始碼位置:CtSph.java

private static volatile Map<ResourceWrapper,ProcessorSlotChain> chainMap
        = new HashMap<ResourceWrapper,ProcessorSlotChain>();
        
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    //先從快取中獲取,如果沒有再建立
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);
    if
(chain == null) { synchronized (LOCK) { chain = chainMap.get(resourceWrapper); if (chain == null) { // Entry size limit. 處理鏈的數量不能超過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; } 複製程式碼

可以看到,chain會先儲存到一個靜態的全域性map中,以資源名稱為key,chain為value,所以同一個resource只有一個chain。

實際建立chain的地方是SlotChainProvider.newSlotChain():

#原始碼位置:SlotChainProvider.java

 private static volatile SlotChainBuilder slotChainBuilder = null;

 public static ProcessorSlotChain newSlotChain() {

    //如果builder不為空,建立chain,否則先建立builder,再建立chain
    if (slotChainBuilder != null) {
        return slotChainBuilder.build();
    }

    resolveSlotChainBuilder();

    if (slotChainBuilder == null) {
        RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder,using default");
        slotChainBuilder = new DefaultSlotChainBuilder();
    }
    return slotChainBuilder.build();
}
複製程式碼

2.2. SlotChainBuilder

SlotChainBuilder負責建立功能插槽鏈,SlotChainBuilder是一個介面,裡面只有一個build方法,定義功能插槽如何建立。

可以看到首先看一下SlotChainBuilder有三個實現,不同的實現增加不同的處理能力。

  • DefaultSlotChainBuilder:預設的功能插槽構造器
  • GatewaySlotChainBuilder:閘道器插槽構造器,裡面新增了閘道器限流功能
  • HotParamSlotChainBuilder:熱點引數插槽構造器,裡面新增了熱點引數功能

這裡看一下預設的功能插槽構造器,其他的兩個只是在適當的位置新增一個處理插槽。

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 SystemSlot());
        chain.addLast(new AuthoritySlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());

        return chain;
    }
}
複製程式碼

可以看到build方法主要是建立一個DefaultProcessorSlotChain,然後將不同功能的插槽新增進去。

需要注意的是,Sentinel是通過SPI的形式選擇使用哪個SlotChainBuilder來建立功能插槽的。

private static final ServiceLoader<SlotChainBuilder> LOADER = ServiceLoader.load(SlotChainBuilder.class);
複製程式碼

2.3. ProcessorSlot

ProcessorSlot代表功能插槽物件,定義了進入功能插槽和退出功能插槽的介面,具體的功能插槽都實現了該介面。

ProcessorSlot繼承結構如下:

通過繼承可以發現,ProcessorSlot主要是兩類,一類是具有特定功能的功能插槽,比如限流、統計、降級等,還有一類是主要是形成插槽鏈,主要就是DefaultProcessorSlotChain。

2.3.1. DefaultProcessorSlotChain

DefaultProcessorSlotChain代表功能處理鏈的開始,裡面定義了兩個節點,第一個節點和最後一個節點,並提供新增節點的方法。


public class DefaultProcessorSlotChain extends ProcessorSlotChain {

    //預設先建立一個頭結點插槽
    AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {

        @Override
        public void entry(Context context,ResourceWrapper resourceWrapper,Object t,int count,boolean prioritized,Object... args)
            throws Throwable {
            super.fireEntry(context,resourceWrapper,t,count,prioritized,args);
        }

        @Override
        public void exit(Context context,Object... args) {
            super.fireExit(context,args);
        }

    };
    AbstractLinkedProcessorSlot<?> end = first;

    @Override
    public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        protocolProcessor.setNext(first.getNext());
        first.setNext(protocolProcessor);
        if (end == first) {
            end = protocolProcessor;
        }
    }

    @Override
    public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        end.setNext(protocolProcessor);
        end = protocolProcessor;
    }
}
複製程式碼
2.3.2. AbstractLinkedProcessorSlot

AbstractLinkedProcessorSlot非常的重要,不僅實現了ProcessorSlot,還實現功能插槽鏈之間的如何連線的。

    private AbstractLinkedProcessorSlot<?> next = null;
複製程式碼

AbstractLinkedProcessorSlot裡面通過新增一個next指標標記下一個功能處理槽是哪一個,這樣,在一個功能槽處理完流程後,通過next可以找到下一個功能槽,退出的時候也是一樣的邏輯。

2.4. 功能插槽小結

上面講了功能插槽建立的流程,Sentinel是如何將所有的插槽組織工作的。但具體每個功能插槽是怎麼工作的,在這個地方先不講。最後,通過一個圖看一下DefaultSlotChainBuilder建立的功能插槽鏈結構:

3. NodeSelectorSlot

官方檔案是這樣描述NodeSelectorSlot的:這個 slot 主要負責收集資源的路徑,並將這些資源的呼叫路徑以樹狀結構儲存起來,用於根據呼叫路徑進行流量控制。

先看原始碼:

public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {

    /**
     * {@link DefaultNode}s of the same resource in different context.
     */
    private volatile Map<String,DefaultNode> map = new HashMap<String,DefaultNode>(10);

    @Override
    public void entry(Context context,Object obj,Object... args)
        throws Throwable {
        DefaultNode node = map.get(context.getName());
        if (node == null) {
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {
                    node = new DefaultNode(resourceWrapper,null);
                    HashMap<String,DefaultNode> cacheMap = new HashMap<String,DefaultNode>(map.size());
                    cacheMap.putAll(map);
                    cacheMap.put(context.getName(),node);
                    map = cacheMap;
                    // Build invocation tree
                    ((DefaultNode) context.getLastNode()).addChild(node);
                }

            }
        }

        context.setCurNode(node);
        fireEntry(context,node,args);
    }

    @Override
    public void exit(Context context,Object... args) {
        fireExit(context,args);
    }
}
複製程式碼

NodeSelectorSlot會建立一個DefaultNode,然後將該節點設定到Context下的最新一個節點子節點列表中,然後將上下文的當前節點設定為本次建立的節點。

public Node getLastNode() {
    if (curEntry != null && curEntry.getLastNode() != null) {
        return curEntry.getLastNode();
    } else {
        return entranceNode;
    }
}
複製程式碼

上面是獲取最新節點的邏輯。

NodeSelectorSlot主要作用就是收集資源的路徑。在這個Slot中,建立了一個DefaultNode,在上一篇中講到DefaultNode是以Context和Resource為維度儲存統計指標的,但是這個地方的map是根據Context來儲存的,這個是為什麼呢?需要注意的是功能插槽處理鏈的建立是根據Resource來的,一個Resource只會建立一個chain,不同的Resource建立不同的chain,所以這地方雖然是使用Context作為key,但其實是Context和Resource為維度的。

4. ClusterBuilderSlot

上面的NodeSelector是建立DefaultNode節點,CLusterBuilderSlot主要是負責建立ClusterNode和來源節點。至於這兩個節點的作用,在上一篇已經講過。

public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    private static volatile Map<ResourceWrapper,ClusterNode> clusterNodeMap = new HashMap<>();

    private static final Object lock = new Object();

    private volatile ClusterNode clusterNode = null;

    @Override
    public void entry(Context context,DefaultNode node,Object... args)
        throws Throwable {
        if (clusterNode == null) {
            synchronized (lock) {
                if (clusterNode == null) {
                    // Create the cluster node.
                    clusterNode = new ClusterNode();
                    HashMap<ResourceWrapper,ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(),16));
                    newMap.putAll(clusterNodeMap);
                    newMap.put(node.getId(),clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }
        node.setClusterNode(clusterNode);

        /*
         * if context origin is set,we should get or create a new {@link Node} of
         * the specific origin.
         */
        //如果context定義了來源名稱,需要給當前的entry建立一個originNode
        if (!"".equals(context.getOrigin())) {
            Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }

        fireEntry(context,args);
    }
}
複製程式碼

可以看到,ClusterBuilderSlot中有一個ClusterNode型別的變數,因為之前說過,同一個Resource只有一個Chain,所以這個地方建立的ClusterNode是統計某個Resource的資料,所以ClusterNode的統計維度為Resource。

如果定義了入口的來源,這裡還會建立一個來源節點,作為統計來源節點相關資料的node,這個地方的統計維度是同一個Resource和orgin為維度。

5. 建立Node、Context、Entry整體流程

上面講NodeSelectorSlot和ClusterBuilderSlot的時候,只是把大致的流程說一下,至於建立了Node後整個結構是什麼樣的,這個需要再說一下,如果在看上一篇和這篇中的內容時比較亂,可以看了這一小節後,再去看一下之前的內容。因為到此,整個Sentinel儲存統計資料的流程才講完。

5.1. 再次回憶Context、Entry、Node的建立時機

  • Context:在使用ContextUtil.entry()或者SphU.entry()的時候會建立Context。
  • EntranceNode:在建立Context的時候建立EntranceNode,並賦值給Context。
  • ProcessorSlot::在使用SphU.entry()建立,同一個Resource共享一個。
  • Entry:在使用SphU.entry()建立,並通過parent和child將Entry鏈連線,同時將該Entry物件賦值給Context的curEntry.
  • DefaultNode:在NodeSelectorSlot中建立,以Resource和Context為維度
  • ClusterNode:在ClusterBuilderSlot中建立,以Resource為維度。
  • 來源源節點:在ClusterBuilderSlot中建立,以Resource和orgin為維度。

5.2. 流程圖

首先先看一下程式碼:

 ContextUtil.enter("context1","origin1");
 Entry entry = SphU.entry("resource1");
複製程式碼

當建立一個Entry的時候,整個關係圖如下:

這個時候在獲取同一個資源一次:

 Entry entry2 = SphU.entry("resource1");
複製程式碼

關係圖如下:

接下來在建立不同資源的:

Entry entry3 = SphU.entry("resource2");
Entry entry4 = SphU.entry("resource3");
複製程式碼

關係圖如下:

上面是在同一個Context下,如果是在不同的Context下,由於chain是以Resource為維度的,所以ClusterNode對同一個Resource只會存在一個,所以多個Context下,關係圖就是把上圖複製一份,只是ClusterNode指向同一個。

根據關係圖,可以很清晰的看到ClusterNode、DefaultNode、OrginNode是按什麼樣的維度來進行統計的。

6. 小結

本篇主要講Sentinel通過責任鏈的方式將各種插槽組合起來實現限流降級等功能,並分析了前兩個Slot:NodeSelectorSlot和ClusterNodeSlot的作用。最後結合第一篇的內容畫出不同場景下Context、Entry、Node的關係,更加體現ClusterNode、DefaultNode、源節點是按照什麼維度進行統計的。在下一篇中,將繼續分析Sentinel中的統計功能StatisticSlot,以及各種Node節點是怎麼儲存資料的。

7. 參考資料

Sentinel工作主流程