常见坑:超时链、重试风暴、慢调用放大
微服务故障很少是单点“炸”出来的,多数是调用链上的小问题被层层放大。本章讲三个最常见的故障放大器——超时链、重试风暴、慢调用放大,以及对应的预防手段。
坑一:超时链——每一跳都超时,整体雪崩
服务 A 调 B、B 调 C,每层超时 1 秒:A 对用户的响应上限其实是“A→B 超时 + B→C 超时”的累加。链路越长,最坏延迟线性叠加;更糟的是默认配置常常是“永不超时”,下游挂了上游就无限等待,线程全部挂起。
// 反例:不设超时的 HTTP/Feign 调用,下游无响应时线程被永久占用
// 正例:显式设置连接超时与读取超时,如 Feign 客户端配置
@Configuration
public class FeignConfig {
@Bean
public Request.Options feignOptions() {
return new Request.Options(
/* connectTimeoutMillis */ 1000,
/* readTimeoutMillis */ 3000);
}
}
预防清单:
- 所有跨服务调用必须显式设超时:连接超时短、读取超时适中,两者分开。
- 网关入口设整体超时与单路由超时,避免请求在网关层无限挂起。
- 超时值沿链路“只减不增”:外层超时 ≥ 内层超时之和要留余量,内层别比外层还大。
- 超时后的默认行为必须定义:快速失败返回降级结果,而不是把超时当成功继续。
坑二:重试风暴——一次失败,放大 N 倍流量
“失败就重试”看起来无害,但多级重试会指数放大:A 对 B 重试 3 次,B 对 C 也重试 3 次,一次原始失败最多触发 9 次调用;如果网关层、负载均衡层再各重试一次,瞬间流量可能是平时的几十倍,把已经故障的下游彻底打垮。
# 反例叠加:网关重试 + Feign 重试 + 业务层自写重试
# 正例:全局统一重试策略,明确“谁负责重试”,且只重试幂等请求
resilience4j:
retry:
instances:
stockService:
max-attempts: 2 # 一层最多 1 次重试
wait-duration: 200ms
预防清单:
- 全链路只在一层做重试(通常是最贴近故障源的调用方),其他层关闭或仅网关入口兜底。
- 只对“临时失败”重试:超时、连接异常、5xx 可重试;4xx、业务失败、幂等冲突禁止重试。
- 写操作重试必须有幂等保护(见幂等章节),否则重试 = 重复下单/重复扣款。
- 重试要退避(间隔递增),并加随机抖动,避免重试请求同拍打向下游。
坑三:慢调用放大——不是挂了,只是变慢
比“挂了”更隐蔽的是“变慢”:下游响应 3 秒(没超时),上游线程/连接被长时间占用。当慢调用占比上升,线程池被占满,新请求开始排队,排队又加剧延迟——系统在“还没报错”时就已经瘫痪。
慢调用放大的传导:
下游变慢(3s)
→ 上游线程被占 3s
→ 线程池排队,RT 上升
→ 网关连接被占,整体吞吐下降
→ 健康检查/心跳超时,误判实例死亡
→ 重启/摘除引发二次抖动
预防清单:
- 舱壁隔离:给不同下游分线程池/信号量(Bulkhead),慢下游只能拖垮自己的舱,不能传染全局。
- 熔断兜底:下游延迟或错误率超阈值就快速失败(见 Resilience4j 章节),给下游恢复时间。
- 线程池监控告警:队列深度、活跃线程、拒绝次数是“慢调用放大”的前置信号。
- 别用“无限队列”:阻塞队列无界会让排队请求无限堆积,必须有界并配拒绝策略。
- 慢查询治理:数据库慢 SQL 是慢调用的高发源头,慢日志要告警。
三个坑的共同根源
超时链、重试风暴、慢调用放大本质上都是“缺乏全局韧性预算”:没人定义一条请求在超时、重试、线程占用上的总预算。落地做法:
- 为每条核心链路画依赖图,标注每跳超时、是否可重试、是否幂等。
- 定链路级 SLO:总超时预算、总重试次数上限、降级触发条件。
- 用故障演练验证:人为注入慢调用/异常,看保护策略是否真的兜得住。
小结
超时链靠“显式超时 + 预算分配”,重试风暴靠“单层重试 + 幂等 + 退避”,慢调用放大靠“舱壁 + 熔断 + 有界队列”。把这三个放大器写进设计评审的检查项,多数线上雪崩都能在设计期被拦住。