1. 前言
这篇文章主要分析Sentinel中的Context、Entry、Node的作用和关系。理解Context、Entry、Node的作用和关系,对掌握Sentinel如何实现限流非常的重要。个人认为,将Sentinel的这三个概念和Sentinel统计数据的滑动窗口实现原理理解清楚,Sentinel限流的实现原理也就明白了。
2. Context
2.1. Context的定义
在Sentinel的源码中,是这样描述Context的:
This class holds metadata of current invocation 。。。
更多对Context的描述可以看一下源代码。源码中描述Context持有当前调用链的信息,这些信息主要包括:
- entranceNode:当前调用链的入口节点
- curEntry:当前调用链此刻的是哪一个Entry对象在执行
- name:context的名称
- origin:调用源名称
上面这几个信息直接在Context中定义,可以很好理解。除了这些信息,Context还持有获取和修改当前节点,获取源节点、最新一个节点的能力,具体可以看如下代码:
/**
* 获取当前节点
* @return
*/
public Node getCurNode() {
return curEntry.getCurNode();
}
/**
* 设置当前节点
* @param node
* @return
*/
public Context setCurNode(Node node) {
this.curEntry.setCurNode(node);
return this;
}
/**
* 获取最新节点
* Get the parent {@link Node} of the current.
*
* @return the parent node of the current.
*/
public Node getLastNode() {
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
/**
* 获取当前节点
* @return
*/
public Node getOriginNode() {
return curEntry == null ? null : curEntry.getOriginNode();
}
可以看到Context对当前节点的持有能力其实是通过curEntry来实现的。
2.2. Context的创建
Sentinel创建Context的时机有:
- 显式使用ContextUtil.entry("contextName")
- 使用SphU.entry("resource")创建Entry的时候如果没有context则创建一个
2.2.1. ContextUtil创建Context
使用ContextUtil.entry()创建Context的时候,最终是由trueEntry()方法创建Context的。源码如下:
#源码位置:ContextUtil.java
/**
* 线程中保存上下文的ThreadLocal
*/
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
/**
* 持有所有的EntranceNode,每一个EntranceNode和一个名称不同的上下文关联
*/
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
/**
* 创建一个context
* @param name 资源名称
* @param origin 请求来源
* @return
*/
protected static Context trueEnter(String name, String origin) {
//先从本地线程中获取
Context context = contextHolder.get();
//本地线程中没有
if (context == null) {
//查看缓存中是否存在EntranceNode类型的调用链入口节点
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
//如果没有
if (node == null) {
--- 省略部分代码 ---
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;
--- 省略部分代码 ---
}
//将EntranceNode和origin赋值给context,创建上下文
context = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}
return context;
}
通过源码可以看到,Sentinel会使用Threadlocal保存Context,所以同一个线程中只会有一个context对象,在创建Context的时候会创建一个EntranceNode,表示调用的入口节点。
2.2.2. Sph.entry()创建Context
在创建Entry的时候,如果不存在Context,会调用ContextUtil创建Context,所以实际还是调用ContextUtil.trueEntry()。
#源码位置:CtSph#entryWithPriority()
//获取当前线程中的上下文
Context context = ContextUtil.getContext();
//如果是NullContext,则返回一个没有chain的entry对象,意味着不需要进行slot的检查
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);
}
//如果不存在context,创建一个默认的context
if (context == null) {
// Using default context.
context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
}
2.3. Context的销毁
Context的销毁很简单,当调用ContextUtil.exit()方法时将ThreadLocal中的值设置为null即把Context销毁。
#源码位置:ContextUtil.java
public static void exit() {
Context context = contextHolder.get();
if (context != null && context.getCurEntry() == null) {
contextHolder.set(null);
}
}
2.4 Context小结
通过源码,可以看到Context的创建和销毁很简单。需要特别注意的是:
- Context是使用ThreadLocal来保存的,但并不是代表在同一个线程中不会改变,比如在线程中对Context进行创建-退出-创建-退出,则会一个线程有多个Context,但是同一时间只有一个,当然,一般是不会这么操作的。所以,Context不是代表线程的上下文,而是代表一次调用链的进入和退出这期间的上下文。
- Context创建的时候会创建一个调用链入口节点对象EntranceNode。
- Context中持有当前Entry、当前节点、源节点等信息,不过这些信息是在后面创建Entry或者Node的时候赋值的。
3. Entry
3.1. Entry的定义
Sentinel中Entry可以理解为每次进入资源的一个凭证,如果调用SphO.entry()或者SphU.entry()能获取Entry对象,代表获取了凭证,没有被限流,否则抛出一个BlockException。
Entry中持有本次对资源调用的相关信息:
- createTime:创建该Entry的时间戳。
- curNode:Entry当前是在哪个节点。
- orginNode:Entry的调用源节点。
- resourceWrapper:Entry关联的资源信息。
3.2. CtEntry
上面说的Entry是一个抽象的类,在Sentinel中真正创建的Entry是CtEntry。CtEntry继承了Entry相关的信息,还持有如下信息:
- parent:当前Entry的父Entry
- child:当前Entry的子Entry
- context:当前的Context
- chain:当前的调用链
可以看到,CtEntry中对加强了Entry的能力,持有Context和调用链的信息。在同一个上下文中多次进行资源的调用会生成一个链表,这个链表就是通过parent和child实现的。
3.3. Entry的创建
当调用SphU.entry()或者SphO.entry()时,如果能够获取到资源,则返回一个Entry对象,否则抛出异常。调用entry()方法创建Entry的时候,最后会进入entryWithPriority()方法创建对象。
#源码位置:CtSph#entryWithPriority()
//获取插槽处理链
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.
*/
//如果不存在插槽处理链,则创建一个没有chain的entry对象,意味着不需要进行slot的检查
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
//创建一个entry
Entry e = new CtEntry(resourceWrapper, chain, context);
正常的流程会创建一个带有chain的Entry,chain代表的是插槽处理链,具体的创建后续再说。现在重点看一下CtEntry的创建。
#源码位子:CtEntry.java
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
super(resourceWrapper);
this.chain = chain;
this.context = context;
setUpEntryFor(context);
}
private void setUpEntryFor(Context context) {
// The entry should not be associated to NullContext.
if (context instanceof NullContext) {
return;
}
#产生entry链
this.parent = context.getCurEntry();
if (parent != null) {
((CtEntry)parent).child = this;
}
#更新context的当curEntry
context.setCurEntry(this);
}
可以看到,在创建CtEntry的时候,会做三件事,一个就是为当前的CtEntry指定相关的属性,比如chain、context,二是如果在同一个Context中多次调用进入资源,会生产entry链,通过parent和child维护。三是将当期进入的entry记录在Context中。
3.4. Entry的销毁
当需要退出当前的entry调用时,使用entry.exit()即可以将当期的entry销毁,源码如下:
#源码位置:CtEntry.java
/**
* 退出
* @param context
* @param count
* @param args
* @throws ErrorEntryFreeException
*/
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) {
//如果退出的不是当前的entry对象的场景
//获取当前entry对应的资源名
String curEntryNameInContext = context.getCurEntry() == null ? null : context.getCurEntry().getResourceWrapper().getName();
// Clean previous call stack.
CtEntry e = (CtEntry)context.getCurEntry();
//从Context的curEntry开始退出,并抛出异常,退出的时候应该从curEntry开始
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 {
//退出所有的slot
if (chain != null) {
chain.exit(context, resourceWrapper, count, args);
}
// Restore the call stack.
//将Context的curEntry设置为parent,并将parent的child设置为null
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.
//清除对Context的引用
clearEntryContext();
}
}
}
可以看到,销毁entry的时候,如果调用exit()的顺序和进入的顺序不一致,会取context中的curEntry开始进行退出,然后抛出异常提示。
3.5. Entry小结
到此,Entry基本创建的流程完毕,需要注意的是:
- Entry可以理解为对资源的获取和释放的一个凭证,记录获取到这个资源的相关信息。
- 在用一个Context中多次进入获取资源会形成调用链,Entry通过parent和child属性实现。
- 在创建Entry的时候会将当前的Entry对象赋值给Context的curEntry。
- 在释放Entry的时候,一定要按照调用链从最后一个Entry开始释放。
4. Node
4.1. Node的定义
Sentinel中是这样描述Node的:
Holds real-time statistics for resources.
Node中保存了对资源的实时数据的统计,Sentinel中的限流或者降级等功能就是通过Node中的数据进行判断的。Node是一个接口,里面定义了各种操作request、exception、rt、qps、thread的方法。
4.2. 各种类型的Node
先看一下Node继承结构图
4.2.1. StatisticNode
StatisticNode实现了Node接口,从图中可以看出,其他的Node都继承了该接口。所以这个Node是一个最基础的Node,没有特性。StatisticNode中使用秒级和分钟级两个滑动窗口、一个代表线程数的原子变量记录资源的各种指标,除此没有其他特殊的地方。
#源码位置:StatisticNode.java
/**
* 持有秒级别的统计数据指标,默认一秒,样本数2
* Holds statistics of the recent {@code INTERVAL} seconds. The {@code INTERVAL} is divided into time spans
* by given {@code sampleCount}.
*/
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
/**
* 持有分钟级别的数据指标,单位一分钟,样本数量为60,即每秒一个样本
* Holds statistics of the recent 60 seconds. The windowLengthInMs is deliberately set to 1000 milliseconds,
* meaning each bucket per second, in this way we can get accurate statistics of each second.
*/
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
/**
* 线程总数
* The counter for thread count.
*/
private LongAdder curThreadNum = new LongAdder();
StatisticNode实现了Node中相关的接口,并且使用三个数据结构保存相关的数据 ,为其他Node实现特殊的统计维度提供基础实现。
在Sentinel中单独使用StatisticNode来作为统计节点的地方是统计来源节点相关的指标的时候,来源节点创建则是在ClusterBuilderSlot中。具体内容后面在讲整个调用链创建的时候会说到,这里只是简单提一下。
4.2.2. DefaultNode
Sentinel源码中是这样描述DefaultNode的:
* <p>
* A {@link Node} used to hold statistics for specific resource name in the specific context.
* Each distinct resource in each distinct {@link Context} will corresponding to a {@link DefaultNode}.
* </p>
* <p>
* This class may have a list of sub {@link DefaultNode}s. Child nodes will be created when
* calling {@link SphU}#entry() or {@link SphO}@entry() multiple times in the same {@link Context}.
* </p>
从源码描述中可以看出,DefaultNode持有指定的Context和指定的Resource的统计数据,意味着DefaultNode是以Context和Resource为维度的统计节点。这一点非常的重要,因为如果想对某个Context中某个Resource的进行限流等操作,就需要使用DefaultNode类型的节点来统计数据。源码中还描述到,多次在同一个上下文中获取资源,会产生一些子节点,这些子节点保存在DefaultNode的childList中。
/**
* 子节点列表
* The list of all child nodes.
*/
private volatile Set<Node> childList = new HashSet<>();
这些子节点有什么用?接下来讲EntranceNode的时候会讲到这个问题。 除了增加一个childList属性,DefaultNode还持有一个ClusterNode节点,至于有什么作用,在马上讲到的ClusterNode中再说。
DefaultNode会在NodeSelectorSlot中创建,并且赋值给当前Context的curEntry的curNode。具体会在讲NodeSelectorSlot的时候再细说。
4.2.3. ClusterNode
Sentinel中是这样描述ClusterNode的:
* <p>
* This class stores summary runtime statistics of the resource, including rt, thread count, qps
* and so on. Same resource shares the same {@link ClusterNode} globally, no matter in which
* {@link com.alibaba.csp.sentinel.context.Context}.
* </p>
* <p>
* To distinguish invocation from different origin (declared in
* {@link ContextUtil#enter(String name, String origin)}),
* one {@link ClusterNode} holds an {@link #originCountMap}, this map holds {@link StatisticNode}
* of different origin. Use {@link #getOrCreateOriginNode(String)} to get {@link Node} of the specific
* origin.<br/>
从描述中可以知道,ClusterNode保存的是同一个Resource的相关的统计信息,是以Resource为维度的,不区分Context,这个是和DefaultNode的区别。
为了区分从不同来源的访问,在ClusterNode中使用StatisticNode保存不同来源的统计信息。这就是在讲StatisticNode的时候说到StatisticNode被直接创建使用的地方。
/**
* <p>The origin map holds the pair: (origin, originNode) for one specific resource.</p>
* <p>
* The longer the application runs, the more stable this mapping will become.
* So we didn''t use concurrent map here, but a lock, as this lock only happens
* at the very beginning while concurrent map will hold the lock all the time.
* </p>
*/
private Map<String, StatisticNode> originCountMap = new HashMap<String, StatisticNode>();
/**
* <p>Get {@link Node} of the specific origin. Usually the origin is the Service Consumer's app name.</p>
* <p>If the origin node for given origin is absent, then a new {@link StatisticNode}
* for the origin will be created and returned.</p>
*
* @param origin The caller's name, which is designated in the {@code parameter} parameter
* {@link ContextUtil#enter(String name, String origin)}.
* @return the {@link Node} of the specific origin
*/
public Node getOrCreateOriginNode(String origin) {
StatisticNode statisticNode = originCountMap.get(origin);
if (statisticNode == null) {
try {
lock.lock();
statisticNode = originCountMap.get(origin);
if (statisticNode == null) {
// The node is absent, create a new node for the origin.
statisticNode = new StatisticNode();
HashMap<String, StatisticNode> newMap = new HashMap<>(originCountMap.size() + 1);
newMap.putAll(originCountMap);
newMap.put(origin, statisticNode);
originCountMap = newMap;
}
} finally {
lock.unlock();
}
}
return statisticNode;
}
上面就是保存和创建源节点统计信息相关的逻辑。
在上一小节讲DefaultNode的时候,DefaultNode中会持有一个ClusterNode,可以看到DefaultNode中对各种指标进行修改的时候,都会去修改ClusterNode相应的指标,这是因为DefaultNode统计的时候区分了Context,但ClusterNode不区分,是针对整个Resource的,那么只要Resource有新的请求进入,都会被ClusterNode记录。
ClusterNode是在ClusterBuilderSlot处理的时候创建并赋值给DefaultNode节点的,具体会在讲ClusterBuilderSlot作用的时候再细说。
4.2.4. EntranceNode
Sentinel源码中是这样描述EntranceNode的:
/**
* <p>
* A {@link Node} represents the entrance of the invocation tree.
* </p>
* <p>
* One {@link Context} will related to a {@link EntranceNode},
* which represents the entrance of the invocation tree. New {@link EntranceNode} will be created if
* current context does't have one. Note that same context name will share same {@link EntranceNode}
* globally.
* </p>
EntranceNode代表调用链的入口节点,持有某个Context中调用的信息,同一个Context共享一个EntranceNode。EntranceNode的统计维度为Context。需要注意的是EntranceNode继承了DefaultNode。
在前面讲Context创建的时候讲到会去创建一个EntranceNode,这正是创建EntranceNode的时机。
在讲DefaultNode的时候,我们讲到DefaultNode中有一个子节点列表childList,在同一上下文中多次获取资源的时候,会将所有的节点保存到里面。所以childList会将Context中不管是不是同一个Resource维度的Node都保存到里面,也就是childList中的是同一个Context中的Node,这样就可以统计Context维度的数据了。看EntranceNode中统计各种指标都是根据childList来统计的。
4.3. Node小结
Node代表Sentinel中各种指标的实时统计信息,根据不同的维度有不同的指标,并且创建的时机也不一样,具体如下表:
节点类型 | 统计维度 | 创建时机 |
---|---|---|
StatisticNode | 基本的统计节点,没有维度之分 | 没有 |
DefaultNode | Context*Resource | NodeSelectorSlot |
ClusterNode | Resource | ClusterBuilderSlot |
EntranceNode | Context | ContextUtil中创建Context的时候 |
来源节点(StatisticNode类型) | Resource*Origin | ClusterBuilderSlot |
5. 总结
本篇主要是分析Context、Entry、Node相关的概念和作用,创建的时机,他们之间的关系。Sentinel是怎么记录多个维度的数据统计的。将这三个放在一起分析,主要是它们之间存在各种关联,并且都可以理解为存储数据的地方。
在本篇中讲到的chain链,还有NodeSelectorSlot、ClusterBuilderSlot,将会在下一篇中进行分析。在下一篇中,主要讲Sentinel整个Slot插槽的处理流程,以及NodeSelectorSlot、ClusterBuilderSlot的作用,并将Context、Entry、Node在多个Context和多个的Resource等组合下怎么生成调用链用画图的方式展示出来,进一步理解三者的关联关系,以及为什么不同的节点能够统计不同维度的数据。