跳到内容

第 6 卷 / 第 14 章 / 第 2 课

14.2 截止时间、重试、断路器与过载保护 ​

远程调用一定会遇到慢、断开、部分成功和结果未知。韧性模式的目的不是掩盖所有失败,而是限制失败持续时间、资源消耗和传播范围。

设计时先确定整个操作的截止时间,再决定哪些失败值得重试,以及断路器、隔舱和负载削减各自保护哪些资源。这是推理顺序,具体装饰器并不存在通用的唯一排列。

从端到端截止时间开始 ​

单跳超时只限制某一次调用;端到端 deadline 表示整个用户操作还剩多少时间。

假设 A、B 串行调用,整个操作的入口预算为 800 ms:

text
入口处理 80 ms
下游 A 预算 250 ms
下游 B 预算 300 ms
序列化、排队和安全余量 170 ms

这里四项相加为 800 ms;若 A、B 并行,等待预算应按关键路径计算。下游不能每层重新获得 800 ms,否则调用链越深,总等待越长。应传播剩余预算。本地可用单调时钟计时,但不能把其原始时间戳发给另一台机器;协议必须处理时钟差异,例如 gRPC 会传播扣除已用时间后的超时值。

客户端取消后应尽量停止无价值工作,但取消需要服务端配合,也不能撤销已经提交的写入。检查所用客户端的超时是否覆盖连接、DNS、TLS 和响应读取,不能只给其中一个阶段计时,却称它为总预算。

超时值来自延迟分布和业务预算,不来自随手选择的整数。过短会制造重试流量,过长会占满线程、连接和队列。

重试必须满足安全条件 ​

适合自动重试的通常是:

  • 幂等读取;
  • 带幂等键的写入;
  • 明确返回瞬态错误且服务端确认未提交的操作。

不应因为“收到超时”就重试非幂等写入。超时意味着结果未知,原操作可能已经成功。

退避可采用指数增长并加入随机抖动:

text
delay = random(0, min(cap, base × 2^attempt))

这里 attempt = 0 表示第一次重试,random 在给定区间内均匀取值,base 和 cap 使用相同时间单位。服务端给出 Retry-After 时,应结合它和剩余截止时间决定是否再试;若等待与下一次尝试已没有预算,就停止。鉴权失败、参数错误、断路器拒绝和本地过载不应被统一当作瞬态网络错误重试。

还需要重试预算:限制额外请求占正常流量的比例。否则依赖容量下降时,重试会把一次故障放大成多倍流量。

重试应尽量放在最了解操作语义的一层。假设网关、服务 SDK 和业务代码三层都允许最多 3 次总尝试(含首次),最深处最坏会收到 3 × 3 × 3 = 27 次调用。若配置的是每层额外重试 3 次,则是 4 × 4 × 4 = 64 次。配置名里的 retries 与 maxAttempts,差的可能不只一个单词。

断路器保护的是调用资源 ​

断路器根据一段时间内的失败率或慢调用率切换状态:

text
CLOSED    ──失败达到阈值──> OPEN
OPEN      ──等待期结束────> HALF_OPEN
HALF_OPEN ──探测满足成功条件──> CLOSED
HALF_OPEN ──探测失败──────> OPEN
  • CLOSED:正常调用并统计结果;
  • OPEN:新调用快速失败,不再为它们发起远程请求;已经在途的调用不会因此自动取消;
  • HALF_OPEN:允许少量探测请求判断是否恢复。

断路器不是健康检查,也不保证业务降级正确。它只决定“现在是否值得继续尝试”。窗口、最小样本数、慢调用阈值和半开探测量都需要结合流量校准。

隔舱、并发限制与负载削减 ​

如果所有依赖共用一个线程池或连接池,一个慢依赖可以拖死整个服务。隔舱为关键依赖或请求类别分配独立资源上限。

队列不是免费容量。请求到达速率长期高于处理速率时,无界排队只会把失败变成更晚的超时。例如持续到达 120 个请求/秒、只能完成 100 个/秒,在这个忽略波动的模型中,100 个位置的空队列仅需 5 秒就会填满;队列没有增加吞吐量。系统应在资源耗尽前:

  • 限制并发和队列长度;
  • 对低优先级请求快速拒绝;
  • 返回可识别的过载信号;
  • 保留健康检查、控制面和关键流量的资源。

负载削减比接受所有请求后一起超时更可恢复。

降级必须保持业务诚实 ​

有效降级示例:

  • 推荐不可用时返回“暂无推荐”;
  • 排行榜读缓存并标注更新时间;
  • 非关键通知进入待发送队列。

危险降级示例:

  • 支付校验失败时默认通过;
  • 权限服务超时时默认授权;
  • 结算结果未知时返回“成功”。

降级是业务政策,应由产品和领域共同定义,不能由基础设施库自动猜测。

组合次序与观测 ​

下面展示一种按单次尝试隔离资源的嵌套方式:

text
整个操作的截止时间
  └─ 有预算的重试循环
       ├─ 每次尝试:检查断路器 → 获取并发许可 → 限时调用 → 释放许可
       └─ 确认可以重试且预算足够 → 退避等待
  → 结果 / 明确的降级

这里退避期间不占下游并发许可;若把许可包在重试循环外,限制的则是整个逻辑调用,等待也会占位。两者控制的资源不同。还应避免把本地许可不足算作下游故障;若先取得半开探测资格却未能真正调用,要按库的机制归还资格。具体库的装饰顺序会影响统计和资源释放,必须用故障测试确认。至少观测:

  • 每个下游的调用量、失败率和延迟;
  • 超时、重试次数和重试成功率;
  • 断路器状态与拒绝数;
  • 并发、连接池、队列和负载削减数;
  • 降级结果在总响应中的比例。

平均延迟会掩盖长尾,应关注 p95/p99 和超出 deadline 的比例。

参考资料 ​

Built with VitePress | Software Systems Atlas