前言
在完成了流量统计和限流逻辑两部分的分析之后,降级其实也就非常简单了,所以我的注意力也回到了Sentinel入口调用部分,这一部分构建了Sentinel的上下文,创建了调用链并且在NodeSelectorSlot和ClusterBuilderSlot两个单元中构建了整个Sentinel资源调用的上下文和统计数据存储单元,为后面的限流熔断逻辑提供了功能基础,也是我花时间分析最多的地方。这里要感谢逅弈的这篇文章,对我帮助不少。
概念解释
在介绍具体逻辑之前,我们还是先明确几点基本的概念(下列描述有的出自官方代码注释):
- Resource
资源是整个Sentinel最基本的一个概念。可以是一段代码,一个http请求,一个微服务,总而言之,他是Sentinel需要保证的实体。大部分情况下,我们可以使用方法签名,URL或者是服务名称来作为资源的名称。它在Sentinel中的体现是:ResourceWrapper,他有两个子类:- StringResourceWrapper 使用string来标识一个资源
- MethodResouceWrapper 使用一个函数签名来标识一个资源
- Node
节点是用来存储统计数据的基本数据单元,Node本身只是一个接口,它有多个实现:- StatisticNode 唯一的直接实现类,实现了流量统计的基本方法,在StatisticSlot中使用(具体参见前文)
- ClusterNode 继承自StatisticNode,对于某一个资源的全局统计
- DefaultNode 继承自StatisticNode, 对于某一个资源在相应上下文中的实现,保存了一个指向ClusterNode的引用。另外还保存了子节点列表,当在同一个context下多次调用SphU.entry时会创建子节点
- EntranceNode 继承自DefaultNode,代表一个调用的根节点,一个Context会对应到一个EntranceNode
- Context
上下文是用来保存当前调用的元数据,它包含了几个信息:- EntranceNode 整个调用树的根节点,即入口
- Entry 当前的调用点
- Node 关联到当前调用点的统计信息
- Origin 通常用来标识调用方,这在我们需要按照调用方来区分流控策略的时候会非常有用
每当我们调用SphU.entry() 或者 SphO.entry()获取访问资源许可的时候都需要当前线程处在某个context中,如果我们没有显式调用ContextUtil.enter(),默认会使用Default context。
如果我们在一个上下文中多次调用SphU.entry()来获取多个资源,一个调用树就会被创建出来
-
Entry
每次SphU.entry()调用都会返回一个Entry,Entry保持了所有关于当前资源调用的信息:- createTime 这个资源调用的创建时间
- currentNode SphU.entry请求进入的资源在当前上下文中的统计数据Node
- originNode SphU.entry请求进入的资源对于特定origin调用方的统计数据node
Entry的实现类为CtEntry,它其中除了上述信息之外,还保存了额外的信息:
- parent 调用树链条中上一个entry
- child 调用树链条中的下一个entry
- chain 当前调用资源所使用的限流工作责任链,包括各个Slot
- context 当前调用点所属的上下文
上图大致说明了这几个概念本身的继承关系以及各自的包含关系。接下来我们通过代码来解释一下。
代码解析
Context初体验
我们按照demo,首先要看的是ContextUtil.enter这个函数调用
static public Context enter(String name, String origin) {
// 判断context不能用与默认上下文重名
if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
throw new ContextNameDefineException(
"The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
}
return trueEnter(name, origin);
}
/**
* 调用创建上下文
* @param name 上下文名称
* @param origin 调用方
* @return
*/
protected static Context trueEnter(String name, String origin) {
// 查看threadLocal中有没有context
Context context = contextHolder.get();
// 如果没有
if (context == null) {
// 用local变量来访问violatile,避免并发访问空指针隐患的同时提升效率
// 讲究
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
// 查看是否有context name对应的entranceNode
DefaultNode node = localCacheNameMap.get(name);
// 如果没有entranceNode 则需要创建
if (node == null) {
//超最大数量
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
try {
//锁定
LOCK.lock();
//double check 重新检查一次
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);
// Add entrance node.
// context之间没有层级结构,只有root -> entrance
Constants.ROOT.addChild(node);
// 重新构建context entranceNode Map
Map<String, DefaultNode> newMap = new HashMap<String, DefaultNode>(
contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
// 根据 entranceNode创建context
// 这里有个特性需要关注: entranceNode是线程共享的,
// 而context是线程独有的,所以对于同一个name的上下文
// 可能多个线程有多个context实例,虽然entranceNode是只有一个
context = new Context(node, name);
// 设置origin, 并设置到线程上下文中
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
上面代码有注释解释,也比较简单,需要注意的是最后注释中提到的,Context是线程独有的,对于同一个名称的上下文,entranceNode只会有一个,但是Context可能有多个线程使用的多个示例,另外,对Context本身的操作不需要考虑线程协同,因为是线程独有的。
SphU走起
在创建了一个上下文之后,我们看SphU.entry又在做什么
/**
* Checking all {@link Rule}s about the resource.
*
* @param name the unique name of the protected resource
* @throws BlockException if the block criteria is met, eg. when any rule's threshold is exceeded.
*/
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
这里的Env.sph使用的是一个标准实现CtSph:
@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);
}
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
// 获取线程上下文中的context
Context context = ContextUtil.getContext();
//如果是NullContext 说明已经超了内存阈值无法创建新的context
// 直接返回一个ctEntry并且chain为null,表示不做任何限流熔断操作直接放过
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);
}
// 如果为null 说明当前线程上下文中没有context,使用default context
if (context == null) {
// Using default context.
context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
}
// Global switch is close, no rule checking will do.
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
// 获取资源所对应的chain
// 这里代码就不展开了,需要注意的是,同一个resource
// 共享同一个处理Slot链条,并使用这个链条来完成限流熔断逻辑 切记
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);
}
// 创建一个CtEntry
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 开始链条处理
chain.entry(context, resourceWrapper, null, count, 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;
}
这里代码也基本比较简单,需要注意的是同一个资源会使用同一条Slot Chain,也就是说,如果同一个资源在不同的Context下都有调用,它们使用的也会是同一个处理链条。
另外还有一个需要注意的是,创建CtEntry的地方:
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
super(resourceWrapper);
this.chain = chain;
this.context = context;
setUpEntryFor(context);
}
/**
* 整理当前上下文中的调用链路关系
* @param context
*/
private void setUpEntryFor(Context context) {
// The entry should not be associated to NullContext.
if (context instanceof NullContext) {
return;
}
// 获取当前的entry 并且赋给 parent,即表示当前entry的上游资源调用
this.parent = context.getCurEntry();
if (parent != null) {
// 如果当前parent有值,说明在此之前有SphU.entry调用并且没有exit
// 则把自己赋给parent的儿子,完成调用链条
((CtEntry)parent).child = this;
}
//将线程context的当前资源调用指向自己
context.setCurEntry(this);
}
上面这段代码比较简单,但是可能大家没有一个比较直观的感觉,我这里也稍加说明一下:
:
ContextUtil.enter("context-test", "");
Entry ea = SphU.entry("resouceA");
Entry eb = SphU.entry("resouceB");
eb.exit();
ea.exit();
当执行到
时,context的内容为:ContextUtil.enter("context-test", "");
当执行到
时,context内容为:Entry ea = SphU.entry("resouceA");
当执行到
时,context内容为:Entry eb = SphU.entry("resouceB");
这就构建了一个调用关系,即我们在上下文context-test中,先获取了resourceA的权限,再获取了resourceB的权限,这两个资源在调用关系上存在一个先后关系。当我们在后面调用exit的时候,也是先退curEntry指向的entry,并且把curEntry指向parent,后面再退他的parent,代码这里就不过多扩展了。接下来我们就来到了Slot链
NodeSelectorSlot
作为责任链的第一个Slot,我们先来看看NodeSelectorSlot的entry
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args)
throws Throwable {
// 按照上下文名称获取统计上下文对应的DefaultNode
// 因为之前说过同一个resource会共享同一个chain
// 所以这个map中的所有元素都属于同一个resource
// 只是按照context name来区分了不同的上下文环境
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
// double check
node = map.get(context.getName());
if (node == null) {
// 创建一个DefaultNode
node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
}
// Build invocation tree
// 这里就很妙了 构建了一个调用树
((DefaultNode)context.getLastNode()).addChild(node);
}
}
// 将这个node放入到context 的curNode
// 实际上是context.curEntry.setCurNode(node)
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, args);
}
上面代码也比较简单,注释也写的比较细了,但是有一个地方很神奇:
((DefaultNode)context.getLastNode()).addChild(node);
这里是在做甚哪?
我们跟进去看看
//context.getLastNode()
public Node getLastNode() {
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
//CtEntry.getLastNode()
@Override
public Node getLastNode() {
return parent == null ? null : parent.getCurNode();
}
这两段代码看起来很简单,但是绕的弯也比较多,我们还是通过图形来分析,依然是之前那个例子:
ContextUtil.enter("context-test", "");
Entry ea = SphU.entry("resouceA");
Entry eb = SphU.entry("resouceB");
eb.exit();
ea.exit();
当执行到
Entry ea = SphU.entry("resouceA");
时, 这里执行的结果为:
当执行到
Entry eb = SphU.entry("resouceB");
时, 这里执行的结果为:
根据两张图我们可以知道,通过这个slot,我们构建了一个完整的调用树。
看完这个Slot之后,我们有个疑问,我们针对一个资源在某个context创建了统计的node,那如果我们要针对context无关来做统计呢?这就是我们要看的下一个Slot的职责了。
ClusterBuiilderSlot
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args)
throws Throwable {
// 因为slot chain 是对于某个resource特定的
// 因此这个slot中的private 变量clusterNode也是对于某个resource全局共享的
if (clusterNode == null) {
synchronized (lock) {
// double check 创建clusterNode 无需多言
if (clusterNode == null) {
// Create the cluster node.
clusterNode = Env.nodeBuilder.buildClusterNode();
HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(16);
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);
clusterNodeMap = newMap;
}
}
}
// 设置clusterNode 到context相关的defaultNode中
node.setClusterNode(clusterNode);
/*
* if context origin is set, we should get or create a new {@link Node} of
* the specific origin.
*/
if (!"".equals(context.getOrigin())) {
// 如果origin调用方不为空,则创建一个对应的统计Node
// PS origin和context并没有交叉。是平行的统计空间
// 这里也是double check创建,无需展开
Node originNode = node.getClusterNode().getOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
// 调用下一步
fireEntry(context, resourceWrapper, node, count, args);
}
这里代码就比较简单了,其实就是创建了一个对于resource全局共享的ClusterNode,并完成了信息的绑定。
后面的slot就是我在系列文章最开始所讲的StatisticNode,这里就不在叙述了。
结语
这篇文章作为Sentinel解析系列的最后一篇文章,回到了一切开始的地方,对于入口的组织进行了分析。可能文章写得有点复杂,这里也总结几个关键点给大家:
- entranceNode作为上下文的入口,每个context name对应全局一个entranceNode
- context是线程相关的,就算context name一样,但是不同线程的context是不同的(虽然他们关联的entranceNode是一样)
- defaultNode对于同一个resource的不同context name有不同的统计实例,但非线程相关
- clusterNode是一个resource的全局统计
- 同一个resource全局共享同一个slot chain
希望大家喜欢