重试不是容错:GitHub 级联故障中的流量放大与恢复陷阱

摘要:GitHub一次级联事故中,客户端重试缺陷把Token Service流量放大约10倍。本文拆解重试正反馈、指数退避与jitter、retry budget、熔断限流、幂等性和渐进恢复。

分布式系统重试风暴与流量放大示意图 > 2026 年 8 月 17 日,GitHub 经历了一次持续 7 小时 47 分钟的事故。它提醒我们:重试只是处理瞬时故障的工具;没有边界的重试,会把局部延迟变成全局过载,并让已经修复的系统再次倒下。
分布式系统重试风暴与流量放大示意图
分布式系统重试风暴与流量放大示意图

从一个变慢的端点到 10 倍流量

GitHub 官方复盘显示,事故始于 Central US 数据中心出现新的流量峰值。一项 Istio sidecar 自动扩缩容策略只观察宿主服务、未正确反映 sidecar 的并发上限;sidecar 达到限制后,压力继续向下游传播,最终四个 HAProxy 节点耗尽 flow limit,网关认证路径出现高延迟和失败。

这条共享认证路径连接了大量产品面:github.com、API、Issues、Pull Requests、Actions、Copilot,以及 SAML/OIDC、SCIM、Team Sync。高峰时 Web/API 错误率约 20%,archive 与 raw content 下载错误率约 50%。这不是“某个页面坏了”,而是共享依赖把单点容量问题扩展成了横跨产品的故障域。

更危险的阶段发生在恢复过程中。多数服务于 16:36 UTC 左右随 Central US 恢复,Actions 的影响持续到约 18:03;但一个内部端点回复延迟触发了 VS Code 中潜伏的重试缺陷。一笔失败的 Copilot token 操作可能产生许多额外请求并进入循环,Token Service 流量从正常的 7–9K RPS 升至 70–100K RPS,约放大 10 倍,直到 21:02 才完全恢复。

可将故障链路概括为:

1
2
3
4
5
6
7
8
9
10
流量创新高
→ sidecar 并发耗尽且未正确扩容
→ HAProxy flow limit 耗尽
→ 共享认证端点延迟/失败
→ 网关“乐观重试”增加内部负载
→ 部分流量切往 Northern Virginia
→ 客户端重试缺陷放大 token 请求约 10 倍
→ 恢复容量再次被重试流量占满
→ 阻断触发重试的响应、削减网关重试、分站点渐进放量
→ 最终恢复

GitHub 还披露,codeload 端点遭遇的若干抓取攻击增加了处置复杂度;官方并未将其认定为事故的最初原因。

为什么“再试一次”会成为正反馈

设原始请求率为 λ,每次失败概率为 p,每个逻辑请求最多重试 r 次。忽略相关性时,后端实际请求率近似为:

[
\lambda_{eff}=\lambda(1+p+p^2+\cdots+p^r)
]

但过载时 p 不是常数:请求越多,排队越长,超时越多;超时又触发更多重试。于是形成 负载↑ → 延迟↑ → 超时↑ → 重试↑ → 负载↑ 的正反馈。多层调用若各自重试,放大还会相乘:五层链路每层最多尝试三次,最坏可能把一次顶层操作变成 3^5=243 次末端调用。

端点“已经能回应”也不等于“已经恢复容量”。此时积压请求、连接重建、缓存回暖、故障转移流量和同步唤醒的客户端一起涌入,刚恢复的实例会被瞬间打回过载状态。GitHub 最终采取 403 阻断入站 Token Service 请求、降低网关认证重试,并按站点逐步恢复流量,实质上是在先切断正反馈,再以受控探针验证容量。

正确的重试控制面

1. 指数退避必须配 jitter

n 次等待可设为:

[
d_n=U(0,\min(d_{max},d_0\times2^n))
]

只做指数退避仍可能让同一时刻失败的客户端按整齐节拍重试;full jitter 用随机等待打散“惊群”。同时必须设置总 deadline,而不是让每层独立耗尽完整超时。

1
2
3
4
5
6
7
8
9
10
11
function call(req, deadline):
for attempt in 0..MAX_RETRIES:
if now() >= deadline or !retryBudget.take(): fail_fast()
if breaker.isOpen(): fail_fast()

resp = send(req, timeout = min(perTryTimeout, deadline-now()))
if resp.ok: return resp
if !isTransient(resp) or !req.isIdempotent: return resp

