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;
}
該方法做了如下幾件事:
-
首先嚐試從當前執行緒獲取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<>();
-
判斷當前執行緒上下文是否超出了閾值,也就是下面語句:
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"); } }
當上面判斷為真時,那麼就不再進行規則檢查。
-
當從當前執行緒獲取的Context為空時,建立新的Context。**(這裡在下面再詳細解讀。) **
-
獲取當前資源對應的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方法,所以唯一確定一個資源的就是資源名。
-
執行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
前面代表的是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;
}
這個方法做的事情如下:
-
從contextHolder中獲取Context,如果有了那麼就直接返回。
-
沒有Context資訊,那麼準備開始建立該上下文資訊,準備工作:從contextNameNodeMap中獲取對應節點,contextNameNodeMap定義如下:
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
這個map中是上下文名和節點的對應關係,而上下文名即是資源名。
-
如果獲取到了這個節點,那麼直接建立一個Context並設定到contextHolder中,然後直接返回。
-
當上面節點不存在,那麼先建立該節點,邏輯如下:
-
先檢查當前上下文數是否超過指定閾值,如果超過了那麼返回NullContext,本次請求不做規則檢查。
-
沒有超過指定閾值,那麼加鎖,進行雙重檢查。
-
使用當前資源建立節點,將建立的節點關聯到根節點下,然後存入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等。