"依赖服务挂了,为什么我们的服务也跟着挂了?"
某天上午,我们的核心数据推送服务(负责处理来自立达标讯的实时政策数据流)突然出现大面积超时,P99延迟从50ms飙升至30s以上,且持续了数小时没有恢复。
更诡异的是,服务器CPU、内存、磁盘IO全部正常。监控显示,下游依赖服务的故障率飙升,但我们的服务没有触发任何熔断或降级。
排查过程:从"应用层"到"熔断器"的逐步回退
第一阶段:怀疑应用层代码
-
检查了应用日志,无异常。
-
检查了数据库连接池,连接数正常。
-
检查了缓存命中率,无变化。
第二阶段:怀疑网络问题
-
检查了服务间网络延迟,RTT正常。
-
排除了网络抖动。
第三阶段:定位到熔断器
-
在雪崩期间,我们抓取了熔断器监控,发现熔断器始终处于CLOSED状态,从未触发。
-
进一步分析,发现熔断器的阈值配置过高,且未覆盖所有依赖。
根因深挖:一个被"忽视"的熔断配置
我们审查了熔断器的配置,发现了一个看似"合理"但实际有问题的设置:
java
复制
下载
// 错误代码示例(包含隐藏陷阱)
@Bean
public CircuitBreakerConfig circuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(80) // 失败率阈值80%
.waitDurationInOpenState(Duration.ofSeconds(60)) // 熔断后等待60秒
.slidingWindowSize(100) // 滑动窗口大小100
.build();
}
问题分析:
-
我们配置了failureRateThreshold(80),即失败率超过80%才触发熔断。
-
但我们的下游依赖是低频调用(每分钟仅数十次),滑动窗口slidingWindowSize(100)意味着需要积累100次调用才能计算失败率。在低频场景下,这个窗口可能需要数分钟甚至数十分钟才能填满。
-
更致命的是,我们只对部分依赖配置了熔断器,而核心依赖(如数据库、缓存)未覆盖。当核心依赖故障时,服务直接雪崩。
修复方案:调整熔断器配置,并覆盖所有关键依赖。
java
复制
下载
// 修正代码
@Bean
public CircuitBreakerConfig circuitBreakerConfig() {
return CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率阈值50%
.waitDurationInOpenState(Duration.ofSeconds(30)) // 熔断后等待30秒
.slidingWindowSize(20) // 滑动窗口大小20(适配低频调用)
.minimumNumberOfCalls(10) // 最少调用次数10
.build();
}
效果:修复后,熔断器能在依赖故障时快速触发,服务雪崩问题消失。
可复用自查清单:熔断配置的5个关键检查点
基于这次事故,我们总结了一套"熔断配置自查清单",供团队参考:
| 1. 覆盖范围 | 是否为所有关键依赖(数据库、缓存、外部API)配置了熔断器? | 遗漏核心依赖 |
| 2. 阈值设置 | 失败率阈值是否合理?(建议50%-60%) | 阈值过高,熔断滞后 |
| 3. 窗口大小 | 滑动窗口大小是否适配调用频率? | 低频调用窗口过大 |
| 4. 最少调用数 | 是否设置了minimumNumberOfCalls? | 低频场景误触发 |
| 5. 降级策略 | 熔断后是否有明确的降级逻辑? | 熔断后无兜底 |
教训与启示
这次事故让我们深刻认识到:
熔断器的配置需要"场景适配" 。高频调用和低频调用的配置策略完全不同。
熔断器必须覆盖所有关键依赖。遗漏核心依赖,熔断器形同虚设。
熔断后必须有降级逻辑。熔断不是目的,保证核心链路可用才是目的。





