Skip to content

Sentinel 限流与熔断 ​

S4-022建议学高级约 60 分钟JDK 21企业开发进阶 · 微服务

1. 本节定位 ​

《Nacos 注册与配置中心》讲注册与配置;本节讲**"依赖开始失败时,怎么不被拖死"**。

★ 本节的实测会复现微服务最经典的故障模式 ✓✓:

text
下游从 10 ms 恶化到 2000 ms,上游只有 20 个工作线程:

① 不熔断:提交 1000 个请求 → 成功 0 / 失败 80 / **超时未完成 959** ✗✗
② 有熔断:提交 1000 个请求 → 成功 0 / 失败 24 / **降级 976 / 超时未完成 0** ✓✓
           降级路径平均耗时:**0.000 ms**(几乎瞬时)✓✓

★ 差别就是"959 个请求从卡死变成立即拿到兜底" ✓✓

2. 学习目标 ​

  • 能说清"雪崩"的完整链路 ★★★;
  • 能说出熔断做的三件事,以及"不是放弃用户"的正确理解 ★★★;
  • 能背出熔断器三个状态的行为与进入条件 ★★★;
  • 知道"半开阶段放太多请求"会引发熔断振荡 ★★★;
  • 能说出 Sentinel 的四种降级策略与四种兜底动作 ★★★;
  • 知道规则必须持久化到 Nacos,否则重启就丢 ★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看"雪崩"的完整链路 ✗✗:

text
① 库存服务开始变慢(10 ms → 2000 ms)—— **它并没有挂,只是慢** ⚠️
② 订单服务的 20 个线程全部阻塞在对库存服务的调用上 ✗
③ 新的下单请求进不来(线程池满)✗
④ 订单服务的健康检查也超时 → **被注册中心摘除** ✗✗
⑤ 网关把流量转给其他订单实例 → **其他实例也被拖慢** ✗✗
⑥ …… 最终整个下单链路不可用 ✗✗✗

★ 本节的实测就是①②③ ✓ —— 特别注意:全程下游"只是慢",一个请求都没失败过(是我们自己超时放弃的) ✓

5. 核心概念 ​

5.1 雪崩:一个下游变慢,拖死整条链路 ★★★ ​

实测 ✓✓:

text
  下游延迟:2000 ms(正常应是 10 ms)| 上游线程池:20 个线程
  提交 1000 个请求,6 秒内:成功 0 / 失败 80 / 超时未完成 959
  下游共被调用 80 次,其中超时 80 次

★ 三个数字分别说明什么 ✓✓:

数字说明
超时未完成 959 ✗✗它们既没成功也没失败,而是"卡着" ——而这正是最糟的状态 ✓
下游只被调用了 80 次 ⚠️6 秒里只发出了 80 个请求(20 线程 × 4 批)——吞吐断崖式下跌 ✗
成功 0 ✗✗所有请求都失败了(要么超时,要么被 6 秒的等待截断)

★★ 为什么"卡着"比"失败"更糟 ✓✓:

失败是明确的(调用方立刻知道,可以重试或降级)✓

卡着是模糊的 ——它占着线程、占着连接、占着内存,而且调用方什么都做不了 ✗✗

更糟的是:这 959 个请求会把上游的线程池、连接池全部占满, 于是连"跟库存无关的接口"也一起不可用了 ✗✗

5.2 熔断:停止调用 + 快速失败 + 返回兜底 ★★★ ​

实测 ✓✓:

text
  提交 1000 个请求:成功 0 / 失败 24 / **降级 976** / 超时未完成 0
  下游共被调用 24 次(第 1 节不熔断时是 80 次)
  降级路径平均耗时:0.000 ms(几乎瞬时)✓✓

★★ 熔断做的三件事(顺序很重要) ✓✓:

#动作作用
①停止调用 ★★★不再往下游发请求 → 给它恢复的机会 ✓✓
②快速失败 ★★★立刻返回,不占上游线程 ✓✓
③返回兜底 ★★★用户看到"暂时不可用",而不是"转圈 100 秒" ✓✓

★★★ 一条关键认识 ✓✓:

熔断不是"放弃用户",而是"牺牲局部保住整体" ✓✓

宁可 976 个请求快速拿到兜底(还能继续用别的功能), 也不要 1000 个请求全部卡死 ✗✗

