摘要: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 | 流量创新高 |
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 | function call(req, deadline): |
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、分区或分站点方式渐进放量
- [ ] 写请求使用幂等键,并测试“成功但响应丢失”场景
- [ ] 在压测和故障演练中验证重试风暴、区域切流及冷启动恢复
重试的目标不是“尽可能成功”,而是在系统仍有成功概率时,以有限、错峰、可撤回的额外负载换取成功。真正的容错,是当成功概率坍塌时,客户端懂得停手,服务端懂得拒绝,恢复过程懂得慢慢开闸。
参考资料
- GitHub Blog, The August 17 outage, and the work ahead, 2026-08-20.
- GitHub Status, Incident with GitHub.com — August 17, 2026.
- AWS Builders’ Library, Timeouts, retries, and backoff with jitter.
- Google SRE Book, Addressing Cascading Failures 与 Handling Overload.
- Microsoft Azure Architecture Center, Circuit Breaker pattern.