前言
Context組件在sentinel中扮演的是一種什麼樣的角色呢?借用原作者的註釋:
This class holds metadata of current invocation
其實就是保存一次資源訪問鏈路元數據的類,鏈路的各個節點都能通過獲取鏈路綁定的context來獲取一些信息進行相應的處理。很多涉及鏈路的框架都會有類似設計,例如netty的ChannelHandlerContext,go語言網絡包中的context等等。下面通過源碼分析sentinel中Context的創建和使用流程。
Context結構
com.alibaba.csp.sentinel.context.Context
public class Context {
/**
* Context name.
*/
private final String name;
/**
* The entrance node of current invocation tree.
*/
private DefaultNode entranceNode;
/**
* Current processing entry.
*/
private Entry curEntry;
/**
* The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP).
*/
private String origin = "";
private final boolean async;
...
Context類主要由五個成員變量組成,分別是
- name,一般是訪問的資源名
- entranceNode,就是記錄當次鏈路流控信號量的node
- curEntry,當前Context所屬的entry
- origin, 資源訪問者信息
- async,是否異步
Context創建
Context通常通過ContextUtil的enter方法創建,我們可以先看下ContextUtil類的源碼
com.alibaba.csp.sentinel.context.ContextUtil
public class ContextUtil {
/**
* Store the context in ThreadLocal for easy access.
*/
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
/**
* Holds all {@link EntranceNode}. Each {@link EntranceNode} is associated with a distinct context name.
*/
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
private static final Context NULL_CONTEXT = new NullContext();
static {
// Cache the entrance node for default context.
initDefaultContext();
}
private static void initDefaultContext() {
String defaultContextName = Constants.CONTEXT_DEFAULT_NAME;
EntranceNode node = new EntranceNode(new StringResourceWrapper(defaultContextName, EntryType.IN), null);
Constants.ROOT.addChild(node);
contextNameNodeMap.put(defaultContextName, node);
}
...
ContextUtil持有了一個線程本地變量和一個靜態的Node Map,通過分析它的trueEnter方法可以知道這兩個變量的作用
com.alibaba.csp.sentinel.context.ContextUtil#trueEnter
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 {
try {
LOCK.lock();
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;
}
線程本地變量是用來存儲調用鏈路所在線程的Context的,也就是說不同線程持有的其實不是一個Context,我在sentinel限流相關指標統計源碼分析最後拋出過一個問題,最後用於流控判斷的Node來自於Context,如果每個線程的Context都不同,這樣的統計還有什麼意義嗎?顯然不會這樣,所以才需要contextNameNodeMap這個屬性。Context在創建時會去contextNameNodeMap中根據自己的name取對應的node,之前說了Context的name就是資源的name,那麼對於相同資源的訪問,即使在不同線程,context中持有的其實都是同一個node,也就是說統計仍然是全局的而不是隨線程分散。
Context的使用
Context是負責在一條鏈路中傳遞信息的,因此自然是在slot中進行傳遞並使用,以entryWithPriority方法爲例
com.alibaba.csp.sentinel.CtSph#entryWithPriority()
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
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被傳入slotChain中並在各個slot中被使用,這裏還是以flowSlot爲例
com.alibaba.csp.sentinel.slots.block.flow.FlowRuleChecker#selectNodeByRequesterAndStrategy
static Node selectNodeByRequesterAndStrategy(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node) {
// The limit app should not be empty.
String limitApp = rule.getLimitApp();
int strategy = rule.getStrategy();
String origin = context.getOrigin();
if (limitApp.equals(origin) && filterOrigin(origin)) {
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// Matches limit origin, return origin statistic node.
return context.getOriginNode();
}
return selectReferenceNode(rule, context, node);
} else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) {
if (strategy == RuleConstant.STRATEGY_DIRECT) {
// Return the cluster node.
return node.getClusterNode();
}
return selectReferenceNode(rule, context, node);
} else if (RuleConstant.LIMIT_APP_OTHER.equals(limitApp)
&& FlowRuleManager.isOtherOrigin(origin, rule.getResource())) {
if (strategy == RuleConstant.STRATEGY_DIRECT) {
return context.getOriginNode();
}
return selectReferenceNode(rule, context, node);
}
return null;
}
根據鏈路狀態會從context中取一些數據