而且第 ① 步对下游更是救命:下游在恢复期最怕的就是持续被压 ✓✓

5.3 熔断器状态机:三个状态与三个参数 ★★★ ​

实测 ✓✓:

text
  初始状态:CLOSED(正常放行)✓
  第 1 次失败后:状态 CLOSED
  第 2 次失败后:状态 CLOSED
  第 3 次失败后:状态 OPEN             ← 达到阈值(3 次)
  此刻再试着调用:拒绝(快速失败,不调用下游)✗
  等待 1100 ms 后再试:允许,状态变为 HALF_OPEN  ✓
  试探成功后:状态 CLOSED(恢复放行)✓✓
  半开试探失败后:状态 OPEN(进入下一轮等待)✗

★ 三个状态的职责(这张表要背下来) ✓✓:

状态行为什么时候进入
CLOSED ★★★正常放行,统计失败率初始 / 试探成功 ✓
OPEN ★★★一律快速失败,不调下游失败率达到阈值 ✗
HALF_OPEN ★★★放少量请求试探OPEN 持续了一段时间 ✓

★ 三个参数决定它的行为 ✓:

参数含义调大的后果
失败阈值 ★★多少失败后打开故障扩散更久才被挡住 ✗
打开时长 ★★多久后允许试探下游恢复后,用户多等一段时间 ✗
半开请求数 ★★★一次放几个试探放太多会把刚恢复的下游再打挂 ✗✗

★★★ 第三个参数最容易出事故 ✓✓:

下游刚从故障中恢复时最脆弱(缓存是空的、连接池是冷的)✗

这时放大量请求过去 → 立刻又被打挂 → 陷入"熔断-恢复-再熔断"的振荡 ✗✗

所以半开阶段要慢慢放量(先用单实例试探,再逐步恢复权重)✓✓

5.4 滑动窗口:为什么 Sentinel 不用固定窗口 ★★★ ​

实测 ✓:

text
  滑动窗口(10 个 100 ms 格子,限额 50/秒):突发的 200 次请求
    放行 50 次 / 拦截 150 次 ✓✓

★ 为什么是"滑动" ✓✓:

固定窗口在窗口切换的瞬间会放行两倍流量 ✗✗ (前一个窗口的残留 + 新窗口的额度)

滑动窗口始终看「最近 1 秒」,所以能精确控制 ✓✓

(这也是 Sentinel 用 WindowLeapArray 实现的原因)✓

★ 与 《Spring Cloud 核心组件》 的网关层限流是同一个道理 ✓ —— 区别只在"限在哪里":网关限入口,Sentinel 限服务出口/方法级 ✓

5.5 四种降级策略与四种兜底动作 ★★★ ​

★ Sentinel 的四种触发策略 ✓:

策略触发条件适用
慢调用比例 SlowRequestRatio ★★★慢调用占比超阈值下游只是变慢(最常用)✓✓
异常比例 ExceptionRatio ★★异常占比超阈值下游大量报错 ✓
异常数 ExceptionCount ★★异常数超阈值绝对数量敏感的场景 ✓
限流(QPS / 线程数) ★★★超过阈值保护自己不被压垮 ✓✓

★ 四种兜底动作(按体验从好到差) ✓✓:

动作例子体验
返回缓存 / 默认值 ★★★推荐位显示热门榜单几乎无感 ✓✓
静默忽略 ★★★加积分失败只记日志用户无感 ✓✓
返回兜底文案 ★★「库存查询暂不可用」有感知但可用 ✓
快速失败 ★★抛异常给上层最差,但保住了上游 ✓

★★★ 一条设计原则 ✓✓:

降级方案必须"业务方参与设计",不能由技术单方面决定 ✓✓

「下单时库存服务挂了,是拒绝下单还是允许超卖?」这是业务问题,不是技术问题 ✓

(呼应 《微服务架构与拆分原则》 的"核心/非核心依赖标注")✓✓

5.6 Sentinel 的两个实践要点 ★★★ ​

★ 要点一:规则必须持久化 ✓✓:

规则默认存在内存里,重启就没了 ✗✗

生产环境必须把规则持久化到 Nacos(《Nacos 注册与配置中心》), 否则每次发布规则都回退到默认值 ✗

