主题
界面
主题
《Nacos 注册与配置中心》讲注册与配置;本节讲**"依赖开始失败时,怎么不被拖死"**。
★ 本节的实测会复现微服务最经典的故障模式 ✓✓:
下游从 10 ms 恶化到 2000 ms,上游只有 20 个工作线程:
① 不熔断:提交 1000 个请求 → 成功 0 / 失败 80 / **超时未完成 959** ✗✗
② 有熔断:提交 1000 个请求 → 成功 0 / 失败 24 / **降级 976 / 超时未完成 0** ✓✓
降级路径平均耗时:**0.000 ms**(几乎瞬时)✓✓★ 差别就是"959 个请求从卡死变成立即拿到兜底" ✓✓
先看"雪崩"的完整链路 ✗✗:
① 库存服务开始变慢(10 ms → 2000 ms)—— **它并没有挂,只是慢** ⚠️
② 订单服务的 20 个线程全部阻塞在对库存服务的调用上 ✗
③ 新的下单请求进不来(线程池满)✗
④ 订单服务的健康检查也超时 → **被注册中心摘除** ✗✗
⑤ 网关把流量转给其他订单实例 → **其他实例也被拖慢** ✗✗
⑥ …… 最终整个下单链路不可用 ✗✗✗★ 本节的实测就是①②③ ✓ —— 特别注意:全程下游"只是慢",一个请求都没失败过(是我们自己超时放弃的) ✓
实测 ✓✓:
下游延迟:2000 ms(正常应是 10 ms)| 上游线程池:20 个线程
提交 1000 个请求,6 秒内:成功 0 / 失败 80 / 超时未完成 959
下游共被调用 80 次,其中超时 80 次★ 三个数字分别说明什么 ✓✓:
| 数字 | 说明 |
|---|---|
| 超时未完成 959 ✗✗ | 它们既没成功也没失败,而是"卡着" ——而这正是最糟的状态 ✓ |
| 下游只被调用了 80 次 ⚠️ | 6 秒里只发出了 80 个请求(20 线程 × 4 批)——吞吐断崖式下跌 ✗ |
| 成功 0 ✗✗ | 所有请求都失败了(要么超时,要么被 6 秒的等待截断) |
★★ 为什么"卡着"比"失败"更糟 ✓✓:
失败是明确的(调用方立刻知道,可以重试或降级)✓
卡着是模糊的 ——它占着线程、占着连接、占着内存,而且调用方什么都做不了 ✗✗
更糟的是:这 959 个请求会把上游的线程池、连接池全部占满, 于是连"跟库存无关的接口"也一起不可用了 ✗✗
实测 ✓✓:
提交 1000 个请求:成功 0 / 失败 24 / **降级 976** / 超时未完成 0
下游共被调用 24 次(第 1 节不熔断时是 80 次)
降级路径平均耗时:0.000 ms(几乎瞬时)✓✓★★ 熔断做的三件事(顺序很重要) ✓✓:
| # | 动作 | 作用 |
|---|---|---|
| ① | 停止调用 ★★★ | 不再往下游发请求 → 给它恢复的机会 ✓✓ |
| ② | 快速失败 ★★★ | 立刻返回,不占上游线程 ✓✓ |
| ③ | 返回兜底 ★★★ | 用户看到"暂时不可用",而不是"转圈 100 秒" ✓✓ |
★★★ 一条关键认识 ✓✓:
熔断不是"放弃用户",而是"牺牲局部保住整体" ✓✓
宁可 976 个请求快速拿到兜底(还能继续用别的功能), 也不要 1000 个请求全部卡死 ✗✗
而且第 ① 步对下游更是救命:下游在恢复期最怕的就是持续被压 ✓✓
实测 ✓✓:
初始状态:CLOSED(正常放行)✓
第 1 次失败后:状态 CLOSED
第 2 次失败后:状态 CLOSED
第 3 次失败后:状态 OPEN ← 达到阈值(3 次)
此刻再试着调用:拒绝(快速失败,不调用下游)✗
等待 1100 ms 后再试:允许,状态变为 HALF_OPEN ✓
试探成功后:状态 CLOSED(恢复放行)✓✓
半开试探失败后:状态 OPEN(进入下一轮等待)✗★ 三个状态的职责(这张表要背下来) ✓✓:
| 状态 | 行为 | 什么时候进入 |
|---|---|---|
| CLOSED ★★★ | 正常放行,统计失败率 | 初始 / 试探成功 ✓ |
| OPEN ★★★ | 一律快速失败,不调下游 | 失败率达到阈值 ✗ |
| HALF_OPEN ★★★ | 放少量请求试探 | OPEN 持续了一段时间 ✓ |
★ 三个参数决定它的行为 ✓:
| 参数 | 含义 | 调大的后果 |
|---|---|---|
| 失败阈值 ★★ | 多少失败后打开 | 故障扩散更久才被挡住 ✗ |
| 打开时长 ★★ | 多久后允许试探 | 下游恢复后,用户多等一段时间 ✗ |
| 半开请求数 ★★★ | 一次放几个试探 | 放太多会把刚恢复的下游再打挂 ✗✗ |
★★★ 第三个参数最容易出事故 ✓✓:
下游刚从故障中恢复时最脆弱(缓存是空的、连接池是冷的)✗
这时放大量请求过去 → 立刻又被打挂 → 陷入"熔断-恢复-再熔断"的振荡 ✗✗
所以半开阶段要慢慢放量(先用单实例试探,再逐步恢复权重)✓✓
实测 ✓:
滑动窗口(10 个 100 ms 格子,限额 50/秒):突发的 200 次请求
放行 50 次 / 拦截 150 次 ✓✓★ 为什么是"滑动" ✓✓:
固定窗口在窗口切换的瞬间会放行两倍流量 ✗✗ (前一个窗口的残留 + 新窗口的额度)
滑动窗口始终看「最近 1 秒」,所以能精确控制 ✓✓
(这也是 Sentinel 用
WindowLeapArray实现的原因)✓
★ 与 《Spring Cloud 核心组件》 的网关层限流是同一个道理 ✓ —— 区别只在"限在哪里":网关限入口,Sentinel 限服务出口/方法级 ✓
★ Sentinel 的四种触发策略 ✓:
| 策略 | 触发条件 | 适用 |
|---|---|---|
| 慢调用比例 SlowRequestRatio ★★★ | 慢调用占比超阈值 | 下游只是变慢(最常用)✓✓ |
| 异常比例 ExceptionRatio ★★ | 异常占比超阈值 | 下游大量报错 ✓ |
| 异常数 ExceptionCount ★★ | 异常数超阈值 | 绝对数量敏感的场景 ✓ |
| 限流(QPS / 线程数) ★★★ | 超过阈值 | 保护自己不被压垮 ✓✓ |
★ 四种兜底动作(按体验从好到差) ✓✓:
| 动作 | 例子 | 体验 |
|---|---|---|
| 返回缓存 / 默认值 ★★★ | 推荐位显示热门榜单 | 几乎无感 ✓✓ |
| 静默忽略 ★★★ | 加积分失败只记日志 | 用户无感 ✓✓ |
| 返回兜底文案 ★★ | 「库存查询暂不可用」 | 有感知但可用 ✓ |
| 快速失败 ★★ | 抛异常给上层 | 最差,但保住了上游 ✓ |
★★★ 一条设计原则 ✓✓:
降级方案必须"业务方参与设计",不能由技术单方面决定 ✓✓
「下单时库存服务挂了,是拒绝下单还是允许超卖?」这是业务问题,不是技术问题 ✓
(呼应 《微服务架构与拆分原则》 的"核心/非核心依赖标注")✓✓
★ 要点一:规则必须持久化 ✓✓:
规则默认存在内存里,重启就没了 ✗✗
生产环境必须把规则持久化到 Nacos(《Nacos 注册与配置中心》), 否则每次发布规则都回退到默认值 ✗
★ 要点二:熔断与限流要"组合"用 ✓✓:
| 手段 | 保护什么 |
|---|---|
| 限流 ★★★ | 保护自己不被压垮(入口)✓ |
| 熔断 ★★★ | 保护自己不被下游拖死(出口)✓✓ |
| 降级 ★★★ | 是两者的共同出口(都要有兜底)✓ |
★ 四个限流粒度 ✓:
| 粒度 | 说明 |
|---|---|
| 接口级 ★★★ | 每个方法一个资源名(最常用)✓✓ |
| 方法级 ★ | 更细的资源名(粒度太细难维护)⚠️ |
| 来源级 ★★ | 按调用方标识(origin)✓ |
| 热点参数 ★★★ | 按参数值限流(秒杀抢购用它)✓✓ |
@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 ★★ | 业务方法抛异常(下游报错、超时)✓ |
@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());
}// ✗ 快速失败但没兜底 → 用户体验和"卡死"差不多(都是失败)
catch (BlockException e) { throw new RuntimeException(e); }
// ✓ 有兜底 → 用户还能看到"暂时不可用",其他功能不受影响
catch (BlockException e) { return Stock.unknown(productId); }代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/CircuitBreakerLesson.java
运行:
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.micro.CircuitBreakerLesson实测输出(关键部分) ✓:
===== 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 个未完成 ✓✓ |
| ③ | 熔断器要配半开探测,且半开要慢慢放量 | 状态机实测 ✓ |
| # | 错误 | 说明 |
|---|---|---|
| 1 | 只配超时不配熔断 ✗✗ | 超时能防卡死,但不防"持续被打" ✓ |
| 2 | 熔断后不给兜底 ✗✗ | 用户体验与卡死差不多 ✓ |
| 3 | 半开一次放太多请求 ✗✗ | 熔断振荡 ✓✓ |
| 4 | 规则不持久化 ✗✗ | 重启就丢 ✓ |
| 5 | liveness 探针查依赖 ✗✗ | 依赖抖动 → 所有实例一起重启(雪崩)✓✓ |
| 6 | 在熔断回调里做重活 ✗ | 它必须"快速返回" ✓ |
| 7 | 降级方案由技术单方面决定 ✗✗ | 「拒绝下单还是允许超卖」是业务问题 ✓✓ |
目标:亲眼看到雪崩与熔断的差别。
步骤:
CircuitBreakerLesson,记录两组数字(959 vs 0)✓✓;验收方式:能说出"你的哪个依赖最可能引发雪崩、熔断阈值该配多少"。
扩展任务:设计一个"热点参数限流"方案(秒杀场景):
ParamFlowRule)✓✓;某接口的超时配置从 1 秒改成 10 秒,
理由是"有些请求确实需要 8 秒"。这个改动有什么风险? 5. (设计)给"商品详情页"设计熔断降级方案:要查库存、评价、推荐,要求推荐挂了页面也能开。
《Seata 分布式事务》:跨服务操作怎么保证最终一致。