流控神器-Sentinel-資源節點樹的構成(NodeSelectorSlot)

目錄

 

1. 概述

2. 一些需要知道的前提

2.1. Resource

2.2. Context

2.3. Entry

2.4. Node

3. 深入分析

3.1. demo啓動

3.2. 創建Context

3.3. 創建Entry

3.4. 執行NodeSelectorSlot.entry

3.4.1. 相同資源名情況下

3.4.2. 不同資源名情況下


1. 概述

之前分析過Sentinel是基於責任鏈的模式,其邏輯處理部分是一個有一個的Slot

這裏大概的介紹下每種Slot的功能職責:

  • NodeSelectorSlot 負責收集資源的路徑,並將這些資源的調用路徑,以樹狀結構存儲起來,用於根據調用路徑來限流降級;

  • ClusterBuilderSlot 則用於存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count 等等,這些信息將用作爲多維度限流,降級的依據;

  • StatisticsSlot 則用於記錄,統計不同維度的 runtime 信息;

  • SystemSlot 則通過系統的狀態,例如 load1 等,來控制總的入口流量;

  • AuthoritySlot 則根據黑白名單,來做黑白名單控制;

  • FlowSlot 則用於根據預設的限流規則,以及前面 slot 統計的狀態,來進行限流;

  • DegradeSlot 則通過統計信息,以及預設的規則,來做熔斷降級;

每個Slot執行完業務邏輯處理後,會調用fireEntry()方法,該方法將會觸發下一個節點的entry方法,下一個節點又會調用他的fireEntry,以此類推直到最後一個Slot,由此就形成了sentinel的責任鏈。

2. 一些需要知道的前提

2.1. Resource

資源,是Sentinel世界中的抽象,任何東西都能被定義成資源,自己提供的服務,調用的服務,甚至一段代碼,有了資源才能在資源上定義規則,去進行限流降級之類的操作

在Sentinel中提供了兩個默認是Resource分別是StringResourceWrapper和MethodResourceWrapper

2.2. Context

Context是上下文的意思,一個線程對應一個Context

其中有三個屬性

  • name:名字
  • entranceNode:調用鏈入口
  • curEntry:當前entry
  • origin:調用者來源
  • async:異步

2.3. Entry

每次調用 SphU.entry() 都會生成一個Entry入口,該入口中會保存了以下數據:入口的創建時間,當前入口所關聯的節點(Node),當前入口所關聯的調用源對應的節點。Entry是一個抽象類,他只有一個實現類,在CtSph中的一個靜態類:CtEntry

其中有這些屬性

  • createtime
  • curNode
  • originNode
  • error
  • resourceWrapper
  • parent
  • child
  • chain
  • context

紅色的是抽象類Entry的,黑色的是CtEntry中的

一個Entry相當於一個token只有正常生成了一個entry才能算pass,不然報異常BlockException肯定是限流了

2.4. Node

節點是用來保存某個資源的各種實時統計信息的,他是一個接口,通過訪問節點,就可以獲取到對應資源的實時狀態,以此爲依據進行限流和降級操作。

有幾種節點類型

  • StatisticNode:統計節點
  • DefaultNode:默認節點,NodeSelectorSlot中創建的就是這個節點
  • ClusterNode:集羣節點
  • EntranceNode:該節點表示一棵調用鏈樹的入口節點,通過他可以獲取調用鏈樹中所有的子節點

3. 深入分析

3.1. demo啓動

Entry entry = null;

try {

    entry = SphU.entry("abc");

    entry = SphU.entry("abc");

} catch (BlockException e1) {

     

} finally {

    if (entry != null) {

        entry.exit();

    }

}

CtSph.entryWithPriority

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)

    throws BlockException {

    //先從ThreadLocal獲取,第一次肯定是null

    Context context = ContextUtil.getContext();

    if (context instanceof NullContext) {

        // The {@link NullContext} indicates that the amount of context has exceeded the threshold,

        // so here init the entry only. No rule checking will be done.

        return new CtEntry(resourceWrapper, null, context);

    }



    if (context == null) {

        //生成Context的部分

        context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());

    }

    //省略

     

}

3.2. 創建Context

ContextUtil.trueEnter

protected static Context trueEnter(String name, String origin) {

    //從ThreadLocal中獲取,第一次肯定是null

    Context context = contextHolder.get();

    if (context == null) {

        //這裏是根據Context的名字獲取Node

        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 {

                            //創建個EntranceNode

                            node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);

                            //加入全局的節點

                            Constants.ROOT.addChild(node);

                            //當如map中

                            Map<String, DefaultNode> newMap = new HashMap<String, DefaultNode>(

                                contextNameNodeMap.size() + 1);

                            newMap.putAll(contextNameNodeMap);

                            newMap.put(name, node);

                            contextNameNodeMap = newMap;

                        }

                    }

                } finally {

                    LOCK.unlock();

                }

            }

        }

        context = new Context(node, name);

        context.setOrigin(origin);

        //放入ThreadLocal中

        contextHolder.set(context);

    }



    return context;

}

這裏的邏輯還是比較簡單的

  1. 首先在ThreadLocal獲取,獲取不到就創建,不然就返回
  2. 然後再Map中根據ContextName找一個Node
  3. 沒有找到Node就加鎖的方式,創建一個EntranceNode,然後放入Map中
  4. 創建Context,設置node,name,origin,再放入ThreadLocal中

到此Context就創建完成