★ 要点二:熔断与限流要"组合"用 ✓✓:

手段保护什么
限流 ★★★保护自己不被压垮(入口)✓
熔断 ★★★保护自己不被下游拖死(出口)✓✓
降级 ★★★是两者的共同出口(都要有兜底)✓

★ 四个限流粒度 ✓:

粒度说明
接口级 ★★★每个方法一个资源名(最常用)✓✓
方法级 ★更细的资源名(粒度太细难维护)⚠️
来源级 ★★按调用方标识(origin)✓
热点参数 ★★★按参数值限流(秒杀抢购用它)✓✓

6. 递进示例 ​

6.1 最小示例:Sentinel 的注解写法 ​

java
@SentinelResource(value = "queryStock",
        blockHandler = "queryStockBlocked",      // ★ 被限流/熔断时走这里
        fallback = "queryStockFallback")         // ★ 业务异常时走这里
public Stock queryStock(Long productId) {
    return stockClient.get(productId);
}

// 被限流或熔断 → 快速返回兜底(不调下游)
public Stock queryStockBlocked(Long productId, BlockException e) {
    log.warn("库存查询被熔断/限流:{}", e.getClass().getSimpleName());
    return Stock.unknown(productId);
}

★ 两个回调的区别(容易搞混) ✓✓:

回调什么时候触发
blockHandler ★★★被 Sentinel 拦截(限流、熔断、系统保护)✓
fallback ★★业务方法抛异常(下游报错、超时)✓

6.2 业务示例:熔断规则持久化到 Nacos ​

java
@PostConstruct
public void initRules() {
    // 从 Nacos 读规则(改规则不用重启)
    ReadableDataSource<String, List<DegradeRule>> ds = new NacosDataSource<>(
            serverAddr, groupId, dataId,
            source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>() {}));
    DegradeRuleManager.register2Property(ds.getProperty());
}

6.3 反例:熔断后直接抛异常 ​

java
// ✗ 快速失败但没兜底 → 用户体验和"卡死"差不多(都是失败)
catch (BlockException e) { throw new RuntimeException(e); }

// ✓ 有兜底 → 用户还能看到"暂时不可用",其他功能不受影响
catch (BlockException e) { return Stock.unknown(productId); }

7. 完整可运行代码与实测 ​

代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/CircuitBreakerLesson.java

运行:

powershell
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.micro.CircuitBreakerLesson

实测输出(关键部分) ✓:

text
===== 1. 不熔断:下游变慢,把上游线程池占满(雪崩)=====
  下游延迟:2000 ms(正常应是 10 ms)| 上游线程池:20 个线程
  提交 1000 个请求,6 秒内:成功 0 / 失败 80 / 超时未完成 959 ✗✗
  下游共被调用 80 次,其中超时 80 次

===== 2. 有熔断:快速失败 + 降级 =====
  提交 1000 个请求:成功 0 / 失败 24 / **降级 976** / 超时未完成 0 ✓✓
  降级路径平均耗时:0.000 ms(几乎瞬时)✓✓

===== 3. 熔断器状态机 =====
  初始状态:CLOSED ✓
  第 3 次失败后:状态 OPEN
  此刻再试着调用:拒绝(快速失败,不调用下游)✗
  等待 1100 ms 后再试:允许,状态变为 HALF_OPEN ✓
  试探成功后:状态 CLOSED(恢复放行)✓✓
  半开试探失败后:状态 OPEN(进入下一轮等待)✗

★ 三条可落地的结论 ✓:

#结论依据
①"卡着"比"失败"更糟 ——必须让失败快速发生959 个超时未完成 ✗✗
②熔断把"卡死"变成"立即兜底"976 个降级,0 个未完成 ✓✓
③熔断器要配半开探测,且半开要慢慢放量状态机实测 ✓

8. Debug 观察点 ​

  1. 把上游线程池从 20 改成 200:观察"雪崩被推迟,但没有消失" ✗(只是需要更久)✓;
  2. 把熔断阈值从 5 改成 50:观察故障扩散更久才被挡住 ✗;
  3. 把半开的试探请求数调大:观察熔断振荡(恢复→被打挂→再熔断)✗✗;
  4. 去掉兜底,只做快速失败:观察用户体验没改善多少 ⚠️;
  5. 让下游恢复后观察熔断器是否自动关闭:验证半开机制 ✓✓。

