![图片[1]-资源指标统计实现全解析(下篇):StatisticSlot核心原理 - 速优课-速优课](http://www.suyouke.com/wp-content/uploads/2026/08/doubao_img_2304x1728_20260803_094414-1024x768.png)
资源指标统计实现全解析(下篇):StatisticSlot核心原理
前言
上一篇我们分析了NodeSelectorSlot和ClusterBuilderSlot,它们负责构建各种Node,为统计做好准备。
今天这篇,我们来分析统计链路的最后一个、也是最核心的一个Slot——StatisticSlot。
StatisticSlot是真正负责记录各项指标数据的Slot。它和NodeSelectorSlot、ClusterBuilderSlot一起,组成了Sentinel的”指标统计流水线”。
一、StatisticSlot的特殊地位
先看一下统计流水线的分工:
![图片[2]-资源指标统计实现全解析(下篇):StatisticSlot核心原理 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/image-19-1024x316.png)
- NodeSelectorSlot:创建DefaultNode,向下传递
- ClusterBuilderSlot:给DefaultNode加上ClusterNode,向下传递
- StatisticSlot:根据后续Slot的执行结果,记录各项指标数据
为什么StatisticSlot这样设计?
注意StatisticSlot的entry方法有个特殊之处:它先调用fireEntry让后面的Slot执行,然后根据执行结果来统计数据。
这也是为什么Sentinel的责任链要用”每个Slot自己调用fireEntry”的方式,而不是用for循环遍历——因为每个Slot都可以决定:
- 是先做自己的事,再让后面的执行
- 还是先让后面的执行完,再做自己的事
StatisticSlot就是后者——它需要知道请求是被放行还是被拒绝了,才能决定记录什么数据。
StatisticSlot的整体框架
先看StatisticSlot的整体结构:
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 {
// 先调用后面的Slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
// 请求通过,记录通过数据
} catch (PriorityWaitException ex) {
// 优先级等待异常,特殊处理
} catch (BlockException e) {
// 请求被拒绝,记录拒绝数据
throw e;
} catch (Throwable e) {
// 其他异常,记录异常数据
throw e;
}
}
@Override
public void exit(Context context, ResourceWrapper resourceWrapper,
int count, Object... args) {
DefaultNode node = (DefaultNode)context.getCurNode();
// 请求处理完成,记录成功和耗时
fireExit(context, resourceWrapper, count);
}
}
简单总结:
- entry阶段:先放行,根据结果记录(通过/拒绝/异常),同时线程数+1
- exit阶段:记录成功和耗时,线程数-1
下面我们分情况详细分析。
二、entry方法的四种情况
entry方法里有四个分支,对应四种不同的情况。我们一个个来看。
2.1 情况一:请求正常通过
后续的Slot没有抛出任何异常,说明请求通过了所有检查,可以放行。
需要做这些事情:
// 1. DefaultNode:线程数+1,通过数+1
node.increaseThreadNum();
node.addPassRequest(count);
// 2. OriginNode:如果有调用来源,来源节点也记录
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseThreadNum();
context.getCurEntry().getOriginNode().addPassRequest(count);
}
// 3. ENTRY_NODE:如果是流入流量,全局入口节点也记录
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseThreadNum();
Constants.ENTRY_NODE.addPassRequest(count);
}
// 4. 回调:通知所有注册的通过回调
for (ProcessorSlotEntryCallback<DefaultNode> handler :
StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
三个层级的Node都要更新:
- DefaultNode:当前Context下这个资源的统计
- OriginNode:按调用来源的统计(如果有的话)
- ENTRY_NODE:全局入口节点的统计(只有IN类型才更新)
为什么要更新这么多Node?因为不同的功能需要不同维度的统计数据。比如按来源限流需要OriginNode的数据,系统自适应限流需要ENTRY_NODE的数据。
回调机制
Sentinel还提供了回调机制——你可以注册ProcessorSlotEntryCallback,在请求通过或被拒绝时收到通知。
public interface ProcessorSlotEntryCallback<T> {
// 请求通过时回调
void onPass(Context context, ResourceWrapper resourceWrapper, T param,
int count, Object... args) throws Exception;
// 请求被拒绝时回调
void onBlocked(BlockException ex, Context context, ResourceWrapper resourceWrapper,
T param, int count, Object... args);
}
通过StatisticSlotCallbackRegistry.addEntryCallback()注册。这是一个扩展点,可以用来做日志、告警等。
2.2 情况二:捕获PriorityWaitException
这是一个比较特殊的情况。
PriorityWaitException是什么?只有在使用优先级限流时才会出现。简单说就是:请求没被直接拒绝,而是先等一会儿再通过。
这种情况下,请求最终还是会通过的,所以:
- 不需要记录通过数(因为已经在别的地方记了?或者说这是预通过)
- 只需要把线程数+1
node.increaseThreadNum();
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseThreadNum();
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseThreadNum();
}
// 回调onPass,因为最终还是通过了
for (ProcessorSlotEntryCallback<DefaultNode> handler :
StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
具体的优先级限流逻辑,我们在分析FlowSlot的时候再详细讲。
2.3 情况三:捕获BlockException
BlockException是Sentinel的”拒绝异常”——只要抛出这个异常,就说明请求被限流或熔断了。
捕获到BlockException后,需要做这些事:
// 1. 把异常存到Entry里,exit方法会用到
context.getCurEntry().setError(e);
// 2. DefaultNode:记录拒绝数
node.increaseBlockQps(count);
// 3. OriginNode:来源节点也记录拒绝数
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseBlockQps(count);
}
// 4. ENTRY_NODE:全局入口也记录
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseBlockQps(count);
}
// 5. 回调onBlocked
for (ProcessorSlotEntryCallback<DefaultNode> handler :
StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onBlocked(e, context, resourceWrapper, node, count, args);
}
// 6. 重新抛出异常!
throw e;
注意最后一步:StatisticSlot只是”偷偷”记录一下,然后把异常重新抛出去。
为什么要重新抛出?因为异常需要一路抛到最外层,让调用方知道请求被拒绝了,才能执行降级逻辑。
这里有个细节:异常先存到了CtEntry里。为什么?因为exit方法需要知道请求是成功还是失败了,才能决定要不要记录成功和耗时。
2.4 情况四:捕获其他异常
除了BlockException和PriorityWaitException,其他异常都算”系统异常”。
注意:这里说的不是业务异常——业务代码还没执行呢,业务异常是通过Tracer.trace()记录的。这里的异常是指Sentinel内部Slot抛出的非BlockException。
处理方式:
// 1. 存到Entry里
context.getCurEntry().setError(e);
// 2. 记录异常数
node.increaseExceptionQps(count);
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().increaseExceptionQps(count);
}
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.increaseExceptionQps(count);
}
// 3. 重新抛出
throw e;
和BlockException类似,记录完异常数就抛出去。
三、exit方法:记录成功与耗时
讲完了entry,再来看exit方法。
exit方法在什么时候调用?在业务代码执行完之后,finally块里调用entry.exit()的时候。
exit方法需要知道:请求是成功完成了,还是中途被拒绝了?
答案就在entry方法里——如果被拒绝了,异常会存在CtEntry.error字段里。exit方法检查这个字段就知道了。
@Override
public void exit(Context context, ResourceWrapper resourceWrapper,
int count, Object... args) {
DefaultNode node = (DefaultNode)context.getCurNode();
// 没有异常 = 请求正常完成
if (context.getCurEntry().getError() == null) {
// 计算耗时:当前时间 - Entry创建时间
long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
// 1. DefaultNode:记录耗时和成功数
node.addRtAndSuccess(rt, count);
// 2. OriginNode:来源节点也记录
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().addRtAndSuccess(rt, count);
}
// 3. DefaultNode:线程数-1
node.decreaseThreadNum();
// 4. OriginNode:线程数-1
if (context.getCurEntry().getOriginNode() != null) {
context.getCurEntry().getOriginNode().decreaseThreadNum();
}
// 5. ENTRY_NODE:流入流量的话也记录
if (resourceWrapper.getEntryType() == EntryType.IN) {
Constants.ENTRY_NODE.addRtAndSuccess(rt, count);
Constants.ENTRY_NODE.decreaseThreadNum();
}
}
// 退出回调
Collection<ProcessorSlotExitCallback> exitCallbacks =
StatisticSlotCallbackRegistry.getExitCallbacks();
for (ProcessorSlotExitCallback handler : exitCallbacks) {
handler.onExit(context, resourceWrapper, count, args);
}
fireExit(context, resourceWrapper, count);
}
核心逻辑:
- 如果没出错:计算耗时,记录成功数和耗时,线程数-1
- 如果出错了:啥也不做(因为entry阶段已经记录过了)
- 最后:调用exit回调,继续向后传递
耗时是怎么计算的?
很简单:当前时间 – Entry的创建时间。
long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
Entry是什么时候创建的?在CtSph.entryWithPriority里,调用chain.entry之前创建的。所以这个耗时包含了:
- 所有Slot的entry方法执行时间
- 业务代码执行时间
- (不包含exit方法的执行时间)
对于大多数场景,这个精度足够了。
四、指标数据的完整传递链路
StatisticSlot调用的是DefaultNode的方法,那ClusterNode的数据是怎么更新的?
答案是:DefaultNode会自动同步给ClusterNode。
看DefaultNode的代码:
public class DefaultNode extends StatisticNode {
private ClusterNode clusterNode;
@Override
public void addPassRequest(int count) {
// 先更新自己的
super.addPassRequest(count);
// 再同步给ClusterNode
this.clusterNode.addPassRequest(count);
}
}
不止addPassRequest,increaseBlockQps、increaseExceptionQps、addRtAndSuccess、increaseThreadNum、decreaseThreadNum……所有这些方法,DefaultNode都会同时更新自己和ClusterNode。
这样设计的好处是:StatisticSlot只需要操作DefaultNode就行,不用关心ClusterNode的存在。数据同步由DefaultNode内部完成。
最终落到滑动窗口
不管是DefaultNode还是ClusterNode,最终统计数据都存在StatisticNode的滑动窗口里。
StatisticNode里有两个滑动窗口:
- 秒级(rollingCounterInSecond):用于实时统计
- 分钟级(rollingCounterInMinute):用于历史数据
以addRtAndSuccess为例:
@Override
public void addRtAndSuccess(long rt, int successCount) {
// 秒级滑动窗口
rollingCounterInSecond.addSuccess(successCount);
rollingCounterInSecond.addRT(rt);
// 分钟级滑动窗口
rollingCounterInMinute.addSuccess(successCount);
rollingCounterInMinute.addRT(rt);
}
两个窗口都要更新。
最终数据存在MetricBucket的LongAdder数组里,就是我们之前讲滑动窗口时说的那个结构。
![图片[3]-资源指标统计实现全解析(下篇):StatisticSlot核心原理 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/image-20-1024x375.png)
Sentinel收集哪些指标?
在MetricEvent枚举里定义了所有指标:
public enum MetricEvent {
PASS, // 通过数
BLOCK, // 拒绝数
EXCEPTION, // 异常数
SUCCESS, // 成功数
RT, // 总耗时
OCCUPIED_PASS // 预通过数
}
| 指标 | 含义 |
|---|---|
| PASS | 请求被放行的总数 |
| BLOCK | 请求被拒绝的总数 |
| EXCEPTION | 请求处理异常的总数 |
| SUCCESS | 请求处理成功的总数 |
| RT | 成功请求的总耗时 |
| OCCUPIED_PASS | 预通过总数(抢占下一个窗口的配额) |
其他指标都可以通过这些基础指标计算出来,比如:
- 平均耗时 = RT / SUCCESS
- 异常率 = EXCEPTION / (SUCCESS + EXCEPTION)
- 等等
五、整体总结:数据是怎么流动的
最后,我们用一张完整的图来梳理一下,从请求进入到退出,数据是怎么流动的:
请求进入
│
▼
entry()
├─→ NodeSelectorSlot:创建DefaultNode,构建调用树
├─→ ClusterBuilderSlot:创建ClusterNode,关联DefaultNode
└─→ StatisticSlot
├─→ 先调用fireEntry() → 经过AuthoritySlot/SystemSlot/FlowSlot/DegradeSlot
│ ├─→ 通过 → 记录pass + thread+1
│ ├─→ 被拒绝 → 记录block + 存异常 + 抛出
│ └─→ 其他异常 → 记录exception + 存异常 + 抛出
│
┌────┘
│
▼
执行业务代码
│
├─→ 业务异常 → Tracer.trace() → 记录exception
│
▼
exit()
└─→ StatisticSlot
├─→ 成功 → 计算RT → 记录success + RT + thread-1
└─→ 失败 → 啥也不做
关键数据结构再回顾
| 结构 | 维度 | 作用 |
|---|---|---|
| DefaultNode | 资源 + Context | 当前调用链下的资源统计 |
| ClusterNode | 资源(全局) | 资源的全局统计 |
| OriginNode | 资源 + 来源 | 按调用来源的统计 |
| ENTRY_NODE | 全局入口 | 系统整体的统计 |
| StatisticNode | – | 基础统计实现(滑动窗口) |
数量关系
- 1个线程 → 1个Context(ThreadLocal)
- 1个Context名称 → 1个EntranceNode
- 1个资源 + N个Context名称 → N个DefaultNode
- 1个资源 → 1个ProcessorSlotChain → 1个ClusterNode
- 1个资源 + M个来源 → M个OriginNode
总结与思考
这两篇文章,我们把Sentinel的指标统计实现完整地过了一遍。
从NodeSelectorSlot构建DefaultNode,到ClusterBuilderSlot添加ClusterNode,再到StatisticSlot记录各项指标——三个Slot各司其职,组成了完整的统计流水线。
几个核心要点再强调一下:
- StatisticSlot的设计很巧妙:先放行再统计,通过异常类型判断结果
- 数据是多维度的:DefaultNode、ClusterNode、OriginNode、ENTRY_NODE,满足不同场景需求
- DefaultNode自动同步ClusterNode:StatisticSlot只操作DefaultNode,同步逻辑封装在DefaultNode里
- 最终都落到滑动窗口:所有指标数据最终存在MetricBucket的LongAdder数组里
- 回调机制提供扩展点:可以注册回调监听通过/拒绝事件
思考一下:
- 为什么线程数的统计不放在滑动窗口里?(因为线程数是”当前值”,不是”窗口累计值”,用LongAdder直接存当前值就行)
- 为什么exit方法不记录BlockException的情况?(因为entry阶段已经记录过了,而且被拒绝的请求根本不会执行到业务代码,也就不会走到exit的成功分支)
- OCCUPIED_PASS(预通过)是什么场景下用的?(匀速排队模式下,当前窗口的请求可以”预占”下一个窗口的配额,下一篇讲FlowSlot的时候会详细说)
搞懂了指标统计,我们就有了”数据基础”。下一篇,我们正式进入功能Slot的分析——先从最核心的FlowSlot(限流降级)开始。










请登录后查看评论内容