sleep(random(0, min(MAX_BACKOFF, BASE * 2^attempt)))
return failure

2. Retry budget,而非固定“重试三次”

预算应限制某窗口内重试量,例如 retry_requests ≤ 10% × successful_requests,并按调用方、端点或优先级隔离。成功率下降时,可用预算随之收缩,客户端开始本地失败,而不是继续向病中的后端证明“它确实坏了”。重试最好只发生在调用链的一层,避免乘法放大。

3. 熔断、限流与 load shedding 分工

  • 限流:令进入系统的请求率不超过可承载速率,按租户与关键度设置配额,防止一个调用方拖垮共享依赖。
  • 熔断:滑动窗口内错误率或慢调用越阈值即 Open,快速失败;冷却后仅放少量 Half-Open 探针,成功后渐进闭合。
  • Load shedding:队列、并发或资源压力越界时尽早拒绝低优先级工作。快速返回 429/503 通常比接收后超时更省资源,但响应应携带明确的 Retry-After,客户端仍须服从预算和 jitter。
  • 降级:认证、写路径或实时结果不可用时,能否返回缓存、只读或功能缩减结果,要在事故前设计,而不是现场临时决定。

4. 没有幂等性,就不要盲目重试

GET 通常可安全重试;创建订单、触发工作流、扣费等写操作可能“服务端已成功、响应在途中丢失”。客户端应携带稳定的 idempotency key,服务端原子地记录“键—结果”,重复请求返回同一结果。否则一次超时可能变成多次副作用。对无法证明幂等的操作,应查询最终状态或交给可去重的消息/工作流系统处理。

恢复不是把开关重新打开

事故处置中最容易误判的是“错误率下降”。它可能只是入口流量暂时减少,并不代表内部队列已排空、依赖已回暖或各区域拥有相同余量。恢复控制至少要观察三组指标:入口的原始请求与重试请求,服务端的队列时间、在途请求和拒绝率,以及下游的容量余量。仅看平均延迟也会掩盖尾部拥塞,应同时跟踪 p95/p99、deadline exceeded、熔断状态和 retry budget 消耗速度。

放量应采用闭环而非固定时间表:先保持足够的 load shedding,让实例完成启动、连接池建立和缓存预热;再向一个故障域注入小比例真实流量;只有成功率、尾延迟和资源余量在观察窗口内稳定,才提高下一档。若指标反转,立即退回上一档,而不是等待新一轮超时确认。GitHub 按站点逐步恢复 Copilot Token Service 流量,正体现了这种“先稳定、再探测、后扩张”的原则。

还应区分可用性探针容量探针:一次请求成功只能证明路径可达,不能证明系统能承受正常峰值。Half-Open 阶段需要限制探针并发,并让不同客户端共享或近似感知恢复节奏;否则成千上万客户端各自发送“少量探针”,汇总后仍是洪峰。

工程清单

  • [ ] 为每条调用链设置端到端 deadline、单次超时和最大尝试数
  • [ ] 只重试可判定的瞬时错误;禁止对所有 4xx/5xx 无差别重试
  • [ ] 使用指数退避 + full jitter,并支持 Retry-After
  • [ ] 建立按客户端/端点/优先级隔离的 retry budget
  • [ ] 规定唯一重试层,监控“逻辑请求数/物理请求数”的放大倍数
  • [ ] 为共享依赖配置并发上限、租户配额、熔断和 load shedding
  • [ ] 将 sidecar、代理、连接数、flow/concurrency limit 纳入容量与扩缩容信号
  • [ ] 恢复时先压低负载,再以 Half-Open、分区或分站点方式渐进放量
  • [ ] 写请求使用幂等键,并测试“成功但响应丢失”场景
  • [ ] 在压测和故障演练中验证重试风暴、区域切流及冷启动恢复

重试的目标不是“尽可能成功”,而是在系统仍有成功概率时,以有限、错峰、可撤回的额外负载换取成功。真正的容错,是当成功概率坍塌时,客户端懂得停手,服务端懂得拒绝,恢复过程懂得慢慢开闸。

参考资料

  1. GitHub Blog, The August 17 outage, and the work ahead, 2026-08-20.
  2. GitHub Status, Incident with GitHub.com — August 17, 2026.
  3. AWS Builders’ Library, Timeouts, retries, and backoff with jitter.
  4. Google SRE Book, Addressing Cascading FailuresHandling Overload.
  5. Microsoft Azure Architecture Center, Circuit Breaker pattern.
分享到