1. 前言
上一篇介绍了Sentinel的Context、Entry、Node相关的信息。在创建Node时,涉及到了NodeSelectorSlot和ClusterBuilderSlot,创建Entry的时候,会创建一个chain。本文会通过源码分析这些对象的作用。
2. 功能插槽
2.1. 功能插槽的作用
当使用SphU.entry()获取资源创建Entry对象的时候,会创建一系列的功能插槽,这些插槽以责任链的方式组合在一起,每个插槽分别处理不同的功能。
在CtSph#entryWithPriority()方法中会尝试去获取chain,如果没有,则创建:
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
具体的获取流程:
源码位置:CtSph.java
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
= new HashMap<ResourceWrapper, ProcessorSlotChain>();
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
//先从缓存中获取,如果没有再创建
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit. 处理链的数量不能超过6000,
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;
}
可以看到,chain会先保存到一个静态的全局map中,以资源名称为key,chain为value,所以同一个resource只有一个chain。
实际创建chain的地方是SlotChainProvider.newSlotChain():
#源码位置:SlotChainProvider.java
private static volatile SlotChainBuilder slotChainBuilder = null;
public static ProcessorSlotChain newSlotChain() {
//如果builder不为空,创建chain,否则先创建builder,再创建chain
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
resolveSlotChainBuilder();
if (slotChainBuilder == null) {
RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
slotChainBuilder = new DefaultSlotChainBuilder();
}
return slotChainBuilder.build();
}
2.2. SlotChainBuilder
SlotChainBuilder负责创建功能插槽链,SlotChainBuilder是一个接口,里面只有一个build方法,定义功能插槽如何创建。
可以看到首先看一下SlotChainBuilder有三个实现,不同的实现增加不同的处理能力。
- DefaultSlotChainBuilder:默认的功能插槽构造器
- GatewaySlotChainBuilder:网关插槽构造器,里面新增了网关限流功能
- HotParamSlotChainBuilder:热点参数插槽构造器,里面新增了热点参数功能
这里看一下默认的功能插槽构造器,其他的两个只是在适当的位置新增一个处理插槽。
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
chain.addLast(new NodeSelectorSlot());
chain.addLast(new ClusterBuilderSlot());
chain.addLast(new LogSlot());
chain.addLast(new StatisticSlot());
chain.addLast(new SystemSlot());
chain.addLast(new AuthoritySlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
}
可以看到build方法主要是创建一个DefaultProcessorSlotChain,然后将不同功能的插槽添加进去。
需要注意的是,Sentinel是通过SPI的形式选择使用哪个SlotChainBuilder来创建功能插槽的。
private static final ServiceLoader<SlotChainBuilder> LOADER = ServiceLoader.load(SlotChainBuilder.class);
2.3. ProcessorSlot
ProcessorSlot代表功能插槽对象,定义了进入功能插槽和退出功能插槽的接口,具体的功能插槽都实现了该接口。
ProcessorSlot继承结构如下:
通过继承可以发现,ProcessorSlot主要是两类,一类是具有特定功能的功能插槽,比如限流、统计、降级等,还有一类是主要是形成插槽链,主要就是DefaultProcessorSlotChain。
2.3.1. DefaultProcessorSlotChain
DefaultProcessorSlotChain代表功能处理链的开始,里面定义了两个节点,第一个节点和最后一个节点,并提供新增节点的方法。
public class DefaultProcessorSlotChain extends ProcessorSlotChain {
//默认先创建一个头结点插槽
AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
throws Throwable {
super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
super.fireExit(context, resourceWrapper, count, args);
}
};
AbstractLinkedProcessorSlot<?> end = first;
@Override
public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
protocolProcessor.setNext(first.getNext());
first.setNext(protocolProcessor);
if (end == first) {
end = protocolProcessor;
}
}
@Override
public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
end.setNext(protocolProcessor);
end = protocolProcessor;
}
}
2.3.2. AbstractLinkedProcessorSlot
AbstractLinkedProcessorSlot非常的重要,不仅实现了ProcessorSlot,还实现功能插槽链之间的如何连接的。
private AbstractLinkedProcessorSlot<?> next = null;
AbstractLinkedProcessorSlot里面通过新增一个next指针标记下一个功能处理槽是哪一个,这样,在一个功能槽处理完流程后,通过next可以找到下一个功能槽,退出的时候也是一样的逻辑。
2.4. 功能插槽小结
上面讲了功能插槽创建的流程,Sentinel是如何将所有的插槽组织工作的。但具体每个功能插槽是怎么工作的,在这个地方先不讲。最后,通过一个图看一下DefaultSlotChainBuilder创建的功能插槽链结构:
3. NodeSelectorSlot
官方文档是这样描述NodeSelectorSlot的:这个 slot 主要负责收集资源的路径,并将这些资源的调用路径以树状结构存储起来,用于根据调用路径进行流量控制。
先看源码:
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
/**
* {@link DefaultNode}s of the same resource in different context.
*/
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
@Override
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 = new DefaultNode(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);
}
}
}
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
fireExit(context, resourceWrapper, count, args);
}
}
NodeSelectorSlot会创建一个DefaultNode,然后将该节点设置到Context下的最新一个节点子节点列表中,然后将上下文的当前节点设置为本次创建的节点。
public Node getLastNode() {
if (curEntry != null && curEntry.getLastNode() != null) {
return curEntry.getLastNode();
} else {
return entranceNode;
}
}
上面是获取最新节点的逻辑。
NodeSelectorSlot主要作用就是收集资源的路径。在这个Slot中,创建了一个DefaultNode,在上一篇中讲到DefaultNode是以Context和Resource为维度保存统计指标的,但是这个地方的map是根据Context来保存的,这个是为什么呢?需要注意的是功能插槽处理链的创建是根据Resource来的,一个Resource只会创建一个chain,不同的Resource创建不同的chain,所以这地方虽然是使用Context作为key,但其实是Context和Resource为维度的。
4. ClusterBuilderSlot
上面的NodeSelector是创建DefaultNode节点,CLusterBuilderSlot主要是负责创建ClusterNode和来源节点。至于这两个节点的作用,在上一篇已经讲过。
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();
private static final Object lock = new Object();
private volatile ClusterNode clusterNode = null;
@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 = new ClusterNode();
HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);
clusterNodeMap = newMap;
}
}
}
node.setClusterNode(clusterNode);
/*
* if context origin is set, we should get or create a new {@link Node} of
* the specific origin.
*/
//如果context定义了来源名称,需要给当前的entry创建一个originNode
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
}
可以看到,ClusterBuilderSlot中有一个ClusterNode类型的变量,因为之前说过,同一个Resource只有一个Chain,所以这个地方创建的ClusterNode是统计某个Resource的数据,所以ClusterNode的统计维度为Resource。
如果定义了入口的来源,这里还会创建一个来源节点,作为统计来源节点相关数据的node,这个地方的统计维度是同一个Resource和orgin为维度。
5. 创建Node、Context、Entry整体流程
上面讲NodeSelectorSlot和ClusterBuilderSlot的时候,只是把大致的流程说一下,至于创建了Node后整个结构是什么样的,这个需要再说一下,如果在看上一篇和这篇中的内容时比较乱,可以看了这一小节后,再去看一下之前的内容。因为到此,整个Sentinel保存统计数据的流程才讲完。
5.1. 再次回忆Context、Entry、Node的创建时机
- Context:在使用ContextUtil.entry()或者SphU.entry()的时候会创建Context。
- EntranceNode:在创建Context的时候创建EntranceNode,并赋值给Context。
- ProcessorSlot::在使用SphU.entry()创建,同一个Resource共享一个。
- Entry:在使用SphU.entry()创建,并通过parent和child将Entry链连接,同时将该Entry对象赋值给Context的curEntry.
- DefaultNode:在NodeSelectorSlot中创建,以Resource和Context为维度
- ClusterNode:在ClusterBuilderSlot中创建,以Resource为维度。
- 来源源节点:在ClusterBuilderSlot中创建,以Resource和orgin为维度。
5.2. 流程图
首先先看一下代码:
ContextUtil.enter("context1","origin1");
Entry entry = SphU.entry("resource1");
当创建一个Entry的时候,整个关系图如下:
这个时候在获取同一个资源一次:
Entry entry2 = SphU.entry("resource1");
关系图如下:
接下来在创建不同资源的:
Entry entry3 = SphU.entry("resource2");
Entry entry4 = SphU.entry("resource3");
关系图如下:
上面是在同一个Context下,如果是在不同的Context下,由于chain是以Resource为维度的,所以ClusterNode对同一个Resource只会存在一个,所以多个Context下,关系图就是把上图复制一份,只是ClusterNode指向同一个。
根据关系图,可以很清晰的看到ClusterNode、DefaultNode、OrginNode是按什么样的维度来进行统计的。
6. 小结
本篇主要讲Sentinel通过责任链的方式将各种插槽组合起来实现限流降级等功能,并分析了前两个Slot:NodeSelectorSlot和ClusterNodeSlot的作用。最后结合第一篇的内容画出不同场景下Context、Entry、Node的关系,更加体现ClusterNode、DefaultNode、源节点是按照什么维度进行统计的。在下一篇中,将继续分析Sentinel中的统计功能StatisticSlot,以及各种Node节点是怎么保存数据的。