前言
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中取一些数据