9. 常见错误 ​

#错误说明
1只配超时不配熔断 ✗✗超时能防卡死,但不防"持续被打" ✓
2熔断后不给兜底 ✗✗用户体验与卡死差不多 ✓
3半开一次放太多请求 ✗✗熔断振荡 ✓✓
4规则不持久化 ✗✗重启就丢 ✓
5liveness 探针查依赖 ✗✗依赖抖动 → 所有实例一起重启(雪崩)✓✓
6在熔断回调里做重活 ✗它必须"快速返回" ✓
7降级方案由技术单方面决定 ✗✗「拒绝下单还是允许超卖」是业务问题 ✓✓

10. 动手实验 ​

目标:亲眼看到雪崩与熔断的差别。

步骤:

  1. 跑 CircuitBreakerLesson,记录两组数字(959 vs 0)✓✓;
  2. 把线程池改小(5 个线程):观察雪崩更早发生 ✓;
  3. 把熔断阈值改成 1:观察"过于敏感"(偶发失败就熔断)⚠️;
  4. 给一个真实接口加熔断 + 兜底,用故障注入验证兜底生效 ★★★;
  5. 把规则配到 Nacos 并重启服务,验证规则还在 ✓✓。

验收方式:能说出"你的哪个依赖最可能引发雪崩、熔断阈值该配多少"。

扩展任务:设计一个"热点参数限流"方案(秒杀场景):

  • 同一个商品 ID 的请求限速(ParamFlowRule)✓✓;
  • 不同商品配额不同(热门商品配额高,防止误伤)✓;
  • 说明:为什么"按接口限流"在秒杀场景不够(提示:一个热点商品会吃掉整个接口的配额)★★

11. 练习题 ​

  1. 判断:下游服务只是变慢(没报错),不会引发故障。(对 / 错)
  2. 填空:熔断器三个状态是 、、______。
  3. 简答:为什么"超时"不能替代"熔断"?
  4. (场景判断)
text
某接口的超时配置从 1 秒改成 10 秒,
理由是"有些请求确实需要 8 秒"。

这个改动有什么风险? 5. (设计)给"商品详情页"设计熔断降级方案:要查库存、评价、推荐,要求推荐挂了页面也能开。

参考答案(要点)
  1. 错。"变慢"是最危险的故障形式 ✗✗ —— 它会占满上游的线程/连接,导致连无关接口也不可用 ✓✓★
  2. CLOSED;OPEN;HALF_OPEN ✓
  3. 超时只限制"单个请求等多久",但不阻止"持续往下游发请求" ✗ —— 下游会被持续压着(无法恢复),而上游的线程仍然被"每请求占 1 秒"消耗 ✗✗ —— 熔断会直接停止调用,给下游恢复的机会 ✓✓★
  4. 风险是"线程占用时间 × 10 倍" ✗✗ —— 原本 1 秒就释放的线程,现在要占 10 秒 → 并发容量直接降到 1/10 ✗✗ —— 正确做法:给这个慢接口单独配超时与熔断,而不是全局放宽 ✓✓★
  5. 要点:库存是核心(熔断 + 快速失败)✓; 评价可降级为"暂无评价" ✓;推荐降级为兜底内容 ✓✓; 三者并行调用,总延迟取最大值 ✓;降级文案要让用户理解(不是报错)✓

12. 掌握标准 ​

  • [ ] 能画出雪崩的完整链路(6 步) ★★★;
  • [ ] 知道"卡着"比"失败"更糟的原因 ★★★;
  • [ ] 能说出熔断的三件事与"牺牲局部保住整体" ★★★;
  • [ ] 能背出三个状态的行为与进入条件 ★★★;
  • [ ] 知道半开放太多会引发熔断振荡 ★★★;
  • [ ] 能说出四种降级策略与四种兜底动作 ★★★;
  • [ ] 知道规则必须持久化到 Nacos ★★★;
  • [ ] 知道限流与熔断要组合使用 ★★★。

13. 扩展资源 ​

14. 下一步学习 ​

《Seata 分布式事务》:跨服务操作怎么保证最终一致。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引