弹性设计:超时、重试、熔断、隔离与降级

弹性的目标不是「不出故障」,而是局部故障不扩散:某个下游变慢或挂掉时,调用方还能按可接受的质量继续服务。手段有五个——超时、重试、熔断、舱壁隔离、降级,它们解决不同的问题,缺任何一个都会留下具体的破口。

一、五件套各自解决什么

手段解决什么保护谁关键参数用错的反效果
超时不让调用方无限等待,尽快释放线程与连接调用方自身资源每跳预算、连接与读超时、总预算太长则资源被拖死;太短则把「慢」误判为「故障」
重试掩盖瞬时故障(网络抖动、实例重启、偶发超时)请求成功率次数、退避、抖动、重试预算、幂等性放大流量,把「慢」升级为「雪崩」
熔断下游持续错误时快速失败,不再消耗资源调用方与下游错误率阈值、最小请求数、熔断窗口、试探流量小流量误触发;窗口过长导致长期不可用
舱壁隔离一个依赖、租户或接口耗尽资源时,不影响其它整个进程池大小、队列长度、拒绝策略池划分过细导致资源空转与排队
降级牺牲非核心,保住核心链路业务核心功能触发条件、兜底数据、开关粒度兜底数据被当成真实数据;开关无人回收

一句话记忆:超时管时间,重试管瞬时,熔断管持续,隔离管资源,降级管业务。

二、组合顺序:请求按顺序穿过五层

客户端请求
   │
   ▼
[ 限流 ] 入口挡量(见 system-design 的限流章节)
   │
   ▼
[ 总超时预算 ] 自入口向下逐级扣减,每层只能花自己那一份
   │
   ▼
[ 舱壁:按依赖 / 租户分池 ] ──池满──► 快速拒绝(而不是排队到超时)
   │
   ▼
[ 熔断 ] ──Open──► 直接走降级,不发真实请求
   │
   ▼
[ 单次调用:连接超时 + 读超时 ]
   │ 失败且可重试
   ▼
[ 重试:退避 + 抖动 + 预算上限 ] ──重试同样消耗总预算──► 回到熔断判定
   │ 最终失败
   ▼
[ 降级返回兜底值 ] 并打点记录降级原因

三条顺序规则:

  1. 熔断在重试之外。熔断打开时不允许重试,否则试探流量被成倍放大(状态机细节可参考 middleware-interview 的熔断章节)。
  2. 重试共享总预算。重试不是「再来一次」,而是从同一个时间预算里再切一块,预算耗尽即失败。
  3. 降级是最后一步,也是最先要定的一步。先明确「失败时给用户看什么」,再回头配超时与重试;失败路径未定义时,参数只能靠猜。

三、重试放大:最容易自己造出的雪崩

重试的危险不在单次,而在层级相乘。以一个三层调用链、每层允许 3 次尝试(1 次原始 + 2 次重试)为例:

层级单个入口请求对应的调用次数相对最底层放大
A(入口)11×
A 调 B,B 每请求调 1 次 C33×
B 对每请求扇出 3 个 C3 × 39×
若 C 之下还有一层同样重试3 × 3 × 327×

下游已经因过载而变慢时,重试正好在最不该加量的时刻把流量乘以 27。四条约束把它压住:

  1. 只在幂等操作上重试。查询、按唯一键的幂等写可以重试;非幂等的下单、转账不能盲重试,除非带幂等键。
  2. 只重试明确的瞬时故障。连接失败、连接超时、502/503 可以重试;业务错误码(余额不足、参数非法)重试一百次结果一样。
  3. 退避加抖动。固定间隔重试会让所有客户端同时再打一次,形成脉冲,反而更伤下游。
  4. 重试预算。按「重试次数 / 总请求数」设上限(例如 10%),超预算直接失败,让重试量有全局天花板。

四、舱壁隔离:按什么维度分池

维度例子适用场景
按依赖用户服务、支付服务各一个线程池与连接池最常用:防止一个慢依赖吃掉所有线程
按租户大客户独占配额,免费用户共享池多租户 SaaS,避免单个客户拖垮全局
按接口同步写接口与后台批量接口分池防止离线任务挤压在线请求
按优先级核心交易池 + 非核心任务池资源紧张时先保核心

代价要说清楚:池是独占资源——拆成 5 个池,每个池都要留余量,总占用上升而平均利用率下降;池间不能互相借资源,突发流量下会出现「别的池空着、这个池排满」。所以分池维度不要超过 3~4 个,每个池都要有明确的容量依据与拒绝策略。

五、降级预案与开关设计

降级要事先分级,故障当天才能不假思索地执行:

级别状态动作
L0正常全功能
L1单个非核心依赖异常关闭该依赖相关功能(推荐位、评论、个性化),返回缺省内容
L2核心依赖不稳定关闭非核心功能集合,缩短超时、提高熔断灵敏度,保留核心写链路
L3严重过载只保核心写入与查询,读走缓存或静态兜底,非核心接口直接拒绝

开关设计要点:

  • 三级粒度:全局开关(一键进 L2)、依赖级开关(关某个下游)、租户或接口级开关(先小流量验证)。
  • 每个开关有主人和到期时间。没有主人的开关会腐烂成永久的隐藏行为。
  • 开关要能被演练:定期在预发真实翻转,验证降级返回的数据格式端上能接受。只写不演练的降级预案等于没有预案。
  • 降级要有可观测标记:在响应头或日志里标记「本次为降级结果」,避免兜底数据被当成真实数据参与业务判断(例如把降级返回的「无库存」当成真实库存)。

六、常见失败模式

  • 超时层层加码:下游超时 5s、上游 10s,最坏情况线程被占 15s,连接池瞬间打满。总预算必须自上而下扣减。
  • 重试与超时互不知情:单次超时 1s、重试 3 次,实际最长 3s,已超过上层的 2s 超时,于是上层又重试一次。
  • 熔断阈值忽略小流量:每秒 2 个请求的服务,1 个错误就触发 50% 错误率阈值,必须设最小请求数。
  • 开关只在故障当天第一次打开:常年不动的开关,谁也不敢在生产上按下去。
  • 只做服务端弹性:客户端无超时、无重试上限时,用户侧会持续重发,把服务端的恢复过程拖长。

小结:弹性五件套分别管时间、瞬时故障、持续故障、资源边界与业务取舍,合起来才形成「局部故障不扩散」的效果。落地顺序是:先给每跳定超时与总预算,再定哪些操作允许重试并配套退避、抖动与预算,然后用熔断处理持续故障、用舱壁划清资源边界,最后把降级分级与开关一并设计并演练。所有配置都要能被演练、能被观测,并且有人负责回收。

笔记加载中…