Sentinel上下文建立及執行

小白先生哦發表於2021-01-19

Sentinel上下文建立及執行,入口示例程式碼:

public static void fun() {
    Entry entry = null;
    try {
        entry = SphU.entry(SOURCE_KEY);
    } catch (BlockException e1) {
    } finally {
        if (entry != null) {
            entry.exit();
        }
    }
}

執行entry

在執行SphU.entry時獲取Entry,Entry代表當前呼叫的入口,用來儲存當前呼叫資訊。

進入到SphU.entry方法可以發現,Entry的獲取使用的是Sph的預設實現CtSph。Sph是資源統計和規則檢查的介面定義。

public class Env {

    public static final Sph sph = new CtSph();

    static {
        // If init fails, the process will exit.
        InitExecutor.doInit();
    }
}

進到CtSph.entry方法:

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

可以看出第一步是建立一個當前資源的包裝類,然後將標識當前請求資源的包裝類傳進entry方法獲取Entry。值得一提的是StringResourceWrapper繼承自ResourceWrapper。而ResourceWrapper重新了hashCode和equals方法,如下:

@Override
public int hashCode() {
    return getName().hashCode();
}

@Override
public boolean equals(Object obj) {
    if (obj instanceof ResourceWrapper) {
        ResourceWrapper rw = (ResourceWrapper)obj;
        return rw.getName().equals(getName());
    }
    return false;
}

可以看出比較兩個Warpper是否指向同一個資源,主要是比較的name,只要獲取的資源名相同那麼就是要求獲取同一個資源,這一點在後面有用。

然後回到CtSph.entry方法,最終進入到了CtSph.entryWithPriority方法,程式碼如下:

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 1.從當前執行緒獲取context
    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) {
        // Using default context.
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }

    // Global switch is close, no rule checking will do.
    if (!Constants.ON) {
        return new CtEntry(resourceWrapper, null, context);
    }

    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.
     */
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }

    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        // This should not happen, unless there are errors existing in Sentinel internal.
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

該方法做了如下幾件事:

  1. 首先嚐試從當前執行緒獲取context,可以看ContextUtil.getContext方法:

    public static Context getContext() {
        return contextHolder.get();
    }
    

    檢視contextHolder屬性是一個ThreadLocal:

    /**
     * Store the context in ThreadLocal for easy access.
     */
    private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
    
  2. 判斷當前執行緒上下文是否超出了閾值,也就是下面語句:

    if (context instanceof NullContext)
    

    我們可以看看NullContext的定義:

    /**
     * If total {@link Context} exceed {@link Constants#MAX_CONTEXT_NAME_SIZE}, a
     * {@link NullContext} will get when invoke {@link ContextUtil}.enter(), means
     * no rules checking will do.
     *
     * @author qinan.qn
     */
    public class NullContext extends Context {
    
        public NullContext() {
        	super(null, "null_context_internal");
        }
    }
    

    當上面判斷為真時,那麼就不再進行規則檢查。

  3. 當從當前執行緒獲取的Context為空時,建立新的Context。**(這裡在下面再詳細解讀。) **

  4. 獲取當前資源對應的Slot執行鏈:

    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
    

    在獲取執行鏈的方法:CtSph.lookProcessChain:

    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.
                    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;
    }
    

    其中chainMap定義:

    private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
            = new HashMap<ResourceWrapper, ProcessorSlotChain>();
    

    可以看出每個資源都是對應的一個執行鏈,在chainMap中就是用ResourceWrapper做為鍵型別,而我們上面已經看到了ResourceWrapper重寫了hashCode和equals方法,所以唯一確定一個資源的就是資源名。

  5. 執行Slot鏈,如果規則檢查未通過那麼丟擲BlockException異常,否則代表符合規則進入成功。

然後接下來看一下Context的建立,也就是下面這段程式碼:

if (context == null) {
    // Using default context.
    context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}

跟蹤程式碼,最終進入到了ContextUtil.trueEnter方法。在閱讀ContextUtil.trueEnte方法時,有必要先看一張圖,來理清一下執行緒thread和Context、Context和Node之間的關係:

