目录
1. 概述
之前分析过Sentinel是基于责任链的模式,其逻辑处理部分是一个有一个的Slot
这里大概的介绍下每种Slot的功能职责:
-
NodeSelectorSlot
负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级; -
ClusterBuilderSlot
则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据; -
StatisticsSlot
则用于记录,统计不同维度的 runtime 信息; -
SystemSlot
则通过系统的状态,例如 load1 等,来控制总的入口流量; -
AuthoritySlot
则根据黑白名单,来做黑白名单控制; -
FlowSlot
则用于根据预设的限流规则,以及前面 slot 统计的状态,来进行限流; -
DegradeSlot
则通过统计信息,以及预设的规则,来做熔断降级;
每个Slot执行完业务逻辑处理后,会调用fireEntry()方法,该方法将会触发下一个节点的entry方法,下一个节点又会调用他的fireEntry,以此类推直到最后一个Slot,由此就形成了sentinel的责任链。
2. 一些需要知道的前提
2.1. Resource
资源,是Sentinel世界中的抽象,任何东西都能被定义成资源,自己提供的服务,调用的服务,甚至一段代码,有了资源才能在资源上定义规则,去进行限流降级之类的操作
在Sentinel中提供了两个默认是Resource分别是StringResourceWrapper和MethodResourceWrapper
2.2. Context
Context是上下文的意思,一个线程对应一个Context
其中有三个属性
- name:名字
- entranceNode:调用链入口
- curEntry:当前entry
- origin:调用者来源
- async:异步
2.3. Entry
每次调用 SphU.entry()
都会生成一个Entry入口,该入口中会保存了以下数据:入口的创建时间,当前入口所关联的节点(Node),当前入口所关联的调用源对应的节点。Entry是一个抽象类,他只有一个实现类,在CtSph中的一个静态类:CtEntry
其中有这些属性
- createtime
- curNode
- originNode
- error
- resourceWrapper
- parent
- child
- chain
- context
红色的是抽象类Entry的,黑色的是CtEntry中的
一个Entry相当于一个token只有正常生成了一个entry才能算pass,不然报异常BlockException肯定是限流了
2.4. Node
节点是用来保存某个资源的各种实时统计信息的,他是一个接口,通过访问节点,就可以获取到对应资源的实时状态,以此为依据进行限流和降级操作。
有几种节点类型
- StatisticNode:统计节点
- DefaultNode:默认节点,NodeSelectorSlot中创建的就是这个节点
- ClusterNode:集群节点
- EntranceNode:该节点表示一棵调用链树的入口节点,通过他可以获取调用链树中所有的子节点
3. 深入分析
3.1. demo启动
Entry entry = null;
try {
entry = SphU.entry("abc");
entry = SphU.entry("abc");
} catch (BlockException e1) {
} finally {
if (entry != null) {
entry.exit();
}
}
CtSph.entryWithPriority
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
//先从ThreadLocal获取,第一次肯定是null
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) {
//生成Context的部分
context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
}
//省略
}
3.2. 创建Context
ContextUtil.trueEnter
protected static Context trueEnter(String name, String origin) {
//从ThreadLocal中获取,第一次肯定是null
Context context = contextHolder.get();
if (context == null) {
//这里是根据Context的名字获取Node
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 {
//创建个EntranceNode
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
//加入全局的节点
Constants.ROOT.addChild(node);
//当如map中
Map<String, DefaultNode> newMap = new HashMap<String, DefaultNode>(
contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
context = new Context(node, name);
context.setOrigin(origin);
//放入ThreadLocal中
contextHolder.set(context);
}
return context;
}
这里的逻辑还是比较简单的
- 首先在ThreadLocal获取,获取不到就创建,不然就返回
- 然后再Map中根据ContextName找一个Node
- 没有找到Node就加锁的方式,创建一个EntranceNode,然后放入Map中
- 创建Context,设置node,name,origin,再放入ThreadLocal中
到此Context就创建完成
目前Context对象的状态如下图
3.3. 创建Entry
新建Entry的过程
Entry e = new CtEntry(resourceWrapper, chain, context);
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;
}
this.parent = context.getCurEntry();
if (parent != null) {
((CtEntry)parent).child = this;
}
context.setCurEntry(this);
}
当第一次Entry生成的时候,context.getCurEntry必定是NULL,那么直接执行Context.setCurEntry方法
然后这个Context的状态如下图
再执行一次新的Sphu.entry后会再次新建一个Entry,这个时候curEntry不是null,那么执行((CtEntry)parent).child = this;
结果如下图
可以看出,原来的CtEntry被移出Context,新建的CtEntry和旧CtEntry通过内部的parent和child引用相连
3.4. 执行NodeSelectorSlot.entry
之前分析过,Sentinel是基于责任链模式,责任链第一个slot是NodeSelectorSlot
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
//这里有个缓存,根据context的名字缓存node
DefaultNode node = map.get(context.getName());
//双重检测,线程安全
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
//这里生成的是DefaultNode节点
node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
//下面这些逻辑是放入map的逻辑,因为后期map比较大,所以这样放入,性能会高一些
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
}
// 关键在这,这是修改调用链树的地方
((DefaultNode)context.getLastNode()).addChild(node);
}
}
//替换context中的curEntry中的curNode
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
// Context.java
public Context setCurNode(Node node) {
this.curEntry.setCurNode(node);
return this;
}
public Node getLastNode() {
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
//CteEntry.java
public Node getLastNode() {
return parent == null ? null : parent.getCurNode();
}
//DefaultNode.java
public void addChild(Node node) {
if (node == null) {
RecordLog.warn("Trying to add null child to node <{0}>, ignored", id.getName());
return;
}
if (!childList.contains(node)) {
synchronized (this) {
if (!childList.contains(node)) {
Set<Node> newSet = new HashSet<Node>(childList.size() + 1);
newSet.addAll(childList);
newSet.add(node);
childList = newSet;
}
}
RecordLog.info("Add child <{0}> to node <{1}>", ((DefaultNode)node).id.getName(), id.getName());
}
}
查询缓存中是否有这个node这里的逻辑也很简单
- 根据ContextName查询缓存是否有这个Node
- 没有就生成这个DefaultNode,放入缓存,然后构造调用树链
- Context的curEntry中的curnode设置为这个node
但是这样有个情况是第二步不是经常出现的,第二步出现的前提是node取不到,而node在缓存中获取不到的条件是contextName不同,除非不同线程才有可能不同contextName,不仅如此,还有非NodeSelectorSlot同对象,那么其中的map是不同的
3.4.1. 相同资源名情况下
假设我们先回到现有一个Entry生成的的情况下
然后执行NodeSelectorSlot中的entry方法
这个时候curEntry是等于CtEntry,但是CtEntry中的parent是null,所以getLastNode还是返回entranceNode
然后再执行下面方法setCurNode
结果如下图
注:这两个Node是相同的Node,是一个对象
然后再执行一次生成Entry和一次NodeSelectorSlot中的entry方法
这一次Context还是相同的Context,因为再一个线程中,那么就不会再生成新的Node,只执行上述过程的第一步和第三步
结果如下图
注:这三个Node都是相同的Node,因为根据ContextName从缓存中获取的
3.4.2. 不同资源名情况下
Entry entry = null;
try {
entry = SphU.entry("abc"); //资源名abc
System.out.println("pass");
entry = SphU.entry("abcd"); //资源名abcd
System.out.println("pass");
} catch (BlockException e1) {
System.out.println("block");
} finally {
if (entry != null) {
entry.exit();
}
}
为什么不同资源名会不同?
本质在于ChainSlot的生成问题
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
//可以看出,chain链根据资源来作为key,不同的资源肯定是不同chain链
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit.
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap;
}
}
}
return chain;
}
然后在NodeSelectorSlot中
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
这是个私有属性,不同资源情况下,默认的map中是没有缓存的
下面开始分析
先回到第一个Entry生成并执行了NodeSelectorSlot的entry方法情况
然后在执行一次Entry生成并执行了NodeSelectorSlot的entry方法情况
结果如下图