前言:
前面我们对 Sentinel 有了一个比较系统的认知,本篇我们正式开始分析 Sentinel 的源码,我们知道 Sentinel 使用了责任链模式,根据 Sentinel 官方提供的流程图,我们知道 NodeSelectorSlot 是整个链路的入口,下面我们开始分析 Sentinel 源码。
Sentinel 系列文章传送门:
Spring Cloud 整合 Nacos、Sentinel、OpenFigen 实战【微服务熔断降级实战】
Sentinel 源码分析入门【Entry、Chain、Context】
NodeSelectorSlot#entry 方法源码解析
NodeSelectorSlot#entry 方法会根据资源名称获取 DefaultNode 链路节点,DefaultNode 用于统计调用链路上某个资源的数据,维持树状结构,然后执行下一个 Slot,也就是 ClusterBuilderSlot,详细拆分的话 NodeSelectorSlot 做了一下几件事:
- 为当前资源创建 DefaultNode。
- 将 DefaultNode 放入缓存中,key 是 contextName,这样不同链路入口的请求,将会创建多个 DefaultNode,相同链路则只有一个DefaultNode。
- 将当前资源的 DefaultNode 设置为上一个资源的 ChildNode,构造树。
- 将当前资源的 DefaultNode 设置为 Context 中的 curNode,也就是当前节点。
//com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot#entry
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
//获取当前资源的 DefaultNode
DefaultNode node = map.get(context.getName());
//DefaultNode 为空判断
if (node == null) {
//node 为空
//synchronized 加锁
synchronized (this) {
//再次获取 node
node = map.get(context.getName());
//再次为空判断
if (node == null) {
//创建一个 DefaultNode
node = new DefaultNode(resourceWrapper, null);
//创建 map 加入缓存
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);
}
}
}
//设置当前 node
context.setCurNode(node);
//执行下一个 slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
AbstractLinkedProcessorSlot#fireEntry 方法源码解析
AbstractLinkedProcessorSlot#fireEntry 是一个通用方法,每个 Slot 执行完毕后都会调用这个方法,去判断是否有下一个 Slot。
//com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot#fireEntry
@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
//next 为空判断
if (next != null) {
//不为空 继续
next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}
@SuppressWarnings("unchecked")
//com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot#transformEntry
void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args)
throws Throwable {
T t = (T)o;
entry(context, resourceWrapper, t, count, prioritized, args);
}
ClusterBuilderSlot#entry 方法源码分析
ClusterBuilderSlot#entry 的逻辑比较简单,用于构建资源的 ClusterNode 以及调用来源节点,ClusterNode 保持某个资源运行统计信息(响应时间、QPS、block 数目、线程数、异常数等)以及调用来源统计信息列表,接下来就是 StatisticSlot。
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args)
throws Throwable {
//clusterNode 为空判断
if (clusterNode == null) {
//synchronized 锁
synchronized (lock) {
//clusterNode 为空判断 double check
if (clusterNode == null) {
// Create the cluster node.
//创建 clusterNode
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;
}
}
}
//设置 clusterNode
node.setClusterNode(clusterNode);
/*
* if context origin is set, we should get or create a new {@link Node} of
* the specific origin.
*/
//记录请求来源 origin
if (!"".equals(context.getOrigin())) {
//获取 origin
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
//设置 origin
context.getCurEntry().setOriginNode(originNode);
}
//继续下一个 slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
StatisticSlot#entry 方法源码解析
StatisticSlot 是 Sentinel 的核心功能插槽之一,用于统计实时的调用数据,我们发现这个 Slot 不太一样,她是一上来就执行 fireEntry 方法,进入下一个 Slot 的逻辑,这样设计的原因是由它的功能决定的,它的作用是统计实时数据,必须要后面得限流、降级 Slot 执行完,才能够拿到结果数据,如果后续的 Slot 都执行通过了,正常的执行完 fireEntry,这个时候需要对一些 Node 进行指标的增加,例如线程数 、QPS ,如果限流、降级 Slot 没有通过就会抛出异常,这个时候就不会再走 fireEntry 下面的代码,而是走catch里面的代码,针对不同的异常进行了不同的处理,我们按条理梳理如下:
- 执行 fireEntry 方法,执行后面的后面得限流、降级 Slot。
- 如果限流、降级 Slot 执行通过,会区分 Node 节点类型,对通过的进行指标增加。
- 如果限流、降级 Slot 执行没有通过,会抛出异常,代码会直接进入 catch 代码块。
- 如果是优先级等待异常,不会增加指标的失败数量,但会区分 Node 节点类型,对通过进行指标增加。
- 如果是阻塞没有通过异常,会区分 Node 节点类型,对阻塞指标进行增加。
- 如果是 Throwable 异常,会设置错误信息到当前 Entry 中。
//com.alibaba.csp.sentinel.slots.statistic.StatisticSlot#entry
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
try {
// Do some checking.
//放行到下一个 slot 做限流 降级 等规则判断
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// Request passed, add thread count and pass count.
//请求已通过 线程数+1 用做线程隔离
node.increaseThreadNum();
//请求通过 计数器+1 用做限流
node.addPassRequest(count);
//请求来源节点判断
if (context.getCurEntry().getOriginNode() != null) {
// Add count for origin node.
//来源节点不为空 来源节点的 线程数 和 计数器 也+1
context.getCurEntry().getOriginNode().increaseThreadNum();
context.getCurEntry().getOriginNode().addPassRequest(count);
}
//是否是入口资源类型
if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
//如果是入口资源类型 全局线程数 和 计数器 也要+1
Constants.ENTRY_NODE.increaseThreadNum();
Constants.ENTRY_NODE.addPassRequest(count);
}
// Handle pass event with registered entry callback handlers.
//请求通过后回调
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (PriorityWaitException ex) {
//优先级等待异常这里没有增加请求失败的数量
node.increaseThreadNum();
//请求来源节点判断
if (context.getCurEntry().getOriginNode() != null) {
// Add count for origin node.
//来源节点不为空 来源节点的 线程数 +1
context.getCurEntry().getOriginNode().increaseThreadNum();
}
//是否是入口资源类型
if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
//如果是入口资源类型 全局线程数 +1
Constants.ENTRY_NODE.increaseThreadNum();
}
// Handle pass event with registered entry callback handlers.
//请求通过后回调
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (BlockException e) {
// Blocked, set block exception to current entry.
//阻塞 没有通过异常 将异常信息保存在 当前的 entry 中
context.getCurEntry().setBlockError(e);
// Add block count.
//增加阻塞数量
node.increaseBlockQps(count);
//请求来源节点判断
if (context.getCurEntry().getOriginNode() != null) {
//请求来源节点 阻塞数量 +1
context.getCurEntry().getOriginNode().increaseBlockQps(count);
}
//是否是入口资源类型
if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
//如果是入口资源类型 全局阻塞数 +1
Constants.ENTRY_NODE.increaseBlockQps(count);
}
// Handle block event with registered entry callback handlers.
//回调
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onBlocked(e, context, resourceWrapper, node, count, args);
}
throw e;
} catch (Throwable e) {
// Unexpected internal error, set error to current entry.
//错误设置到 当前 Entry
context.getCurEntry().setError(e);
throw e;
}
}
StatisticSlot#entry 方法源码解析
StatisticNode#addPassRequest 方法中,分别对当前链路和当前资源的计数器都进行了加 1 操作。
//com.alibaba.csp.sentinel.node.StatisticNode#addPassRequest
@Override
public void addPassRequest(int count) {
// DefaultNode的计数器,代表当前链路的 计数器
super.addPassRequest(count);
// ClusterNode计数器,代表当前资源的 总计数器
this.clusterNode.addPassRequest(count);
}
至此,统计数据部分的 Slot 已经分析完毕了,下一篇我们将进入规则校验的相关 Slot 了,根据 Sentinel 官方提提供的 wiki 文档,规则校验部分的 Slot 执行顺序依次是:
- ParamFlowSlot:负责热点参数的限流规则。
- SystemSlot:负责系统保护规则。
- AuthoritySlot:负责授权(来源控制)的规则。
- FlowSlot:负责限流的规则。
- DegradeSlot:负责熔断降级的规则。
欢迎提出建议及对错误的地方指出纠正。