服务雪崩是如何形成的?超时、重试、熔断与隔离的系统设计
服务雪崩通常不是某个实例宕机,而是一个依赖变慢后,上游仍按正常速度灌入请求,等待中的任务逐层占满线程、连接和内存,最终把健康服务也拖死。
一条典型故障链
订单服务调用库存,库存数据库延迟从 20ms 升到 3s。订单线程池有 200 个线程,入口 500 QPS。根据 Little’s Law,系统中等待任务约为 500 × 3 = 1500,远超线程池容量。工作线程被占满,队列增长;网关超时后客户端重试,实际流量翻倍;订单继续调用支付和用户服务,连接池也被耗尽,故障扩散。
关键指标不是“库存还能返回”,而是其延迟已经超过上游容量模型。慢失败往往比快速失败更危险。
超时预算必须逐层递减
若用户请求总预算 2 秒,网关、服务 A、服务 B 都配置 2 秒超时,上游已经超时时,下游仍在做无人需要的工作。应从端到端 SLA 倒推:预留网络、排队和降级时间,并让越下游的单次调用预算更短。
Java 代码要同时设置连接超时、读取超时、连接池获取超时;只有 read timeout 不能阻止 DNS、建连或池等待拖死请求。请求取消信号也应尽可能传播。
这里应区分 timeout 与 deadline:timeout 是某一步最多等待多久,deadline 是整个逻辑请求在某个绝对时刻前必须结束。gRPC 原生支持 deadline 传播;HTTP 没有一个所有框架都自动遵守的通用标准,通常需要在受信任的服务间传递剩余预算,并在每一跳校验上限。不能直接信任外部客户端传入一个超长 deadline。
四个机制各自解决什么
- 限流:在容量入口拒绝超额请求,保护系统不进入失稳区。
- 隔离:按依赖或业务拆分线程池、信号量、连接池,避免一个慢依赖占光共享资源。
- 熔断:失败率或慢调用率超过阈值时快速失败,定期半开探测恢复。
- 降级:返回缓存、默认值或关闭非核心能力,保住主链路。
还需要负载卸除(load shedding):当并发数、队列等待或剩余预算已表明请求不可能按时完成时,在入口尽早返回 429/503,比让请求占用资源后再超时更安全。自适应并发限制可以作为静态限流的补充,但必须保留硬上限和回滚开关。
熔断器不是故障检测的唯一来源,也不能增加真实容量。半开时若同时放入大量探测请求,会再次压垮刚恢复的依赖。
熔断统计应按“依赖目标 + 操作类型”设置合理边界:粒度太粗会让一个非核心接口熔断整个依赖,粒度细到用户或订单又会造成状态爆炸。虚拟线程能降低等待线程的成本,却不会增加数据库连接、下游 QPS 或 CPU 容量,不能替代隔离、限流和 deadline。
队列为什么不能无限大
大队列把过载转化为高延迟,并消耗内存。请求在队列中等待 10 秒后即使成功,调用方可能早已超时。队列应有界,并根据服务时间、可接受等待和实例容量设定;拒绝策略必须可观测,不能静默丢弃。
演练与验证
在预发注入依赖延迟、错误、连接拒绝和部分实例丢包,观察:入口吞吐是否受控;线程/连接池是否隔离;熔断是否在预期窗口打开;核心接口是否仍满足 SLO;恢复后是否发生流量洪峰。
生产检查清单
- 是否有端到端 deadline,而不是每层独立超时?
- 过载时是否在入口快速拒绝,并返回可识别、可统计的状态?
- 重试是否受预算约束并带退避抖动?
- 核心与非核心调用是否资源隔离?
- 队列、线程池、连接池是否有界且有饱和告警?
- 熔断是否同时考虑慢调用率与错误率?
- 是否定期演练慢依赖,而不只演练实例下线?
稳定性设计的目标不是保证每个请求成功,而是在过载和局部故障时,主动牺牲少量请求,避免整个系统失去服务能力。