目前Context對象的狀態如下圖

 

3.3. 創建Entry

新建Entry的過程

Entry e = new CtEntry(resourceWrapper, chain, context);
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {

    super(resourceWrapper);

    this.chain = chain;

    this.context = context;



    setUpEntryFor(context);

}

private void setUpEntryFor(Context context) {

    // The entry should not be associated to NullContext.

    if (context instanceof NullContext) {

        return;

    }

    this.parent = context.getCurEntry();

    if (parent != null) {

        ((CtEntry)parent).child = this;

    }

    context.setCurEntry(this);

}

當第一次Entry生成的時候,context.getCurEntry必定是NULL,那麼直接執行Context.setCurEntry方法

然後這個Context的狀態如下圖

再執行一次新的Sphu.entry後會再次新建一個Entry,這個時候curEntry不是null,那麼執行((CtEntry)parent).child = this;

結果如下圖

可以看出,原來的CtEntry被移出Context,新建的CtEntry和舊CtEntry通過內部的parent和child引用相連

 

3.4. 執行NodeSelectorSlot.entry

之前分析過,Sentinel是基於責任鏈模式,責任鏈第一個slot是NodeSelectorSlot

public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)

    throws Throwable {

    //這裏有個緩存,根據context的名字緩存node

    DefaultNode node = map.get(context.getName());

    //雙重檢測,線程安全

    if (node == null) {

        synchronized (this) {

            node = map.get(context.getName());

            if (node == null) {

                //這裏生成的是DefaultNode節點

                node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);

                //下面這些邏輯是放入map的邏輯,因爲後期map比較大,所以這樣放入,性能會高一些

                HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());

                cacheMap.putAll(map);

                cacheMap.put(context.getName(), node);

                map = cacheMap;

            }

            // 關鍵在這,這是修改調用鏈樹的地方

            ((DefaultNode)context.getLastNode()).addChild(node);

        }

    }

    //替換context中的curEntry中的curNode

    context.setCurNode(node);

    fireEntry(context, resourceWrapper, node, count, prioritized, args);

}

// Context.java

public Context setCurNode(Node node) {

    this.curEntry.setCurNode(node);

    return this;

}

public Node getLastNode() {

    if (curEntry != null && curEntry.getLastNode() != null) {

        return curEntry.getLastNode();

    } else {

        return entranceNode;

    }

}

//CteEntry.java

public Node getLastNode() {

    return parent == null ? null : parent.getCurNode();

}

//DefaultNode.java

public void addChild(Node node) {

    if (node == null) {

        RecordLog.warn("Trying to add null child to node <{0}>, ignored", id.getName());

        return;

    }

    if (!childList.contains(node)) {

        synchronized (this) {

            if (!childList.contains(node)) {

                Set<Node> newSet = new HashSet<Node>(childList.size() + 1);

                newSet.addAll(childList);

                newSet.add(node);

                childList = newSet;

            }

        }

        RecordLog.info("Add child <{0}> to node <{1}>", ((DefaultNode)node).id.getName(), id.getName());

    }

}


查詢緩存中是否有這個node這裏的邏輯也很簡單

  • 根據ContextName查詢緩存是否有這個Node
  • 沒有就生成這個DefaultNode,放入緩存,然後構造調用樹鏈
  • Context的curEntry中的curnode設置爲這個node

但是這樣有個情況是第二步不是經常出現的,第二步出現的前提是node取不到,而node在緩存中獲取不到的條件是contextName不同,除非不同線程纔有可能不同contextName,不僅如此,還有非NodeSelectorSlot同對象,那麼其中的map是不同的

 

 

 

3.4.1. 相同資源名情況下

假設我們先回到現有一個Entry生成的的情況下

然後執行NodeSelectorSlot中的entry方法

這個時候curEntry是等於CtEntry,但是CtEntry中的parent是null,所以getLastNode還是返回entranceNode

然後再執行下面方法setCurNode

結果如下圖

注:這兩個Node是相同的Node,是一個對象

然後再執行一次生成Entry和一次NodeSelectorSlot中的entry方法

這一次Context還是相同的Context,因爲再一個線程中,那麼就不會再生成新的Node,只執行上述過程的第一步和第三步

結果如下圖

注:這三個Node都是相同的Node,因爲根據ContextName從緩存中獲取的

 

 

3.4.2. 不同資源名情況下

Entry entry = null;

try {

    entry = SphU.entry("abc");  //資源名abc

    System.out.println("pass");

    entry = SphU.entry("abcd");  //資源名abcd

    System.out.println("pass");

} catch (BlockException e1) {

    System.out.println("block");

} finally {

    if (entry != null) {

        entry.exit();

    }

}

爲什麼不同資源名會不同?

本質在於ChainSlot的生成問題

 

ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);





ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {

    //可以看出,chain鏈根據資源來作爲key,不同的資源肯定是不同chain鏈

    ProcessorSlotChain chain = chainMap.get(resourceWrapper);

    if (chain == null) {

        synchronized (LOCK) {

            chain = chainMap.get(resourceWrapper);

            if (chain == null) {

                // Entry size limit.

                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;

}

然後在NodeSelectorSlot中

private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

這是個私有屬性,不同資源情況下,默認的map中是沒有緩存的

下面開始分析

先回到第一個Entry生成並執行了NodeSelectorSlot的entry方法情況

然後在執行一次Entry生成並執行了NodeSelectorSlot的entry方法情況

結果如下圖

 

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