![图片[1]-Sentinel 集群限流深度解析(下):源码实现与核心算法 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/cover-2117.png)
本文导读
在上一篇文章中,我们介绍了 Sentinel 集群限流的架构设计、部署模式和配置方式。本文作为集群限流系列的下篇,将深入源码层面,剖析集群限流的实现细节。
我们将从以下几个维度展开:
- 核心接口与类结构:TokenService、ClusterTokenClient、ClusterTokenServer
- 客户端实现原理:如何发起 Token 请求,如何处理响应
- 服务端实现原理:如何分配 Token,优先级请求如何处理
- 指标数据统计:集群限流的滑动窗口实现
- 常见问题与注意事项:namespace、阈值计算、回退机制
通过本文的学习,你将彻底理解 Sentinel 集群限流的工作原理,遇到问题时能够快速定位和排查。
一、核心接口与类结构
1.1 设计思想
理解集群限流,我们可以结合令牌桶算法来思考:
- 服务端:负责”生产令牌”和统一管理令牌总数
- 客户端:向服务端”申请令牌”,申请到了才能放行请求
- 申请失败:要么等待,要么被拒绝,要么回退到本地限流
集群限流也支持热点参数限流,实现原理大致相同。本文重点分析普通的集群限流,热点参数的集群限流留给大家自行研究。
1.2 核心接口定义
sentinel-core 模块的 cluster 包下定义了集群限流的核心接口:
| 接口 | 作用 |
| TokenService | 定义申请 Token 的接口,由 FlowRuleChecker 调用 |
| ClusterTokenClient | 客户端接口,继承 TokenService,增加启动/停止方法 |
| ClusterTokenServer | 服务端接口,定义启动/停止方法 |
| EmbeddedClusterTokenServer | 嵌入模式接口,同时继承 TokenService 和 ClusterTokenServer |
TokenService 接口:
TokenService 是最核心的接口,定义了申请令牌的方法:
public interface TokenService {
// 申请普通令牌
TokenResult requestToken(Long ruleId, int acquireCount, boolean prioritized);
// 申请热点参数令牌
TokenResult requestParamToken(Long ruleId, int acquireCount, Collection<Object> params);
}
方法参数说明:
| 参数 | 说明 |
| ruleId | 集群限流规则的全局唯一 ID |
| acquireCount | 申请的令牌数量 |
| prioritized | 请求是否具有优先级 |
| params | 热点参数值(参数限流专用) |
TokenResult 响应实体:
服务端的响应结果封装在 TokenResult 中:
public class TokenResult {
private Integer status; // 响应状态码
private int remaining; // 剩余令牌数
private int waitInMs; // 等待时间(毫秒)
private Map<String, String> attachments; // 附加属性
}
各字段含义:
| 字段 | 说明 |
| status | 响应状态码(见下表) |
| remaining | 当前时间窗口剩余的令牌数 |
| waitInMs | 需要休眠等待的时间,用于优先级请求预占下一个窗口 |
| attachments | 附加属性,暂未使用 |
TokenResultStatus 状态码:
| 状态 | 含义 |
| OK | 申请成功,放行 |
| SHOULD_WAIT | 需要等待一段时间后放行 |
| BLOCKED | 申请失败,拒绝请求 |
| NO_RULE_EXISTS | 规则不存在 |
| BAD_REQUEST | 请求参数错误 |
| FAIL | 服务端处理失败 |
| TOO_MANY_REQUEST | 服务端请求过多,限流 |
ClusterTokenClient 接口:
客户端接口,继承 TokenService,增加了启动和停止方法:
public interface ClusterTokenClient extends TokenService {
void start() throws Exception; // 启动客户端
void stop() throws Exception; // 停止客户端
}
客户端负责维护与服务端的连接,并实现向远程服务端申请令牌的逻辑。
ClusterTokenServer 接口:
服务端接口,定义启动和停止方法:
public interface ClusterTokenServer {
void start() throws Exception; // 启动服务端
void stop() throws Exception; // 停止服务端
}
启动后开始监听端口,接收和处理客户端的请求。
EmbeddedClusterTokenServer 接口:
嵌入模式的服务端接口,同时继承了 ClusterTokenServer 和 TokenService:
public interface EmbeddedClusterTokenServer
extends ClusterTokenServer, TokenService {
}
为什么要同时继承两个接口?
在嵌入模式下,如果当前节点本身就是服务端,那就没必要再发起网络请求了,直接在本地调用 TokenService 的方法即可。这就是 EmbeddedClusterTokenServer 存在的意义。
1.3 类关系图
接口和实现类的关系如下图所示:
![图片[2]-Sentinel 集群限流深度解析(下):源码实现与核心算法 - 速优课-速优课](https://www.suyouke.com/wp-content/uploads/2026/08/a3a9f1c0-f5b9-11ea-a625-2d171281165b.jpg)
各实现类说明:
| 实现类 | 所属模块 | 说明 |
| DefaultClusterTokenClient | sentinel-cluster-client-default | 客户端默认实现,基于 Netty |
| DefaultTokenService | sentinel-cluster-server-default | 服务端 Token 分配逻辑 |
| DefaultEmbeddedTokenServer | sentinel-cluster-server-default | 嵌入模式服务端实现 |
服务端的具体实现通过 Java SPI 机制加载,因此可以灵活替换。
二、客户端实现原理
2.1 入口:FlowRuleChecker#passClusterCheck
我们从单机限流的流程继续往下看,集群限流的入口在 FlowRuleChecker#passClusterCheck 方法:
private static boolean passClusterCheck(FlowRule rule, Context context, DefaultNode node,
int acquireCount, boolean prioritized) {
try {
// (1) 获取 TokenService
TokenService clusterService = pickClusterService();
if (clusterService == null) {
return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
}
// (2) 获取规则 ID
long flowId = rule.getClusterConfig().getFlowId();
// (3) 申请令牌
TokenResult result = clusterService.requestToken(flowId, acquireCount, prioritized);
// (4) 处理响应结果
return applyTokenResult(result, rule, context, node, acquireCount, prioritized);
} catch (Throwable ex) {
RecordLog.warn("[FlowRuleChecker] Request cluster token unexpected failed", ex);
}
// (5) 异常时回退
return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
}
整体四步走:
- 获取 TokenService:根据当前节点角色获取对应的实现
- 获取规则 ID:从集群限流配置中获取全局唯一的 flowId
- 申请令牌:调用
requestToken方法向服务端申请 - 处理结果:根据响应结果判断是否放行
- 异常回退:发生异常时根据配置决定是否回退到本地限流
2.2 pickClusterService:选择 TokenService
pickClusterService 方法根据当前节点的角色选择对应的 TokenService 实现:
private static TokenService pickClusterService() {
// 客户端角色 → 获取 ClusterTokenClient
if (ClusterStateManager.isClient()) {
return TokenClientProvider.getClient();
}
// 服务端角色(嵌入模式)→ 获取 EmbeddedClusterTokenServer
if (ClusterStateManager.isServer()) {
return EmbeddedClusterTokenServerProvider.getServer();
}
return null;
}
巧妙的设计:
- 客户端角色:通过网络请求远程服务端
- 服务端角色(嵌入模式):直接本地调用,不走网络
- 对外统一是
TokenService接口,上层调用无需关心底层实现
2.3 requestToken:发起 Token 请求
拿到 TokenService 后,调用 requestToken 方法申请令牌。
- 如果是客户端角色:将方法参数构造为请求数据包,通过 Netty 向服务端发起请求,同步等待响应
- 如果是嵌入模式服务端角色:直接在本地调用 DefaultTokenService 的方法
关于 Netty 通信的具体实现不是本文重点,我们更关注限流逻辑本身。
2.4 applyTokenResult:处理响应结果
applyTokenResult 方法根据服务端返回的状态码决定后续行为:
private static boolean applyTokenResult(TokenResult result, FlowRule rule, Context context,
DefaultNode node, int acquireCount, boolean prioritized) {
switch (result.getStatus()) {
case TokenResultStatus.OK:
// 申请成功,直接放行
return true;
case TokenResultStatus.SHOULD_WAIT:
// 需要等待,休眠后放行
try {
Thread.sleep(result.getWaitInMs());
} catch (InterruptedException e) {
}
return true;
case TokenResultStatus.NO_RULE_EXISTS:
case TokenResultStatus.BAD_REQUEST:
case TokenResultStatus.FAIL:
case TokenResultStatus.TOO_MANY_REQUEST:
// 各种失败情况,回退到本地限流
return fallbackToLocalOrPass(rule, context, node, acquireCount, prioritized);
case TokenResultStatus.BLOCKED:
default:
// 被限流,拒绝请求
return false;
}
}
状态处理逻辑总结:
| 状态 | 处理方式 |
| OK | 直接放行 |
| SHOULD_WAIT | 休眠 waitInMs 毫秒后放行(用于优先级请求) |
| BLOCKED | 直接拒绝 |
| 其他失败状态 | 回退到本地限流或直接放行 |
2.5 fallbackToLocalOrPass:失败回退
当请求失败或服务端异常时,走 fallbackToLocalOrPass 方法:
private static boolean fallbackToLocalOrPass(FlowRule rule, Context context, DefaultNode node,
int acquireCount, boolean prioritized) {
if (rule.getClusterConfig().isFallbackToLocalWhenFail()) {
// 开启回退:走本地限流逻辑
return passLocalCheck(rule, context, node, acquireCount, prioritized);
} else {
// 不开启回退:直接放行(规则不生效)
return true;
}
}
两种策略:
- fallbackToLocalWhenFail = true(默认,推荐):失败时回退到本地限流,保证系统安全
- fallbackToLocalWhenFail = false:失败时直接放行,相当于限流失效
强烈建议保持默认值 true。服务可用性永远是第一位的,其次才是限流的精确性。
2.6 关于 SHOULD_WAIT
注意:Sentinel 集群限流没有实现匀速排队和冷启动的流控效果,只支持直接拒绝。
那 SHOULD_WAIT 是做什么用的呢?
它是用于实现优先级请求的:当请求具有优先级且当前窗口令牌不足时,可以尝试”预占”下一个时间窗口的令牌。如果抢占成功,就告诉客户端”你可以通过,但需要等到下一个窗口开始”。
这是一种”提前申请未来时间通过”的方式,用于实现优先级语义。
三、服务端实现原理
3.1 入口:DefaultTokenService#requestToken
无论是客户端发来的请求,还是嵌入模式的本地调用,最终都会交给 DefaultTokenService 处理:
@Override
public TokenResult requestToken(Long ruleId, int acquireCount, boolean prioritized) {
// 参数校验
if (notValidRequest(ruleId, acquireCount)) {
return badRequest();
}
// (1) 根据规则 ID 获取限流规则
FlowRule rule = ClusterFlowRuleManager.getFlowRuleById(ruleId);
if (rule == null) {
return new TokenResult(TokenResultStatus.NO_RULE_EXISTS);
}
// (2) 判断是否允许通过
return ClusterFlowChecker.acquireClusterToken(rule, acquireCount, prioritized);
}
流程很清晰:
- 根据 ruleId 从
ClusterFlowRuleManager获取限流规则 - 调用
ClusterFlowChecker#acquireClusterToken进行核心判断
为什么只传 ruleId 而不传整个规则?
因为 ruleId 只需要一个 Long 类型(8 字节),而整个规则对象会大很多。只传 ID 可以大幅减小数据包大小,优化网络通信性能。这也是要求 flowId 全局唯一的原因。
3.2 acquireClusterToken:核心判断逻辑
ClusterFlowChecker#acquireClusterToken 是服务端限流判断的核心方法。代码比较长,我们拆分成四个部分来分析。
第一部分:前置检查与阈值计算
static TokenResult acquireClusterToken(FlowRule rule, int acquireCount, boolean prioritized) {
Long id = rule.getClusterConfig().getFlowId();
// (1) 全局 QPS 限制检查(按 namespace 级别的总限流)
if (!allowProceed(id)) {
return new TokenResult(TokenResultStatus.TOO_MANY_REQUEST);
}
// (2) 获取该规则的指标统计
ClusterMetric metric = ClusterMetricStatistics.getMetric(id);
if (metric == null) {
return new TokenResult(TokenResultStatus.FAIL);
}
// (3) 计算阈值和剩余令牌
double latestQps = metric.getAvg(ClusterFlowEvent.PASS);
double globalThreshold = calcGlobalThreshold(rule) * ClusterServerConfigManager.getExceedCount();
double nextRemaining = globalThreshold - latestQps - acquireCount;
if (nextRemaining >= 0) {
// 第二部分:令牌充足,放行
} else {
if (prioritized) {
// 第三部分:令牌不足但是优先级请求,尝试预占下一个窗口
}
// 第四部分:令牌不足,拒绝
}
}
关键步骤解读:
- allowProceed:检查 namespace 级别的全局 QPS 是否超限(可以给每个 namespace 配置一个总 QPS 上限)
- 获取 ClusterMetric:获取该规则对应的滑动窗口统计数据
- 计算剩余令牌:
latestQps:当前窗口已通过的 QPSglobalThreshold:集群总阈值nextRemaining:剩余可用 = 总阈值 – 已通过 – 当前申请
getExceedCount()是一个允许的超额比例,默认 1.0,即不允许超额。
关于 namespace 全局限流:
Sentinel 支持按 namespace 配置全局 QPS 限制,无视具体规则,只要是同一 namespace 的请求都先过这一层。配置方式:
ServerFlowConfig serverFlowConfig = new ServerFlowConfig();
serverFlowConfig.setMaxAllowedQps(1000);
ClusterServerConfigManager.loadFlowConfig("serviceA", serverFlowConfig);
这个功能使用场景不多,官方文档也没有详细介绍,了解即可。
3.3 calcGlobalThreshold:计算集群总阈值
calcGlobalThreshold 方法根据阈值类型计算集群总阈值:
private static double calcGlobalThreshold(FlowRule rule) {
double count = rule.getCount();
switch (rule.getClusterConfig().getThresholdType()) {
case ClusterRuleConstant.FLOW_THRESHOLD_GLOBAL:
// 集群总阈值模式:直接使用配置值
return count;
case ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL:
default:
// 单机均摊模式:单机阈值 × 连接的客户端数
int connectedCount = ClusterFlowRuleManager.getConnectedCount(
rule.getClusterConfig().getFlowId());
return count * connectedCount;
}
}
两种阈值类型:
| 类型 | 计算方式 |
| 集群总阈值(GLOBAL) | 直接使用规则配置的 count |
| 单机均摊(AVG_LOCAL) | count × 当前连接的客户端数量 |
为什么客户端要传 namespace?
这就是原因所在!
在单机均摊模式下,服务端需要知道有多少个客户端连接,才能计算总阈值。而客户端连接是按 namespace 分组的,所以客户端连接时必须带上 namespace。
划重点:如果客户端没有正确传递 namespace,单机均摊模式下计算出来的总阈值会是 0,导致所有请求都被限流!这是使用集群限流时最容易踩的坑之一。
3.4 第二部分:令牌充足,放行
当 nextRemaining >= 0 时,说明令牌充足,执行放行逻辑:
metric.add(ClusterFlowEvent.PASS, acquireCount);
metric.add(ClusterFlowEvent.PASS_REQUEST, 1);
if (prioritized) {
metric.add(ClusterFlowEvent.OCCUPIED_PASS, acquireCount);
}
return new TokenResult(TokenResultStatus.OK)
.setRemaining((int) nextRemaining)
.setWaitInMs(0);
做了三件事:
- 记录 PASS(通过的令牌数)
- 记录 PASS_REQUEST(通过的请求数)
- 如果是优先级请求,额外记录 OCCUPIED_PASS(预占用通过)
- 返回 OK 状态和剩余令牌数
3.5 第三部分:优先级请求预占下一个窗口
当令牌不足但请求具有优先级时,尝试预占下一个时间窗口的令牌:
double occupyAvg = metric.getAvg(ClusterFlowEvent.WAITING);
if (occupyAvg <= ClusterServerConfigManager.getMaxOccupyRatio() * globalThreshold) {
int waitInMs = metric.tryOccupyNext(ClusterFlowEvent.PASS, acquireCount, globalThreshold);
if (waitInMs > 0) {
return new TokenResult(TokenResultStatus.SHOULD_WAIT)
.setRemaining(0)
.setWaitInMs(waitInMs);
}
}
逻辑说明:
- 检查当前等待队列的大小是否超过限制(maxOccupyRatio 是最大预占比例)
- 调用
tryOccupyNext尝试预占下一个窗口,计算需要等待的时间 - 如果可以预占,返回
SHOULD_WAIT状态,告诉客户端等待多久
预占的含义:
- 当前窗口的令牌用完了
- 但我可以提前”预约”下一个窗口的令牌
- 需要等到下一个窗口开始才能通过
- 等待时间 = 下一个窗口开始的时间 – 当前时间
3.6 第四部分:令牌不足,拒绝
如果令牌不足,且不是优先级请求(或优先级请求也预占失败),则拒绝请求:
metric.add(ClusterFlowEvent.BLOCK, acquireCount);
metric.add(ClusterFlowEvent.BLOCK_REQUEST, 1);
if (prioritized) {
metric.add(ClusterFlowEvent.OCCUPIED_BLOCK, acquireCount);
}
return blockedResult();
做了三件事:
- 记录 BLOCK(被拒绝的令牌数)
- 记录 BLOCK_REQUEST(被拒绝的请求数)
- 如果是优先级请求,额外记录 OCCUPIED_BLOCK(预占用拒绝)
- 返回 BLOCKED 状态
四、集群限流的指标统计
4.1 独立的滑动窗口实现
集群限流使用的滑动窗口不是 sentinel-core 模块中的实现,而是 sentinel-cluster-server-default 模块自己实现的。
在加载集群限流规则时,会为每条规则创建一个 ClusterMetric:
private static void applyClusterFlowRule(List<FlowRule> list, String namespace) {
// ...
for (FlowRule rule : list) {
if (!rule.isClusterMode()) {
continue;
}
// ...
ClusterFlowConfig clusterConfig = rule.getClusterConfig();
// 如果不存在,则为规则创建 ClusterMetric
ClusterMetricStatistics.putMetricIfAbsent(flowId,
new ClusterMetric(clusterConfig.getSampleCount(),
clusterConfig.getWindowIntervalMs()));
}
// 移除不再使用的 ClusterMetric
clearAndResetRulesConditional(namespace, new Predicate<Long>() {
@Override
public boolean test(Long flowId) {
return !ruleMap.containsKey(flowId);
}
});
// ...
}
创建参数就是我们在 ClusterFlowConfig 中配置的:
sampleCount:窗口数量windowIntervalMs:窗口总时长(毫秒)
4.2 统计指标
集群限流收集的指标数据定义在 ClusterFlowEvent 枚举中:
public enum ClusterFlowEvent {
PASS, // 已发放的令牌总数
BLOCK, // 被驳回的令牌总数
PASS_REQUEST, // 被放行的请求总数
BLOCK_REQUEST, // 被拒绝的请求总数
OCCUPIED_PASS, // 预占用-已发放的令牌数
OCCUPIED_BLOCK, // 预占用-被驳回的令牌数
WAITING // 等待下一个窗口的请求数
}
各指标含义:
| 指标 | 说明 |
| PASS | 通过的令牌数(一个请求可能申请多个令牌) |
| BLOCK | 被拒绝的令牌数 |
| PASS_REQUEST | 通过的请求数(按请求计数) |
| BLOCK_REQUEST | 被拒绝的请求数 |
| OCCUPIED_PASS | 优先级请求预占通过的令牌数 |
| OCCUPIED_BLOCK | 优先级请求预占被拒绝的令牌数 |
| WAITING | 当前等待下一个窗口的请求数 |
除了统计的指标项不同,滑动窗口的实现方式和 sentinel-core 中的基本一致,都是基于 LeapArray 的滑动窗口算法。
五、总结与注意事项
5.1 两种部署模式的选择
| 模式 | 优点 | 缺点 | 适用场景 |
| 嵌入模式 | 无需单独部署 | 影响应用性能,无自动切换 | 单服务、小规模 |
| 独立模式 | 性能隔离,统一服务 | 需要额外部署和运维 | 多服务、生产环境 |
5.2 Namespace 的重要性
关于 namespace,再强调几点:
- 按应用区分 namespace:不要整个项目所有微服务共用一个 namespace
- 客户端必须设置 namespace:否则单机均摊模式会出问题
- namespace 在连接时传递:通过 PING 消息携带给服务端
- 规则按 namespace 隔离:不同 namespace 的规则互不影响
为什么不能共用 namespace?
如果多个应用共用一个 namespace,在单机均摊模式下,计算客户端总数时会把所有应用的客户端都算进去,导致总阈值被放大,限流就不准了。
5.3 集群限流的局限性
- 不是解决请求倾斜的银弹:集群限流只能保证总阈值准确,但某些节点流量过高的问题仍然存在,需要结合负载均衡优化
- 只支持快速拒绝:没有实现匀速排队和冷启动的流控效果
- 服务端单点问题:独立模式下服务端是单点,需要考虑高可用
- 请求倾斜严重时:可能导致某些节点负载过高,需要配合系统自适应限流和熔断降级做兜底
5.4 实践建议
- 生产环境使用独立模式:避免影响业务应用性能
- 务必开启失败回退:
fallbackToLocalWhenFail = true - 正确配置 namespace:按应用隔离,避免串扰
- 监控集群限流状态:监控服务端健康、连接数、通过率等
- 结合其他保护手段:系统自适应限流、熔断降级不能少
- 压测验证:上线前充分压测,了解性能瓶颈
总结与思考
本文作为集群限流系列的下篇,深入源码层面剖析了 Sentinel 集群限流的实现原理。
核心要点回顾
1. 核心接口设计
- TokenService:统一的 Token 申请接口
- ClusterTokenClient / ClusterTokenServer:客户端和服务端接口
- EmbeddedClusterTokenServer:嵌入模式,本地调用不走网络
2. 客户端流程
- FlowRuleChecker → pickClusterService → requestToken → applyTokenResult
- 失败时可回退到本地限流(默认开启)
- 优先级请求支持预占下一个窗口
3. 服务端流程
- DefaultTokenService → ClusterFlowChecker → acquireClusterToken
- 支持全局阈值和单机均摊两种阈值类型
- 优先级请求支持预占未来窗口的令牌
4. 指标统计
- 每条集群限流规则对应一个 ClusterMetric
- 独立实现滑动窗口,统计 PASS / BLOCK / OCCUPIED 等指标
集群限流是 Sentinel 的高级功能,虽然使用场景不如单机限流广泛,但在需要精确控制总流量的场景下非常有用。理解它的实现原理,能帮助我们更好地使用和排查问题。
下一篇文章我们将探讨一个大家都关心的话题:Sentinel 对应用性能的影响到底有多大?敬请期待。












请登录后查看评论内容