Sentinel中有很多类型的Node,例如DefaultNode、StatisticNode、ClusterNode、还有个EntranceNode总共四种类型的Node,第一次看的时候非常懵逼,Node是啥?四个Node有什么不同?
上篇文章中,我们看到StatisticSlot中使用了Node去统计了请求信息,那么Node应该就是做请求统计用的,看下Node接口里定义
public interface Node {
long totalRequest();
long totalSuccess();
long blockRequest();
long totalException();
long passQps();
long blockQps();
long totalQps();
long successQps();
long maxSuccessQps();
long exceptionQps();
long avgRt();
long minRt();
int curThreadNum();
long previousBlockQps();
long previousPassQps();
Map metrics();
void addPassRequest(int count);
void addRtAndSuccess(long rt, int success);
void increaseBlockQps(int count);
void increaseExceptionQps(int count);
void increaseThreadNum();
void decreaseThreadNum();
void reset();
void debug();
}
方法比较多,但是方法名字很清晰,那么可以得出结论:
- 内部实现可以不说,但是对外部来说Node是做为一个请求数据统计和获取的载体
另外再看下四个Node的关系:
那么这四个Node中,哪个去实现了Node接口的方法呢? 通过代码看到是StatisticNode实现了Node接口的所有方法,也就是说,另外三个Node有两种可能的作用:
- DefaultNode、ClusterNode、EntranceNode继承于StatisticNode,基于其提供的数据统计和获取方法,实现自身一些特殊逻辑
- StatisticNode提供了默认的方法,DefaultNode、ClusterNode、DntranceNode有自身的计算逻辑,需要重写这些方法
接下来通过具体代码分析,来看下到底是哪种情况。
EntranceNode
首先在上一篇文章中,我们从入口开始分析了整个调用链路流程,最先遇到和Node相关的代码的时候,是在ContextUtil#trueEnter
中
//#com.alibaba.csp.sentinel.Constants
public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
Env.nodeBuilder.buildClusterNode());
//ContextUtil#trueEnter
protected static Context trueEnter(String name, String origin) {
Context context = contextHolder.get();
if (context == null) {
Map localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
//....
} else {
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node.
Constants.ROOT.addChild(node);
Map newMap = new HashMap(
contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
//....
}
return context;
}
这里可以看到,一个ContextName会对应一个EntranceNode,EntranceNode从字面意思来看,叫做入口节点,也就是说,一个上下文开始的时候,会创建一个EntranceNode与其对应,代表该上下文的入口。
另外看到EntranceNode会挂在ROOT节点下面,而ROOT又是一个EntranceNode节点,而其是全局唯一的,他代表应用的入口节点,如下
ROOT是一个EntranceNode类型的节点,他可以挂子节点,子节点为上下文节点,那么上下文节点下面挂的是啥?别急,我们继续分析
接下看下类的定义
public class EntranceNode extends DefaultNode {
public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) {
super(id, clusterNode);
}
@Override
public long avgRt() {
long total = 0;
long totalQps = 0;
for (Node node : getChildList()) {
total += node.avgRt() * node.passQps();
totalQps += node.passQps();
}
return total / (totalQps == 0 ? 1 : totalQps);
}
@Override
public long blockQps() {
long blockQps = 0;
for (Node node : getChildList()) {
blockQps += node.blockQps();
}
return blockQps;
}
@Override
public long blockRequest() {
long r = 0;
for (Node node : getChildList()) {
r += node.blockRequest();
}
return r;
}
@Override
public int curThreadNum() {
int r = 0;
for (Node node : getChildList()) {
r += node.curThreadNum();
}
return r;
}
@Override
public long totalQps() {
long r = 0;
for (Node node : getChildList()) {
r += node.totalQps();
}
return r;
}
@Override
public long successQps() {
long r = 0;
for (Node node : getChildList()) {
r += node.successQps();
}
return r;
}
@Override
public long passQps() {
long r = 0;
for (Node node : getChildList()) {
r += node.passQps();
}
return r;
}
@Override
public long totalRequest() {
long r = 0;
for (Node node : getChildList()) {
r += node.totalRequest();
}
return r;
}
}
可以看到EntranceNode重写了获取数据统计的方法,获取的时候将所有子节点的数据全累加后返回
DefaultNode
第二次遇到与Node相关的应该是调用链中的NodeSelectorSlot,在看代码之前,先看下类上的注释,其中注释里画两个图:
machine-root
/ \
/ \
EntranceNode1 EntranceNode2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
| |
+- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);
EntranceNode1和EntranceNode2表示两个不同的上下文,而上下文节点下分别挂了个DefaultNode
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
HashMap cacheMap = new HashMap(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
}
((DefaultNode)context.getLastNode()).addChild(node);
}
}
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
// com.alibaba.csp.sentinel.node.DefaultNodeBuilder#buildTreeNode
public DefaultNode buildTreeNode(ResourceWrapper id, ClusterNode clusterNode) {
return new DefaultNode(id, clusterNode);
}
这里与EntranceNode的创建有点类似,都是以ContextName为key去保存的,也就是说DefaultNode也是和上下文相关的节点。
当创建成功后,会调用DefaultNode的addChild方法将创建的DefaultNode挂在某个节点下,我们看下getLastNode返回的是什么
/**
* Current processing entry.
*/
private Entry curEntry;
public Node getLastNode() {
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
curEntry是什么?在上篇文章分析到的entryWithPriority
方法中,有如下代码
Entry e = new CtEntry(resourceWrapper, chain, context);// 1
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);
}
注意看标记1这个位置,这里创建了一个Entry,并将创建好的上下文对象作为参数传进去,看下其构造方法
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot
- 1~3:获取当前上下文的curEntry,如果不为空,则表示当前上下文中,在创建Entry之前就已经有一个Entry被创建过了,那么需要设置父子关系
- 4:这个地方就是我们要找的curEntry初始化的地方
setUpEntryFor方法可能有点难理解,什么情况下parent不为空,首先要知道的是只有我们调用SphU#entry
方法的时候才会创建一个Entry,即Entry代表一个当前调用的一个标志,假设我们在一次上下文中调用了多次,也是可行的,那么这时候会创建多个Entry,如以下代码:
ContextUtil.enter("contextName1");
Entry entry1 = null;
try {
entry1 = SphU.entry("resourceName1");
System.out.println("run method 1");
Entry entry2 = null;
try {
entry2 = SphU.entry("resourceName2");
System.out.println("run method 1");
}finally {
if (entry2 != null) {
entry2.exit();
}
}
} finally {
if (entry1 != null) {
entry1.exit();
}
}
这种情况下,entry2的parent就是entry1,而entry1的parent为空
分析完curEntry的获取后 ,再回到getLastNode方法,当curEntry不为空,还需要再判断一下curEntry.getLastNode
是否为空,看下其实现
//com.alibaba.csp.sentinel.CtEntry#getLastNode
public Node getLastNode() {
return parent == null ? null : parent.getCurNode();
}
如果当前Entry有parent,则返回其parent对应的节点,如果parent为空,则返回Context对应的EntranceNode
那么在上面的栗子中,entry1进入到NodeSelectorSlot#entry
方法的时候,由于parent为空,所以curEntry != null && curEntry.getLastNode() != null
这行代码为false,Context#getLastNode
方法返回EntranceNode
,这样说可能不好理解,现在以上面的代码为例,一步步分析Entry和Node所构成的结构
当代码刚进入Entry的时候,此时的结构如下:
- EntranceNode1在
ContextUtil#trueEnter
中被创建且和上下文Context绑定 - curEntry在
entryWithPriority
方法中初始化且和上下文Context绑定,curEntry也即使代码中的entry1
这时候代码执行到((DefaultNode)context.getLastNode()).addChild
,由于curEntry
即entry1没有parent,所以context.getLastNode()
返回的是EntranceNode1,将创建的DefaultNode挂在其下面,此时结构如下:
代码继续走到context.setCurNode(node);
即curEntry.setCurNode(node)
,执行完毕后,此时结构如下:
然后entry1继续往后执行,执行完毕后,entry2有执行NodeSelectorSlot#entry
方法,此时结构如下:
- curEntry的变化在entryWithPriority方法中处理
代码继续走到((DefaultNode)context.getLastNode()).addChild
,这时候由于curEntry的parent不为空,那么就会去到DefaultNode1,将创建的DefaultNode挂在其下面,执行完毕后,此时结构如下:
代码继续走到context.setCurNode(node)
,执行完毕后,此时结构如下:
好了,到此为止,了解整个节点链路的构建过程,可以发现,对于一个资源,在同个上下文中,多次调用entry,会创建多个DefaultNode节点,这些节点依次挂在上下文的入口节点EntranceNode下面,而每个节点会负责当前上下文中调用entry后一个代码块的的请求数据的统计,记住,DefaultNode是与上下文相关的,假设是不同上下文,那么会呈现之前发过的结构
* machine-root
* / \
* / \
* EntranceNode1 EntranceNode2
* / \
* / \
* DefaultNode(nodeA) DefaultNode(nodeA)
* | |
* +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA)
最下面两个DefaultNode是在不同上下文中调用entry所产生的结构(每个上下文只调用一次entry)
ClusterNode
终于轮到最后一个Node了,在NodeSelectorSlot
之后,还有个ClusterBuilderSlot
,其中有ClusterNode
的处理
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args)
throws Throwable {
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
// Create the cluster node.
clusterNode = Env.nodeBuilder.buildClusterNode();
HashMap newMap = new HashMap(Math.max(clusterNodeMap.size(), 16));
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);
clusterNodeMap = newMap;
}
}
}
node.setClusterNode(clusterNode);
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
可以看到这里也有个Map结构,但是,key是资源,而不是像以前那样是上下文,所以这里就已经清楚了,ClusterNode和资源绑定,即使是不同上下文,同一个资源,应该都是只有一个ClusterNode,由其进行流量统计
另外,当创建完成后还会调用node.setClusterNode(clusterNode);
将ClusterNode
与DefaultNode进行关联,即不同的DefaultNode
都关联了一个ClusterNode
,这样我们在不同上下文中都可以拿到当前资源一个总的流量统计情况
接着再看下DefaultNode重写的方法,以其中两个为例
@Override
public void increaseBlockQps(int count) {
super.increaseBlockQps(count);
this.clusterNode.increaseBlockQps(count);
}
@Override
public void increaseExceptionQps(int count) {
super.increaseExceptionQps(count);
this.clusterNode.increaseExceptionQps(count);
}
统计方法中,除了调用父类(即StatisticNode
)来统计本身的一个流量外,还会再调用ClusterNode
的相应方法统计整个资源的一个流量
OriginNode
OriginNode
整个东西其实在代码中没有对应的类,只不过是概念上的,其本身还是StatisticNode
,这个东西又是什么呢,在ClusterBuilderSlot中有以下代码:
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
假设origin属性不为空,从通过origin去获取一个Node节点,然后放到Context中,getOrCreateOriginNode
方法内部逻辑比较简单,就是通过origin去获取一个StatisticNode
,他是与origin属性绑定的,那么origin是什么呢?
在使用ContextUtil创建上下文的时候,其实是可以传入origin参数的,这个就是上面的origin,他代表的是请求来源,例如我有三个dubbo服务,分别是A和B、C,调用关系为A->C,B->C那么C在限流的时候,可以将A和B作为origin传入,那么ClusterBuilderSlot
就会为其创建对应节点,用来统计AB服务对B服务调用的一个总体情况
总结
- StatisticNode实现了Node接口,封装了基础的流量统计和获取方法
- EntranceNode代表入口节点,每个上下文都会有一个入口节点,用来统计当前上下文的总体流量情况
- DefaultNode代表同个资源在不同上下文中各自的流量情况
- ClusterNode代表同个资源在不同上下文中总体的流量情况
- OriginNode是一个StatisticNode类型的节点,代表了同个资源请求来源的流量情况
为什么需要这样设计?这和Sentinel后续的限流降级等规则的设计有关,后续会继续分析