前言
Sentinel1.8总共有8个重要的Slot,都是通过SPI机制加载的。
本章学习前三个ProcessorSlot:
- NodeSelectorSlot:构建资源(Resource)的路径(DefaultNode),用树的结构存储。
- ClusterBuilderSlot:构建ClusterNode,用于记录资源维度的统计信息。
- StatisticSlot:使用Node记录指标信息,如RT、Pass/Block Count,为后续规则校验提供数据支撑。
这三个Slot为规则校验提供数据支撑。
1、Node
在阅读NodeSelectorSlot和ClusterBuilderSlot源码之前,再深入学习一下Node。
Node的作用主要是为FlowRule流控规则,提供数据支撑。
回顾第一章内容,在Sentinel中,Node的实现类有四个,StatisticNode负责统计指标数据,无论是DefaultNode还是ClusterNode都继承了StatisticNode。
DefaultNode
DefaultNode,普通链路节点。
统计维度:Context + Resource。
创建时机:多次调用SphU.entry,NodeSelectorSlot执行时创建DefaultNode(如果Context+Resource维度已经有这个Node了,将使用原来的Node实例),并加入Context上下文的Node链表中。
public class DefaultNode extends StatisticNode {
// 关联的资源
private ResourceWrapper id;
// 子节点
private volatile Set<Node> childList = new HashSet<>();
// 关联的ClusterNode
private ClusterNode clusterNode;
}
DefaultNode维护了一个Node集合,用于表示当前节点的子节点,通过这种方式,DefaultNode构成了一颗树。
DefaultNode重写了StaticNode更新统计数据的相关方法,如increaseBlockQps增加被拒绝的QPS时,额外调用了ClusterNode的increaseBlockQps方法。
@Override
public void increaseBlockQps(int count) {
super.increaseBlockQps(count);
this.clusterNode.increaseBlockQps(count);
}
EntranceNode
EntranceNode入口节点,是特殊的DefaultNode链路节点。
统计维度:Context。
创建时机:每个上下文Context,都会有一个EntranceNode与之关联,代表链路的入口。
EntranceNode继承了DefaultNode,是DefaultNode树的树根。
EntranceNode重写了所有统计方法,比如avgRt统计平均响应时间,根据所有子节点的统计数据,计算得到最终的平均响应时间。
public class EntranceNode extends DefaultNode {
public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) {
super(id, clusterNode);
}
@Override
public double avgRt() {
double total = 0;
double totalQps = 0;
for (Node node : getChildList()) {
total += node.avgRt() * node.passQps();
totalQps += node.passQps();
}
return total / (totalQps == 0 ? 1 : totalQps);
}
}
ClusterNode
ClusterNode簇点,统计一个Resource的数据。
统计维度:Resource。
创建时机:多次调用Sph.entry获取不同资源,ClusterBuilderSlot执行时创建ClusterNode。如果这个Resource从来没有被entry调用过,则创建一个ClusterNode,否则沿用Resource对应的ClusterNode。
public class ClusterNode extends StatisticNode {
// 资源名称
private final String name;
// 资源类型
private final int resourceType;
// origin来源 - 统计数据
private Map<String, StatisticNode> originCountMap = new HashMap<>();
}
一个Resource对应一个ClusterNode,ClusterNode还根据Sph.entry时传入的origin不同(origin存储在上下文中),在originCountMap中保存了Resource下不同origin的统计数据StatisticNode。
区别
其实三者的主要区别在于统计维度的不同,由于统计维度不同,创建时机和数量就不同:
- DefaultNode:统计维度是Context + Resource,表示同一个上下文中的同一资源,共享一个DefaultNode实例,在NodeSelectorSlot中创建;
- EntranceNode:统计维度是Context,表示同一个上下文中,共享同一个EntranceNode实例,在Context创建时创建;
- ClusterNode:统计维度是Resource,表示同一个资源共享同一个ClusterNode实例,在ClusterBuilderSlot中创建;
此外,在数据结构上,DefaultNode是一棵树,EntranceNode继承了DefaultNode是树根,这样在每个Context中,EntranceNode+DefaultNode(s)构成了一整颗链路树。
通过这颗树的不同节点,可以得到每个Context+Resource维度下,当前节点的流量指标。
案例
从NodeSelectorSlot的javadoc中,找到了关于三个Node的关系的案例。
案例一
对于下面这段代码:
ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
会生成这样的树:
首先machine-root是一个Java进程中【真正】的树根,EntranceNode是每个Context的树根。
public final class Constants {
public final static String ROOT_ID = "machine-root";
/**
* Global ROOT statistic node that represents the universal parent node.
*/
public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));
}
ContextUtil.enter("entrance1", "appA")执行后,会创建名为entrance1的Context和他对应的EntranceNode;
SphU.entry("nodeA")执行后,NodeSelectorSlot会在EntranceNode下新增一个DefaultNode子节点nodeA;
又由于nodeA这个资源是首次调用entry方法,为了记录Resource维度的流量指标,ClusterBuilderSlot会创建资源nodeA对应的ClusterNode。
案例二
案例二通过开启多个Context,展现Node树的数据结构。
ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
ContextUtil.enter("entrance2", "appA");
nodeA = SphU.entry("nodeA");
if (nodeA != null) {
nodeA.exit();
}
ContextUtil.exit();
通过ContextUtil.enter,可以创建多个Context对应EntranceNode,统计Context维度流量;
通过SphU.entry,可以创建多个DefaultNode,统计Context+Resource维度流量;
同样的Resource,对应同一个ClusterNode实例,统计Resource维度流量。
2、NodeSelectorSlot
NodeSelectorSlot负责构造并存储DefaultNode,DefaultNode负责统计Context+Resource维度流量;
@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
/**
* {@link DefaultNode}s of the same resource in different context.
* 一个Resource 对应一个NodeSelectorSlot实例 即对应一个这个map
* map的key是Context,所以这个map存储的Node是Context+Resource维度的Node
*/
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
}
NodeSelectorSlot内部用一个Map存储context和DefaultNode的映射关系;
又因为,上一章讲到CtSph中存储了Resource与ProcessorSlotChain的映射关系,即一个Resource对应一个ProcessorSlotChain实例,一个ProcessorSlotChain实例只包含一个NodeSelectorSlot实例,所以NodeSelectorSlot里的Map是针对一个Resource的;
// CtSph.java
/**
* Same resource({@link ResourceWrapper#equals(Object)}) will share the same
* {@link ProcessorSlotChain}, no matter in which {@link Context}.
*/
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap = new HashMap<ResourceWrapper, ProcessorSlotChain>();
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// 1. 同一个name的资源,会使用同一个ProcessorSlotChain
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
//...
}
return chain;
}
综上NodeSelectorSlot实例里的map维护了Context+Resource维度对应的流量DefaultNode。
NodeSelectorSlot的entry方法主要分为两步:
- 获取Resource+Context维度的DefaultNode,如果不存在则创建
- 更新当前Context中的调用树
// NodeSelectorSlot.java
/**
* {@link DefaultNode}s of the same resource in different context.
* 一个Resource 对应一个NodeSelectorSlot实例 即对应一个这个map
* map的key是Context,所以这个map存储的Node是Context+Resource维度的Node
*/
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) {
// 1. 创建DefaultNode,存储到当前NodeSelectorSlot实例
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;
// 2. 将这个DefaultNode加入调用树
((DefaultNode) context.getLastNode()).addChild(node);
}
}
}
// 3. 设置当前节点
context.setCurNode(node);
// 4. 执行后面的Slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
值得注意的是,如果Context+Resource维度已经存在了对应DefaultNode,调用树的结构不会更新,只会设置当前节点为对应的DefaultNode。
比如:A.entry->B.entry->B.exit->A.exit->B.entry->B.exit
public static void m1() {
Entry nodeA = SphU.entry("nodeA");
Entry nodeB = SphU.entry("nodeB");
nodeB.exit();
nodeA.exit();
}
public static void m2() {
Entry nodeB = SphU.entry("nodeB");
nodeB.exit();
}
public static void main(String[] args) {
m1();
m2();
}
最后的结果还是树结构。
如果要更精细地使用FlowRule流控规则,可以在入口先创建自定义上下文,设置FlowRule#strategy=STRATEGY_CHAIN(链路),这样可以对Resource+Context维度做流控,具体见后面FlowSlot的分析。
3、ClusterBuilderSlot
ClusterBuilderSlot构建ClusterNode,用于记录Resource维度的统计信息。
@Spi(isSingleton = false, order = Constants.ORDER_CLUSTER_BUILDER_SLOT)
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
// 保存全局 资源 - 资源统计数据
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();
private static final Object lock = new Object();
// ClusterBuilderSlot每个资源一个实例,这里保存当前资源对应ClusterNode
private volatile ClusterNode clusterNode = null;
}
ClusterBuilderSlot维护了一个Map,保存所有Resource对应的ClusterNode;
另外,还是由于同一Resource对应一个ClusterBuilderSlot实例,clusterNode保存了当前资源对应的流量信息ClusterNode。
ClusterBuilderSlot的entry方法主要分为三步:
- 获取Resource对应ClusterNode,如果不存在则创建;
- 保存ClusterNode到当前上下文当前处理节点DefaultNode(NodeSelectorSlot传入)中;
- 如果当前上下文中设置了来源系统origin,则创建origin + Resource维度ClusterNode(一个单纯的StatisticNode,由对应ClusterNode维护) ,存入当前ClusterNode,设置origin ClusterNode到上下文中继续向后传递;
// ClusterBuilderSlot.java
// 保存全局 资源 - 资源统计数据
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();
private static final Object lock = new Object();
// ClusterBuilderSlot每个资源一个实例,这里保存当前资源对应ClusterNode
private volatile ClusterNode clusterNode = null;
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args)
throws Throwable {
// 1. 构造Resource对应ClusterNode
if (clusterNode == null) {
synchronized (lock) {
if (clusterNode == null) {
clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
newMap.putAll(clusterNodeMap);
newMap.put(node.getId(), clusterNode);
clusterNodeMap = newMap;
}
}
}
// 2. 保存ClusterNode到当前上下文正在处理的DefaultNode
node.setClusterNode(clusterNode);
// 3. 设置origin ClusterNode
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
// 4. 执行下一个slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
4、StatisticSlot
StatisticSlot负责记录指标信息,如RT、Pass/Block Count,为后续规则校验提供数据支撑。
StatisticSlot是全局单例,entry方法分为两步:
- 先执行后续的规则校验Slot(fireEntry);
- 如果校验通过,统计threadNum/passRequest;如果发生PriorityWaitException异常,只统计threadNum;如果发生BlockException,统计blockQps;
@Spi(order = Constants.ORDER_STATISTIC_SLOT)
public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
try {
// 1. 先执行后续Slot的entry
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// 2. 统计数据ThreadNum++ passRequest++
// 2-1. DefaultNode Resource+Context维度 ClusterNode Resource 维度
node.increaseThreadNum();
node.addPassRequest(count);
// 2-2. origin ClusterNode origin+Resource维度
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseThreadNum();
context.getCurEntry().getOriginNode().addPassRequest(count);
}
// 2-3 全局入口流量统计
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseThreadNum();
Constants.ENTRY_NODE.addPassRequest(count);
}
// 3. 给用户的扩展点,可以通过SPI加载
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (PriorityWaitException ex) {
// 这是流控规则才会抛出的异常
node.increaseThreadNum();
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseThreadNum();
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseThreadNum();
}
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (BlockException e) {
// 1. 设置BlockError到上下文中
context.getCurEntry().setBlockError(e);
// 2. 统计数据 BlockQps++
// 2-1. DefaultNode Resource+Context维度 ClusterNode Resource 维度
node.increaseBlockQps(count);
// 2-2 origin ClusterNode origin+Resource维度
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseBlockQps(count);
}
// 2-3 全局入口流量统计
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseBlockQps(count);
}
// 3. 给用户的扩展点,可以通过SPI加载
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onBlocked(e, context, resourceWrapper, node, count, args);
}
throw e;
} catch (Throwable e) {
context.getCurEntry().setError(e);
throw e;
}
}
}
无论是规则校验正常还是异常,都会从几个维度来统计流量数据:
- DefaultNode:Resource+Context维度,注意这里的指标统计方法都被重写了,额外操作了ClusterNode;
- ClusterNode:Resource维度;
// DefaultNode.java
@Override
public void increaseBlockQps(int count) {
super.increaseBlockQps(count); // 增加DefaultNode自身blockQps
this.clusterNode.increaseBlockQps(count); // 增加ClusterNode.blockQps
}
- origin ClusterNode:如果上下文的来源系统非空,则记录origin+Resource维度流量;
- 全局入口流量:如果资源属于入口流量,则记录到全局入口流量中,用于系统规则校验;
总结
本章主要学习了三个Slot:
- NodeSelectorSlot,构建Node树,每个Node节点都是一个DefaultNode,记录Context+Resource维度数据;
- ClusterBuilderSlot,针对Resource构建ClusterNode,每个ClusterNode维护Resource维度数据。如果当前上下文设置了来源系统origin,则在当前Resource维度下的ClusterNode内,创建origin+Resource维度的StatisticNode;
- StatisticSlot:利用上述两个Slot构建的各种Node,执行数据统计。先执行后续规则校验Slot,如果校验通过,统计threadNum/passRequest;如果发生PriorityWaitException异常(流控规则,默认流控效果下,priority=true,才会出现,发生该异常会直接放行该请求),只统计threadNum;如果发生BlockException,统计blockQps。