圖片來源(該文章可以一看):https://www.jianshu.com/p/e39ac47cd893

img

前面代表的是3個執行緒,可以看成他們都是獲取helloWorld資源,可以看出每一個執行緒在執行的時候都是獨立的建立了一個Context,每一個執行緒裡面的Context都是對應到了一個點EntranceNode上,而該EntranceNode則是用於儲存一個資源的資訊。梳理了這三者之間的關係,那麼接下來看ContextUtil.trueEnt方法,程式碼如下:

protected static Context trueEnter(String name, String origin) {
    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 {
                LOCK.lock();
                try {
                    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);
                            // Add entrance node.
                            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;
}

這個方法做的事情如下:

  1. 從contextHolder中獲取Context,如果有了那麼就直接返回。

  2. 沒有Context資訊,那麼準備開始建立該上下文資訊,準備工作:從contextNameNodeMap中獲取對應節點,contextNameNodeMap定義如下:

    private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
    

    這個map中是上下文名和節點的對應關係,而上下文名即是資源名。

  3. 如果獲取到了這個節點,那麼直接建立一個Context並設定到contextHolder中,然後直接返回。

  4. 當上面節點不存在,那麼先建立該節點,邏輯如下:

    1. 先檢查當前上下文數是否超過指定閾值,如果超過了那麼返回NullContext,本次請求不做規則檢查。

    2. 沒有超過指定閾值,那麼加鎖,進行雙重檢查。

    3. 使用當前資源建立節點,將建立的節點關聯到根節點下,然後存入contextNameNodeMap中。然後建立Context並返回。可以看看在新增節點的時候,它的做法是在原map的基礎上新建一個size+1的新map,然後將原map的所有節點資訊加入新map中,同時儲存新節點資訊:

      Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
      newMap.putAll(contextNameNodeMap);
      newMap.put(name, node);
      contextNameNodeMap = newMap;
      

      而我們的contextNameNodeMap屬性是用volatile進行修飾的,當contextNameNodeMap引用的值發生變更時,能夠立即對其它執行緒可見。

      那麼為什麼不在原來的contextNameNodeMap中直接加入新節點,而要新建map然後進行一次複製呢?

做完上面這些事情後,我們就得到了需要的Context。

執行exit

不管是上面示例程式碼中的finally裡面的entry.exit()呼叫,還是CtSph.entryWithPriority方法中呼叫的e.exit(count, args)方法,最終都是在CtEntry.exitForContext方法中執行,程式碼如下:

protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException {
    if (context != null) {
        // Null context should exit without clean-up.
        if (context instanceof NullContext) {
            return;
        }

        if (context.getCurEntry() != this) {
            String curEntryNameInContext = context.getCurEntry() == null ? null
                : context.getCurEntry().getResourceWrapper().getName();
            // Clean previous call stack.
            CtEntry e = (CtEntry) context.getCurEntry();
            while (e != null) {
                e.exit(count, args);
                e = (CtEntry) e.parent;
            }
            String errorMessage = String.format("The order of entry exit can't be paired with the order of entry"
                    + ", current entry in context: <%s>, but expected: <%s>", curEntryNameInContext,
                resourceWrapper.getName());
            throw new ErrorEntryFreeException(errorMessage);
        } else {
            // Go through the onExit hook of all slots.
            if (chain != null) {
                chain.exit(context, resourceWrapper, count, args);
            }
            // Go through the existing terminate handlers (associated to this invocation).
            callExitHandlersAndCleanUp(context);

            // Restore the call stack.
            context.setCurEntry(parent);
            if (parent != null) {
                ((CtEntry) parent).child = null;
            }
            if (parent == null) {
                // Default context (auto entered) will be exited automatically.
                if (ContextUtil.isDefaultContext(context)) {
                    ContextUtil.exit();
                }
            }
            // Clean the reference of context in current entry to avoid duplicate exit.
            clearEntryContext();
        }
    }
}

邏輯比較簡單,執行chain.exit,清空context等。

相關文章