Sentinel 流量控制
作者:大翔 CTO 审核 | 白歌 架构师 技术校验 | 小崔 后端开发 示例编写 | 黄俪 前端开发 Dashboard 验证
定位
Sentinel 解决什么问题
Sentinel(哨兵)是阿里巴巴开源的流量防卫兵,以流量为切入点,从流量控制(Flow Control)、熔断降级(Circuit Breaking)、**系统自适应保护(System Adaptive Protection)**三个维度保障微服务稳定性。对于 985 科班同学而言,可以把它理解为微服务架构中的"智能交通调度系统"——既要保证高速公路不被冲垮(流量整形),也要在事故路段提前分流(熔断降级),还要在节假日整体限流(系统保护)。
具体解决三大问题:
| 问题域 | 具体表现 | Sentinel 对策 |
|---|---|---|
| 流量整形(Traffic Shaping) | 秒杀场景瞬间 QPS 从 100 飙升至 10000,后端服务被打垮 | 通过 QPS/线程数阈值限流,超出流量快速失败或排队等待 |
| 过载保护(Overload Protection) | 下游依赖变慢导致调用方线程池耗尽,级联雪崩 | 通过熔断降级快速失败,保护调用方自身资源 |
| 流量调度(Traffic Routing) | 核心链路与边缘链路共用同一资源池,互相影响 | 通过关联限流和链路限流,优先保障核心业务 |
与 Hystrix 的对比
Hystrix 是 Netflix 开源的熔断器库,已于 2018 年进入维护模式。Sentinel 在功能丰富度、可观测性和活跃度上全面超越:
| 维度 | Hystrix | Sentinel |
|---|---|---|
| 规则类型 | 仅熔断(线程池/信号量隔离) | 流控 + 熔断 + 热点参数 + 系统规则 + 授权规则 |
| 可视化 | 需要集成 Hystrix Dashboard + Turbine | 自带 Dashboard,实时监控 QPS/RT/线程数/调用链路 |
| 隔离机制 | 线程池隔离(重量级,额外上下文切换开销) | 信号量隔离(轻量级,基于并发线程数统计) |
| 规则推送 | 无原生推送,需依赖 Archaius 轮询 | 支持原始模式 → Pull 模式 → Push 模式(配置中心推送) |
| 活跃度 | 停维,仅修复严重 Bug | 阿里巴巴持续迭代,社区活跃 |
| 扩展性 | 有限的插件体系 | Slot Chain 责任链模式,可自定义 Slot SPI 扩展 |
核心概念
在深入代码之前,理解四个核心概念的定位至关重要:
- 资源(Resource):Sentinel 要保护的目标。可以是一个 URL(
/order/{id})、一个方法、甚至一段代码块。在 Sentinel 的世界观里,一切需要被保护的调用都是资源。 - 规则(Rule):围绕资源设定的控制策略。例如"资源
getOrder的 QPS 不超过 10"、"资源getUser在异常比超过 50% 时熔断 10 秒"。规则可以动态添加、修改和删除,无需重启应用。 - 入口(Entry):每次资源调用都会生成一个 Entry 对象,代表一次流量"进入 Sentinel 管辖范围"。Entry 穿过 Slot Chain 时会被各种规则检查。
- Slot Chain(插槽链):Sentinel 的核心处理流水线,基于责任链模式(Chain of Responsibility Pattern)。每个 Slot 负责一个维度的检查(统计、流控、熔断、授权等),链式调用,逐层校验。
核心概念
资源的三种定义方式
小崔在飞翔科技订单服务中,根据不同的保护粒度选择不同的资源定义方式:
方式一:URL 默认资源(零侵入)
Sentinel 自动将 Spring MVC 的 URL 作为资源名,无需任何代码改动。适合快速接入、按接口维度限流的场景。
# Sentinel Dashboard 中自动出现:
# 资源名: GET:/order/{id}
# 资源名: POST:/order/create
缺点:资源名不可自定义,粒度固定为 URL 级别。
方式二:@SentinelResource 注解(推荐)
通过注解声明资源名和降级处理方法,粒度为方法级别,是生产环境最常用的方式。
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping("/{id}")
@SentinelResource(
value = "getOrder", // 资源名
blockHandler = "getOrderBlockHandler" // 流控/熔断降级方法
)
public String getOrder(@PathVariable Long id) {
// 业务逻辑...
return "Order-" + id;
}
// blockHandler:处理 BlockException(流控/熔断触发)
public String getOrderBlockHandler(Long id, BlockException ex) {
return "订单查询限流中,请稍后重试";
}
}
优点:资源名可控;可指定 blockHandler 和 fallback;支持热点参数限流。
方式三:SphU API 硬编码(代码块级)
最细粒度的控制方式,适合保护非 Web 入口的代码块(如定时任务、MQ 消费、第三方 SDK 调用)。
import com.alibaba.csp.sentinel.SphU;
public void batchExportOrders() {
Entry entry = null;
try {
entry = SphU.entry("batchExportOrders");
// 受保护的代码块
doBatchExport();
} catch (BlockException e) {
// 被限流或熔断
log.warn("批量导出被限流");
} finally {
if (entry != null) {
entry.exit();
}
}
}
三种方式的推荐顺序:
@SentinelResource> URL 默认资源 > SphU API。99% 的场景用注解即可满足。
流量控制规则(FlowRule)
流量控制规则是 Sentinel 最核心的规则类型,围绕"多少请求可以通过"展开。大翔在技术评审时总结了一句话:"理解 FlowRule,就理解了 Sentinel 的 80%。"
两种统计维度:
| 维度 | 含义 | 适用场景 | 技术本质 |
|---|---|---|---|
| QPS 模式(FLOW_GRADE_QPS) | 每秒通过的请求数 | 流量整形,控制请求频率 | 通过滑动窗口统计每秒请求数,超过阈值则拒绝。适合大多数 Web 接口保护 |
| 线程数模式(FLOW_GRADE_THREAD) | 同时处理的并发线程数 | 隔离慢调用,防止线程池耗尽 | 统计当前正在处理的请求数,超过阈值则拒绝。适合保护调用下游慢服务的场景 |
// 代码方式定义 FlowRule
FlowRule rule = new FlowRule();
rule.setResource("getOrder"); // 资源名
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // QPS 模式
rule.setCount(3); // 阈值:3 QPS
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); // 快速失败
FlowRuleManager.loadRules(Collections.singletonList(rule));
白歌补充:"QPS 和线程数的选择有门道。QPS 像高速公路收费站——控制进入的车流量;线程数像停车场——控制同时停着的车数量。如果下游 RT 波动不大,用 QPS 就够了;如果下游偶尔会变慢(比如调用第三方接口),用线程数模式能更好地隔离慢调用扩散。"
流控效果(ControlBehavior)
当 QPS/线程数超过阈值后,如何处理超出的请求?三种效果:
| 效果 | 行为 | 适用场景 | 用户体验 |
|---|---|---|---|
| 快速失败(Fast Fail,默认) | 直接抛出 FlowException | 对响应时间要求极高,不允许排队 | "系统繁忙" 提示(黄俪设计的友好提示页) |
| Warm Up(预热) | 阈值从 1/coldFactor 逐步升至设定值 | 冷启动防止流量冲击 | 用户无感知(客户端自动重试) |
| 排队等待(Rate Limiter) | 请求排队,匀速通过,多余等待超时 | 脉冲流量削峰填谷 | 响应变慢但不会失败 |
Warm Up 预热原理(令牌桶算法的变体):
Sentinel 预热模式基于 Guava 的 SmoothWarmingUp 思想,使用**令牌桶算法(Token Bucket Algorithm)**的变体实现。启动时桶内令牌生成速率很低(阈值 / coldFactor,coldFactor 默认为 3),在 warmUpPeriodSec 时长内线性增长到全速率。
预热曲线:
QPS │
│ ┌────────── 全速率 100
100 ├────────────────────┤
│ ╱
│ ╱
33 ├─────────╱ coldFactor=3,初始速率 33
│
└─────┬──────────────────► 时间
预热期(warmUpPeriodSec,默认 10s)
小崔的感受:"之前有一次服务重启后立即被大量流量打挂,就是因为刚启动时缓存没热、连接池没满。加上 Warm Up 预热后,前 10 秒只放行 1/3 流量,等 JIT 编译完成、连接池建立好,再全量放开。"
流控模式(Strategy)
三种流控模式决定了"什么时候触发限流":
| 模式 | 触发条件 | 配置项 | 典型场景 |
|---|---|---|---|
| 直接(Direct) | 当前资源 QPS/线程数超过阈值 | strategy=0 | 限制下单接口本身 |
| 关联(Relate) | 关联资源超过阈值时,限制当前资源 | strategy=1 + refResource | 写接口压力大时,自动限制读接口 |
| 链路(Chain) | 当前资源在指定入口链路中超过阈值 | strategy=2 + refResource | 同一个 Service 方法,只限制来自下单入口的调用 |
直接模式:最常用,资源自己管自己。
关联模式:优先级调度思想。白歌举了个例子:"订单查询(读)和订单写入(写)共享数据库连接池。写操作本身不重要可以排队,但如果写操作 QPS 过高抢占连接池,会导致读操作 RT 升高。这时设置关联规则——当写接口 QPS 超过阈值时,自动限制读接口,优先保证写操作完成。"
链路模式:精细化流量治理。同一个 Service 方法被多个入口调用,只想限制某个入口的调用频率。
// 关联流控规则
FlowRule rule = new FlowRule();
rule.setResource("getOrder"); // 被限制的资源:读接口
rule.setStrategy(RuleConstant.STRATEGY_RELATE); // 关联模式
rule.setRefResource("createOrder"); // 关联资源:写接口
rule.setCount(50); // 当写接口 QPS > 50 时,限制读接口
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
热点参数限流(ParamFlowRule)
流量控制的精细化工夫。不是"整个接口限 100 QPS",而是"接口整体限 100 QPS,但商品 ID=100 的热卖品只给 1 QPS"。
核心思想:统计同一资源中不同参数值(如商品 ID、用户 ID)的请求频率,对高频参数值(热点)单独限流,对低频参数值(普通)使用全局阈值。既保障了整体流量,又精准打压了热点。
// 热点参数规则
ParamFlowRule rule = new ParamFlowRule();
rule.setResource("getProductDetail"); // 商品详情接口
rule.setParamIdx(0); // 第 0 个参数(商品 ID)
rule.setCount(100); // 全局阈值:100 QPS
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 对特定参数值设置例外:商品 ID=100 的 QPS 限制为 1
ParamFlowItem item = new ParamFlowItem();
item.setObject("100"); // 参数值
item.setClassType(String.class.getName());
item.setCount(1); // 例外阈值
rule.setParamFlowItemList(Collections.singletonList(item));
热点参数限流的判定优先级:先检查热点参数例外 → 再检查全局 QPS 阈值。如果商品 ID=100 的请求已达 1 QPS,即使全局还有 99 QPS 的余量也会被拒绝。
系统自适应保护规则(SystemRule)
前面的规则都是"针对某个资源的限流",而系统规则是"从整个应用实例的维度保护",防止系统整体负载过高导致雪崩。
| 触发条件 | 含义 | 默认阈值 | 说明 |
|---|---|---|---|
| Load(系统负载) | SystemLoad >= 阈值 | CPU 核数 × 2.5 | 仅 Linux/Unix 有效,Windows 不可用 |
| RT(平均响应时间) | 所有入口流量的平均 RT >= 阈值 | — | 单位 ms,防止 RT 恶化 |
| 线程数 | 所有入口的并发线程数 >= 阈值 | — | 防止线程池耗尽 |
| 入口 QPS | 所有入口的 QPS 总和 >= 阈值 | — | 整体限流,兜底策略 |
| CPU 使用率 | CPU usage >= 阈值 | — | 仅支持 JDK 9+ / Sun JDK |
SystemRule rule = new SystemRule();
rule.setHighestSystemLoad(4.0); // Load 阈值
rule.setHighestCpuUsage(0.8); // CPU 使用率 80%
rule.setAvgRt(100); // 平均 RT ≥ 100ms
rule.setMaxThread(200); // 并发线程数 ≥ 200
rule.setQps(500); // 入口 QPS ≥ 500
SystemRuleManager.loadRules(Collections.singletonList(rule));
白歌强调:"系统规则是最后一道防线。当业务层的流控规则配置失误时,系统规则能从操作系统层面兜底,确保单个实例不会被打挂。"
核心原理
Sentinel Slot Chain 架构
Sentinel 的整个处理流水线基于**责任链模式(Chain of Responsibility Pattern)**构建。每个请求进入 Sentinel 时,依次穿过如下 Slot 链:
各 Slot 职责:
| Slot | 职责 | 关键操作 |
|---|---|---|
| NodeSelectorSlot | 构建调用链路树,记录资源间的调用关系 | 为每个资源创建 DefaultNode,挂载到 Context 的调用树 |
| ClusterBuilderSlot | 构建集群节点统计信息,聚合同一资源的全局指标 | 创建 ClusterNode,同一资源的所有链路共享 |
| StatisticSlot | 核心统计插槽:记录通过数、阻塞数、异常数、RT、线程数 | Fire-and-Forget 模式:先放行后续 Slot,再统计(保证异常和 RT 被准确计数) |
| AuthoritySlot | 黑白名单授权控制 | 按 origin(调用方来源应用)做访问控制 |
| SystemSlot | 系统自适应保护 | 检查 Load / CPU / RT / 线程数 |
| FlowSlot | 流量控制 | 从 FlowRuleManager 获取规则,判断是否限流 |
| DegradeSlot | 熔断降级 | 从 DegradeRuleManager 获取规则,判断是否熔断 |
| ParamFlowSlot | 热点参数限流 | 按参数值统计,对热点参数执行例外规则 |
关键设计点——StatisticSlot 的 Fire-and-Forget 统计时机:
伪代码展示核心逻辑:
entry(context, resource, node, count, ...) {
// 步骤1:先放行后续 Slot 链(FlowSlot → DegradeSlot → 业务逻辑)
fireEntry(context, resource, node, count, ...);
// 步骤2:请求成功通过,统计数据
node.increaseThreadNum(); // 当前线程数 +1
node.addPassRequest(count); // 通过 QPS + count
long startTime = now();
try {
// 步骤3:执行业务逻辑
} catch (BlockException e) {
node.increaseBlockQps(count); // 被阻塞 QPS + count
throw e;
} catch (Exception e) {
node.increaseExceptionQps(count); // 异常 QPS + count
} finally {
long rt = now() - startTime;
node.addRtAndSuccess(rt, count); // RT 统计 + 成功数
node.decreaseThreadNum(); // 当前线程数 -1
}
}
Fire-and-Forget 的设计精妙之处:先放行再统计,确保即使业务逻辑抛出异常,统计信息也不会丢失。同时 RT 的统计完全包含业务逻辑的执行时间,真实反映用户体感延迟。
扩展机制——Slot SPI:
Sentinel 的 Slot Chain 通过 Java SPI 机制加载,开发者可以自定义 Slot 并插入到链的任意位置。大翔曾在一次技术分享中提到:"Sentinel 的可扩展性源于 Slot Chain 的开放性。曾经在一个项目里,我们写了一个 MetricsPushSlot 插在 StatisticSlot 后面,把 QPS/RT 数据实时推送到 Prometheus,就改了一行 SPI 配置。"
LeapArray 滑动窗口统计原理
Sentinel 的 QPS 统计不是简单的"每秒清零计数器",而是基于**环形数组(Ring Buffer)+ 滑动窗口(Sliding Window)**的 LeapArray 算法。这是白歌在面试中最喜欢追问候选人的知识点。
关键参数与默认值:
| 参数 | 默认值 | 说明 |
|---|---|---|
sampleCount | 2 | 环形数组窗口数量 |
intervalInMs | 1000 | 统计周期(ms) |
| 窗口长度(windowLengthInMs) | 500ms | intervalInMs / sampleCount |
核心操作——currentWindow() 窗口定位:
当前时间 currentTime = 1520000000000ms
// 1. 计算窗口索引
windowIndex = (currentTime / windowLengthInMs) % sampleCount
= (1520000000000 / 500) % 2
= 0 或 1
// 2. 计算窗口起始时间
windowStart = currentTime - (currentTime % windowLengthInMs)
// 3. 判断窗口是否过期
if (windowStart > currentWindow.windowStart) {
重置窗口数据 // 旧窗口被复用
}
为什么不用简单计数器?
简单计数器(AtomicLong 每秒清零)存在跨秒边界精度崩溃问题:
简单计数器问题示意:
Time: 0ms ───────── 900ms ── 1000ms ── 1100ms ─────────►
请求A=100个 请求B=100个
↑ 按简单计数器,前一秒=200, 后一秒=100
↑ 但实际900~1100ms这200ms内有200个请求
↑ 精度崩溃:200ms窗口被统计为1s的数据
滑动窗口始终覆盖最近 intervalInMs 的精确窗口,无边界误差。每个窗口的数据是"最近一个 intervalInMs 内该窗口时段的真实统计"。
控制台通信机制
Sentinel Dashboard 与客户端(微服务实例)之间的通信是理解规则推送和监控数据来源的关键。
关键配置:
spring:
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080 # Dashboard 地址
port: 8719 # 应用与 Dashboard 通信的本地端口
heartbeat-interval-ms: 5000 # 心跳间隔(默认 5s)
eager: true # 启动时立即注册到 Dashboard
规则推送模式演进:
| 模式 | 原理 | 优缺点 |
|---|---|---|
| 原始模式(API 直推) | Dashboard 通过 HTTP API 直接推送到应用内存 | 简单但重启丢失 |
| Pull 模式(定时拉取) | 应用定时从配置中心拉取规则 | 有延迟,实时性差 |
| Push 模式(配置中心推送) | 规则写入 Nacos/Apollo → 配置中心实时推送到所有实例 | 推荐方案,持久化 + 实时推送 |
Push 模式配置(推荐):
spring:
cloud:
sentinel:
datasource:
flow:
nacos:
server-addr: 127.0.0.1:8848
data-id: ${spring.application.name}-flow-rules
group-id: SENTINEL_GROUP
data-type: json
rule-type: flow
白歌提醒:"面试时一定会问规则持久化。记住一句话:Dashboard 负责规则的可视化管理,Nacos 负责规则的持久化存储和实时推送,应用实例通过 DataSource 监听 Nacos 变更并实时更新本地 RuleManager。三者各司其职。"
环境准备
Sentinel Dashboard 下载与启动
Sentinel Dashboard 是独立的 Spring Boot 应用,提供规则管理和实时监控可视化。
下载:
从 GitHub Release 页面下载最新版 jar 包:
# 下载 Sentinel Dashboard(以 1.8.8 版本为例)
wget https://github.com/alibaba/Sentinel/releases/download/1.8.8/sentinel-dashboard-1.8.8.jar
启动:
# Windows 启动(默认端口 8080)
java -Dserver.port=8080 ^
-Dcsp.sentinel.dashboard.server=localhost:8080 ^
-Dproject.name=sentinel-dashboard ^
-jar sentinel-dashboard-1.8.8.jar
# Linux/macOS 启动
java -Dserver.port=8080 \
-Dcsp.sentinel.dashboard.server=localhost:8080 \
-Dproject.name=sentinel-dashboard \
-jar sentinel-dashboard-1.8.8.jar
启动后访问 http://localhost:8080,默认账号密码均为 sentinel。
黄俪体验 Dashboard 后的反馈:"界面很简洁,实时监控面板一眼就能看到 QPS 曲线和单台机器的 RT 分布。簇点链路功能可以直观看到每个资源的调用量排名,给前端做监控大屏时直接嵌入 iframe 就行。"
客户端接入依赖与配置
小崔在订单服务中引入 Sentinel:
Maven 依赖:
<!-- Sentinel 核心 Starter(已包含 sentinel-core + sentinel-transport-simple-http) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
使用
spring-cloud-alibaba-dependenciesBOM 管理版本,无需指定 version。
application.yml 配置:
spring:
application:
name: order-service
cloud:
sentinel:
transport:
dashboard: 127.0.0.1:8080 # Dashboard 地址
port: 8719 # 本地通信端口(每个实例需不同)
eager: true # 启动时立即注册(否则首次访问后才注册)
web-context-unify: true # 默认统一 Web Context(链路限流时需设为 false)
filter:
enabled: true # 启用 Spring MVC Filter 拦截
transport.port默认从 8719 开始自增寻找可用端口,多实例部署在同一机器时无需手动分配。
完整示例1:QPS 限流(直接模式 + 快速失败)
场景描述
飞翔科技的订单服务是小崔负责的核心微服务。某天运营部门策划了一场爆品秒杀活动,预计瞬时 QPS 将突破 500。小崔在压力测试中发现,不做任何保护时,订单查询接口在 500 QPS 下直接 OOM(Out Of Memory)。
大翔在技术评审会上拍板:"订单查询接口是核心中的核心,但数据库只能撑 3 QPS。多出来的请求没必要进业务层,直接在网关层被 Sentinel 拦截,快速失败返回,既保护了数据库也让用户立刻知道限流了。"
代码实现
package com.feixiangtech.order.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/order")
public class OrderController {
/**
* 订单查询接口
* @SentinelResource 将本方法声明为资源 "getOrder"
* blockHandler 指定流控/熔断触发时的降级方法
*/
@GetMapping("/{id}")
@SentinelResource(
value = "getOrder", // 资源名
blockHandler = "getOrderBlockHandler" // 流控/熔断降级方法
)
public String getOrder(@PathVariable Long id) {
// 模拟数据库查询
// 真实场景:orderService.findById(id)
return "订单详情: ID=" + id + ", 商品=MacBook Pro, 金额=14999元";
}
/**
* BlockHandler 方法(流控/熔断触发时调用)
* 必须满足以下条件:
* 1. 方法签名与原方法一致(参数类型和返回值类型)
* 2. 最后额外加一个 BlockException 参数
* 3. 方法必须是 public
* 4. 如果原方法在同一个类中,blockHandler 方法必须是 static
* (不同类则不需要 static)
*/
public String getOrderBlockHandler(Long id, BlockException ex) {
return "Blocked by Sentinel —— 订单查询限流中,请稍后重试";
}
}
Dashboard 配置
- 启动订单服务,确认 Sentinel Dashboard 的"机器列表"中出现
order-service - 进入"簇点链路",找到资源
getOrder,点击"流控"按钮 - 配置规则:
| 配置项 | 值 | 说明 |
|---|---|---|
| 资源名 | getOrder | 与 @SentinelResource value 一致 |
| 阈值类型 | QPS | 按每秒请求数限流 |
| 单机阈值 | 3 | 每秒最多 3 个请求 |
| 流控模式 | 直接 | 当前资源自己触发限流 |
| 流控效果 | 快速失败 | 直接抛出 FlowException |
操作前后对比
| 状态 | 最大 QPS | 超出流量行为 | 数据库压力 | 用户体验 |
|---|---|---|---|---|
| 操作前(无限流) | 500+ | 全部进入业务层 | CPU 100%,OOM 崩溃 | 服务不可用,500 错误 |
| 操作后(QPS=3) | 3 | 快速返回限流提示 | CPU < 20%,稳定 | 限流提示友好,核心请求正常处理 |
黄俪在前端加了一个简单的重试提示:"收到 'Blocked by Sentinel' 后,页面显示一个倒计时 3 秒的遮罩层,用户无需手动刷新,自动重试。"
完整示例2:关联限流 + Warm Up 预热
场景描述
飞翔科技订单服务有两个核心接口:
GET /order/{id}—— 订单查询(核心,用户下单前必查,对延迟敏感)POST /order/create—— 创建订单(次要,允许排队,对延迟不敏感)
两个接口共享同一个数据库连接池(最大连接数 20)。白歌在 Code Review 时发现一个隐患:"当批量导入历史订单时,创建接口 QPS 暴增,连接池被写操作占满,导致查询接口 RT 从 50ms 飙升至 3000ms。用户在订单确认页面卡住,开始刷屏,进一步放大问题。"
解决方案:设置关联限流——当创建订单 QPS 超过 10 时,自动限流查询接口,优先保证写操作完成。同时,查询接口配置 Warm Up 预热,防止服务重启后冷启动流量冲击。
Dashboard 配置(关联规则)
| 配置项 | 值 | 说明 |
|---|---|---|
| 资源名 | getOrder | 被限制的资源(查询接口) |
| 阈值类型 | QPS | — |
| 单机阈值 | 3 | 当关联资源 QPS > 10 时,查询接口降到 3 QPS |
| 流控模式 | 关联 | 不是 getOrder 自己触发,而是关联资源触发 |
| 关联资源 | createOrder | 当 createOrder QPS > 10 时限流 getOrder |
| 流控效果 | Warm Up | 预热模式(兼顾服务重启场景) |
| 预热时长 | 10s | 启动前 10s 阈值 = 3 / 3 = 1,线性增长到 3 |
Warm Up 效果说明
QPS │
3 ├──────────────────────────────── 全阈值
│ ╱
│ ╱
│ ╱
1 ├───────────╱ coldFactor=3,初始 QPS = 3/3 = 1
│
└────┬──────────┬────────────────► 时间
0s 10s(warmUpPeriodSec)
启动 预热完成
操作前后对比
| 状态 | 创建订单 QPS=20 时 | 查询接口情况 | 用户体验 |
|---|---|---|---|
| 操作前(无关联) | 数据库连接池占满 | RT 3000ms,几乎不可用 | 页面卡死,用户刷屏 |
| 操作后(关联限流) | 自动触发查询限流 | 查询 QPS 降到 3,RT 回到 50ms | 查询返回限流提示但可接受 |
白歌的评价:"关联限流本质上是一种优先级调度——高优先级的资源(查询接口)在低优先级资源(写接口)压力大时主动让渡资源。这种设计比无脑限流要优雅得多。"
完整示例3:热点参数限流
场景描述
飞翔科技商城有 10 万个商品,商品详情接口 GET /product/{productId} 的整体 QPS 上限设为 100。但在"618 大促"中,运营团队发现 99% 的流量集中在前 3 个爆款商品上,其中商品 ID=100(iPhone 16 Pro)的请求量是普通商品的 100 倍以上。
如果按全局 100 QPS 限流,热卖品会把额度全占满,普通商品反而被误伤。小崔意识到需要用热点参数限流——对商品 ID=100 单独设置 1 QPS 的严苛限制。
代码实现
@RestController
@RequestMapping("/product")
public class ProductController {
/**
* 商品详情接口
* @param productId 商品 ID(热点参数)
*/
@GetMapping("/{productId}")
@SentinelResource(
value = "getProductDetail",
blockHandler = "getProductDetailBlockHandler"
)
public String getProductDetail(@PathVariable Long productId) {
// 模拟查询商品详情
return "商品详情: ID=" + productId + ", 名称=iPhone 16 Pro, 库存=有限";
}
/**
* BlockHandler
* 注意:参数列表必须与原方法一致,最后加 BlockException
*/
public String getProductDetailBlockHandler(Long productId, BlockException ex) {
return "商品 " + productId + " 访问过热,请稍后再试——限流保护中";
}
}
Dashboard 配置
- 进入"热点规则"标签页,新增热点规则:
| 配置项 | 值 | 说明 |
|---|---|---|
| 资源名 | getProductDetail | 商品详情资源 |
| 参数索引 | 0 | 从 0 开始计数,productId 是方法第 0 个参数 |
| 单机阈值 | 100 | 全局默认阈值 |
| 统计窗口时长 | 1s | 1 秒内统计 |
- 在"参数例外项"中添加:
| 参数类型 | 参数值 | 限流阈值 |
|---|---|---|
| long | 100 | 1 |
操作前后对比
| 状态 | 全局 QPS | 商品 ID=100 的 QPS | 普通商品 QPS | 效果 |
|---|---|---|---|---|
| 操作前(统一限流) | 100 | 98(占满额度) | 2(被挤压) | 热卖品霸占资源 |
| 操作后(热点限流) | 100 | 1(单独限制) | 100(有余量) | 热卖品被精准打压 |
大翔在复盘时说:"热点参数限流是 Sentinel 的杀手锏功能,Hystrix 做不到。它的价值不在技术复杂度,而在于解决了一个真实的业务痛点——不是所有请求生而平等。"
完整示例4:链路限流
场景描述
飞翔科技的订单服务中有一个 OrderService.queryOrder() 方法,被两个入口调用:
- 下单入口:
OrderController.createOrder()→OrderService.queryOrder()(下单前查询库存和价格) - 管理后台入口:
AdminController.exportOrders()→OrderService.queryOrder()(批量导出订单报表)
大翔发现管理后台的导出操作非常重,一次导出可能查询数千条订单。如果不加限制,导出操作会拖慢真实用户的下单流程。但小崔不想限制管理后台本身(管理员需要导出),只想限制来自下单入口的调用量。
解决方案:使用链路限流——只对从 createOrder 入口出发的调用链路进行限流,从 exportOrders 入口出发的调用不受限。
代码实现
第一步:关闭 Context 统一
spring:
cloud:
sentinel:
web-context-unify: false # 必须关闭!否则所有 URL 共享同一个 Context
默认
web-context-unify: true会将所有 URL 入口的 Context 合并为sentinel_spring_web_context,导致链路限流失效。关闭后,每个 URL 入口拥有独立的 Context。
第二步:在 Service 方法上定义资源
@Service
public class OrderService {
/**
* 查询订单——被两个入口调用
* @SentinelResource 将此方法声明为资源 "queryOrderService"
*/
@SentinelResource("queryOrderService")
public Order queryOrder(Long orderId) {
// 数据库查询逻辑
return orderMapper.findById(orderId);
}
}
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public String createOrder(@RequestParam Long orderId) {
// 下单前查询——来自 createOrder 入口
Order order = orderService.queryOrder(orderId);
// ...
return "下单成功";
}
}
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private OrderService orderService;
@GetMapping("/export-orders")
public void exportOrders() {
// 批量导出——来自 exportOrders 入口
List<Long> ids = getAllOrderIds();
for (Long id : ids) {
Order order = orderService.queryOrder(id); // 不会被链路限流限制
}
}
}
Dashboard 配置
| 配置项 | 值 | 说明 |
|---|---|---|
| 资源名 | queryOrderService | Service 层的资源名 |
| 阈值类型 | QPS | — |
| 单机阈值 | 10 | — |
| 流控模式 | 链路 | 只限制特定入口链路的流量 |
| 入口资源 | POST:/order/create | 只限制来自 createOrder 的调用链路 |
效果验证
| 入口 | 资源 | 链路限流是否生效 | 实际 QPS |
|---|---|---|---|
POST:/order/create | queryOrderService | 是(被限制) | ≤ 10 |
GET:/admin/export-orders | queryOrderService | 否(不受限) | 无限流 |
白歌的解释:"链路限流本质上是给同一资源在不同入口设置不同的限流规则。它的底层实现依赖于 Sentinel 的 Context 机制——每个入口有独立的 Context,Context 中记录了 Entry 的调用链路。FlowSlot 判断限流时,不仅检查资源名,还检查当前 Context 的入口是否匹配 refResource。"
易错场景
错误1:blockHandler 方法签名不正确
错误写法:
@SentinelResource(value = "getOrder", blockHandler = "handleBlock")
// ❌ 错误:参数类型不匹配(缺少 Long id 参数)
public String handleBlock(BlockException ex) {
return "限流了";
}
// ❌ 错误:方法不是 public
String handleBlock(Long id, BlockException ex) {
return "限流了";
}
// ❌ 错误:BlockException 参数位置不对(必须在最后)
public String handleBlock(BlockException ex, Long id) {
return "限流了";
}
正确写法:
/**
* blockHandler 四条铁律:
* 1. 方法必须 public
* 2. 返回值类型与原方法一致
* 3. 参数列表与原方法完全一致
* 4. 最后额外加一个 BlockException 参数
*/
public String handleBlock(Long id, BlockException ex) {
return "限流了";
}
小崔踩坑实录:"有一次我在 Controller 里写 blockHandler,方法是 public 的但忘了加 static,结果限流时直接 500 报 NoSuchMethodException。排查了半小时才发现:同类的 blockHandler 必须是 static,不同类才不需要。"
错误2:Sentinel Dashboard 规则未持久化
现象:应用重启后,Dashboard 上配置的流控规则全部消失。
原因:默认规则存储在应用内存中(通过 Dashboard HTTP API 推送到 JVM 内存),重启即丢失。
解决方案:配置 Nacos DataSource 持久化规则。
spring:
cloud:
sentinel:
datasource:
flow:
nacos:
server-addr: 127.0.0.1:8848
data-id: ${spring.application.name}-flow-rules
group-id: SENTINEL_GROUP
data-type: json
rule-type: flow
Nacos 中存储的流控规则 JSON:
[
{
"resource": "getOrder",
"limitApp": "default",
"grade": 1,
"count": 3,
"strategy": 0,
"controlBehavior": 0,
"clusterMode": false
}
]
错误3:Fallback 和 BlockHandler 混淆
这是一个高频错误——两个降级方法的触发条件完全不同。
| 概念 | 触发条件 | 处理方法签名 | 典型场景 |
|---|---|---|---|
| BlockHandler | 流控/熔断触发(BlockException) | 参数 + BlockException | "请求太多,限流了" |
| Fallback | 业务异常(Throwable) | 参数 + Throwable | "下游服务挂了,降级处理" |
@SentinelResource(
value = "getOrder",
blockHandler = "handleBlock", // ← 流控/熔断 → BlockException
fallback = "handleFallback" // ← 业务异常 → Throwable
)
public String getOrder(Long id) {
// 可能抛异常的业务代码
}
// 处理流控/熔断(BlockException)
public String handleBlock(Long id, BlockException ex) {
return "Blocked by Sentinel";
}
// 处理业务异常(Throwable)
public String handleFallback(Long id, Throwable ex) {
return "Fallback: 服务暂时不可用,请稍后重试";
}
白歌总结:"记住一句话——BlockHandler 管流量,Fallback 管异常。面试时被问到区别,一定要说清楚 BlockException 是 Sentinel 自己抛的,而 Fallback 捕获的是业务代码里的 Exception。"
错误4:@SentinelResource 未配合 blockHandler
现象:限流触发后,用户看到的是 500 错误页面,而不是友好的限流提示。黄俪为此跟小崔吵过一架——"前端明明做了优雅的降级提示,结果后端直接甩了个 500,用户体验极差!"
原因:@SentinelResource 声明了资源但未指定 blockHandler,限流时 Sentinel 抛出 FlowException,Spring MVC 的默认异常处理将其转为 500。
// ❌ 错误:没有 blockHandler,限流时直接 500
@SentinelResource("getOrder")
public String getOrder(Long id) {
return "Order-" + id;
}
// ✅ 正确:指定 blockHandler
@SentinelResource(value = "getOrder", blockHandler = "handleBlock")
public String getOrder(Long id) {
return "Order-" + id;
}
public String handleBlock(Long id, BlockException ex) {
return "Blocked by Sentinel —— 限流保护中";
}
错误5:热点参数限流 paramIndex 写错
现象:配置了热点参数限流但完全不生效。
原因:paramIndex 从 0 开始计数,容易因为参数列表的写法而出错。
// 假设想对 productId 做热点限流
// ❌ 错误:@PathVariable 是第 1 个参数,paramIndex 应为 1
public String getProductDetail(@RequestParam String source,
@PathVariable Long productId) {
// paramIndex=0 实际对应的是 source,不是 productId!
}
// ✅ 正确:明确 productId 是第 1 个参数(从 0 开始),paramIndex=1
@SentinelResource(value = "getProductDetail")
public String getProductDetail(@RequestParam String source,
@PathVariable Long productId) {
// paramIndex=1 对应 productId
}
面试考点
一、Sentinel Slot Chain 的设计模式与扩展机制
考点:你了解 Sentinel 的 Slot Chain 架构吗?它用了什么设计模式?怎样扩展?
核心回答:
Sentinel 的核心处理流水线基于责任链模式(Chain of Responsibility Pattern)。每个 Slot 是一个独立的处理单元,链式串联:NodeSelectorSlot → ClusterBuilderSlot → StatisticSlot → AuthoritySlot → SystemSlot → FlowSlot → DegradeSlot → ParamFlowSlot。
每个 Slot 有两个核心方法:entry() 和 exit()。请求进入时按顺序调用各 Slot 的 entry(),退出时逆序调用 exit()。
StatisticSlot 是核心统计插槽,采用 Fire-and-Forget 模式——先调用 fireEntry() 放行后续 Slot,再统计通过数和 RT。这样即使后续 Slot 或业务逻辑抛异常,统计数据也不会丢失。
扩展机制:通过 Java SPI 机制实现 Slot 扩展。在 META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot 文件中注册自定义 Slot 的完全限定类名,Sentinel 在初始化 Slot Chain 时自动加载。开发者可以实现自定义的统计、日志、告警等 Slot 并插入到链的任意位置。
二、LeapArray 滑动窗口的低开销原理
考点:Sentinel 的 QPS 统计为何比简单计数器精确?滑动窗口如何保证低开销?
核心回答:
精度优势:简单计数器(每秒清零)在跨秒边界时精度崩溃——前 1 秒的最后 100ms + 后 1 秒的前 100ms 共 200ms 的请求被统计为 1s 的数据。LeapArray 使用环形数组 + 滑动窗口,始终覆盖最近 intervalInMs(默认 1s)内的精确数据,无边界误差。
低开销原理:
- 无锁设计:每个窗口槽位内部的计数器使用
LongAdder(高并发场景性能远超AtomicLong)和AtomicInteger,避免了全局锁。 - 环形数组复用:环形数组大小固定(默认 2 个槽位),旧槽位被直接重置复用,无 GC 压力。
- O(1) 定位:通过
(currentTimeMillis / windowLengthInMs) % sampleCount取模运算直接定位当前窗口,无遍历。 - 懒加载:窗口槽位仅在首次访问时创建,未使用的槽位不占用内存。
面试加分项:提到 StatisticSlot 的统计时机在业务逻辑执行之后(finally 块),确保 RT 统计包含完整的业务执行时间。
三、预热(Warm Up)的数学基础
考点:Sentinel 的 Warm Up 是基于令牌桶算法还是漏桶算法?
核心回答:
Sentinel 的 Warm Up 基于令牌桶算法(Token Bucket Algorithm)的变体,借鉴了 Guava 的 SmoothWarmingUp 实现。
- 令牌桶算法:以恒定速率生成令牌放入桶中,请求需要获取令牌才能通过。桶满后令牌溢出丢弃。允许突发流量(短时间内可以消耗桶中积累的令牌)。
- 漏桶算法:请求进入漏桶,以恒定速率流出。不允许突发流量(即使桶空了请求也必须按速率流出)。
Sentinel 的 Warm Up 在令牌桶基础上增加了预热因子(coldFactor)——启动时令牌生成速率为正常速率的 1/coldFactor,在预热期内线性增长到正常速率。流量整形曲线是梯形而非标准令牌桶的矩形,因此能防止冷启动瞬间的流量突发。
追问"Sentinel 能不能用漏桶实现":排队等待(Rate Limiter)模式更接近漏桶思想——固定间隔放行请求。但 Sentinel 的排队等待底层用的是改良的信号量 + 等待时间计算,并非传统漏桶。
四、热点参数限流与 QPS 限流的关系
考点:如果同时配置了 QPS 流控和热点参数限流,哪个先生效?
核心回答:
在 Sentinel 的 Slot Chain 中,FlowSlot 排在 ParamFlowSlot 之前,所以QPS 流控先于热点参数限流判定。但热点参数限流的内部逻辑是:
- 先检查热点参数例外:如果当前参数值匹配例外项(如商品 ID=100),按例外阈值判定
- 再检查全局 QPS:如果参数值不匹配任何例外项,按热点规则的全局阈值判定
- 最后检查 FlowRule:如果热点规则未限流,再走 FlowSlot 的 QPS 规则
所以"热点参数限流优先于 QPS 限流"指的是在 FlowSlot 内部的热点检查逻辑中,参数例外项优先于全局阈值。
五、系统自适应保护规则的触发条件
考点:Sentinel 的系统规则有哪些触发条件?它们适用于什么场景?
核心回答:
系统规则从应用实例整体维度保护,共五种触发条件:
| 条件 | 指标来源 | Windows 支持 | 适用场景 |
|---|---|---|---|
| Load(系统负载) | OperatingSystemMXBean.getSystemLoadAverage() | ❌ 不可用 | Linux 服务器整体负载保护 |
| RT(平均响应时间) | 所有入口流量的平均 RT | ✅ | 防止 RT 恶化级联 |
| 线程数 | 所有入口的并发线程数 | ✅ | 防止线程池耗尽 |
| 入口 QPS | 所有入口的 QPS 总和 | ✅ | 整体兜底限流 |
| CPU 使用率 | JDK 9+ getCpuLoad() | ✅ | 精确 CPU 保护 |
触发逻辑:任一条件达到阈值即触发限流(OR 关系),而不是全部条件同时满足。因为 Sentinel 认为任何一个维度亮红灯都说明系统已经不堪重负。
面试追问"为什么 Windows 上 Load 不可用":Java 的
OperatingSystemMXBean.getSystemLoadAverage()在 Windows 上始终返回 -1,这是 JDK 的已知限制。Windows 上建议用 CPU 使用率替代。
六、Sentinel 与 Hystrix 在隔离机制上的本质区别
考点:Sentinel 和 Hystrix 的隔离机制有什么本质区别?为什么 Sentinel 更轻量?
核心回答:
| 维度 | Hystrix | Sentinel |
|---|---|---|
| 隔离方式 | 线程池隔离(默认)+ 信号量隔离 | 信号量隔离(基于并发线程数统计) |
| 原理 | 每个依赖分配独立线程池,调用在独立线程中执行 | 统计当前资源占用的线程数,超过阈值则拒绝 |
| 额外开销 | 线程上下文切换、线程池管理、Queue 排队 | 仅一个 AtomicInteger 计数器 |
| 隔离强度 | 强隔离(完全物理隔离,慢调用不会影响其他资源) | 软隔离(无法阻止慢调用占用 CPU,但能防止线程耗尽) |
| 适用场景 | 对隔离性要求极高的场景(如调用不可信第三方) | 大多数微服务内部调用场景 |
Hystrix 的线程池模式每次调用都涉及线程切换(~10μs 额外开销 + 内存开销,每个线程池约 1MB 栈内存),在 QPS 较高的微服务内部调用场景下,这种开销不可忽视。Sentinel 的信号量隔离只做"线程数加减"的轻量操作(~0.1μs),更适合同一公司内部微服务间的流量保护。
本质区别:Hystrix 追求的是物理隔离(降级目标资源本身),Sentinel 追求的是流量控制(控制进入目标资源的流量速率)。这是两种不同的设计哲学——大翔总结为"Hystrix 像防火墙(隔离),Sentinel 像交通管制(疏导)。"
参考资源:
- Sentinel 官方文档:https://sentinelguard.io/zh-cn/docs/introduction.html
- Sentinel GitHub:https://github.com/alibaba/Sentinel
- Spring Cloud Alibaba Sentinel:https://sca.aliyun.com/docs/2025/user-guide/sentinel/overview/