GitHub 近 8 小时宕机复盘:一个负载均衡错误如何演变成重试风暴

2026-08-21 30 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

8 月 17 日,GitHub 经历了持续 7 小时 47 分钟的服务中断。影响并未停留在 github.com 无法访问:认证系统失效、GitHub Actions 中断,Pull Request、Issue 和 Copilot 等功能也受到波及。根据官方事后分析,事故的关键链路是负载均衡配置错误,以及错误响应触发的客户端重试风暴。

这类事故值得工程团队关注,因为它不是简单的“配置写错导致少量请求失败”。在分布式系统里,一次局部故障会改变客户端行为;如果重试没有退避、抖动和总量限制,原本用于提高可用性的机制反而会持续放大流量,让系统失去恢复窗口。

从错误配置到全站故障的放大链路

可以把这次事故抽象为一条反馈回路:

  1. 负载均衡器的配置错误使部分请求未被正确处理。
  2. 客户端或内部服务收到错误响应,按照既定策略重新发起请求。
  3. 重试流量进一步占用连接、线程、CPU 和下游服务容量。
  4. 更多正常请求因资源拥塞而失败,继而加入重试队列。
  5. 即使修正配置,积压请求和持续重试也可能让服务迟迟无法恢复。

假设正常入口流量为每秒 10 万个请求,其中 40% 失败,每个失败请求立即重试两次,那么入口层看到的流量就可能迅速接近每秒 18 万个请求。现实系统还存在多层调用:浏览器重试 API,API 服务重试认证服务,认证服务再重试数据库或缓存。各层重试次数相乘后,放大倍数会远高于单层估算。

这也解释了为什么认证故障特别危险。认证通常位于大量产品功能的公共调用路径上。一旦认证依赖拥塞,代码托管页面、自动化任务、协作功能和 AI 辅助服务都可能同时失去关键依赖。表面上是多个产品一起出问题,底层却可能共享同一条故障链路。

重试不是免费保险

工程代码里常见这样的策略:请求失败就立即重试三次。它在单机测试中通常表现良好,但在线上故障期间会制造同步流量峰值。

可靠的重试策略至少要回答五个问题:

  • 哪些错误允许重试?连接中断、超时和部分 5xx 可能适合重试,认证失败、参数错误等 4xx 通常不应重试。
  • 请求是否幂等?GET 一般可以安全重试,创建订单、触发部署等写操作需要幂等键或去重机制。
  • 等待多久?应采用指数退避,而不是立即循环。
  • 如何避免同步?加入随机抖动,防止大量客户端同时在固定时间点重新请求。
  • 最多增加多少负载?使用重试预算限制重试请求占总流量的比例,并设置总超时。

尤其需要检查“重试叠加”。如果入口代理、服务 SDK 和业务代码都各自重试三次,一次用户请求理论上可能触发数十次下游调用。重试责任应由明确的一层承担,其他层则快速返回或执行降级。

可以这样实践:实现有边界的 HTTP 重试

下面是一个可直接运行的 Python 示例。它只重试幂等的 GET 请求和有限的瞬时错误,使用指数退避、随机抖动与总时限。运行前安装 requests,并把示例地址替换为自己的健康检查或只读 API。

python -m pip install requests
import random
import time
from typing import Iterable

import requests

RETRYABLE_STATUS = {429, 502, 503, 504}


def get_with_budget(
    url: str,
    attempts: int = 4,
    base_delay: float = 0.25,
    total_timeout: float = 5.0,
    retryable_status: Iterable[int] = RETRYABLE_STATUS,
) -> requests.Response:
    deadline = time.monotonic() + total_timeout
    retryable_status = set(retryable_status)
    last_error = None

    for attempt in range(attempts):
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            break

        try:
            response = requests.get(url, timeout=min(2.0, remaining))
            if response.status_code not in retryable_status:
                response.raise_for_status()
                return response
            last_error = RuntimeError(
                f"retryable HTTP status: {response.status_code}"
            )
        except (requests.Timeout, requests.ConnectionError) as exc:
            last_error = exc

        if attempt == attempts - 1:
            break

        # Full jitter: sleep randomly between zero and the exponential cap.
        delay_cap = base_delay * (2**attempt)
        sleep_for = random.uniform(0, delay_cap)
        if sleep_for >= deadline - time.monotonic():
            break
        time.sleep(sleep_for)

    raise RuntimeError(f"request failed within retry budget: {last_error}")


if __name__ == "__main__":
    result = get_with_budget("https://httpbin.org/status/200")
    print(result.status_code)

这段代码刻意没有重试所有异常,也没有无限延长单次请求。生产环境还应在进程或服务级别设置重试预算。例如,在一分钟窗口内,当重试请求超过总请求的 5% 时暂时关闭重试,让服务保留处理正常请求和健康检查的能力。

负载均衡层也要限制重试。以下是一个可改造的 Envoy 配置片段,演示如何限定可重试状态、重试次数和单次尝试超时。字段是否适用仍需结合实际 Envoy 版本验证。

route:
  cluster: api_backend
  timeout: 3s
  retry_policy:
    retry_on: connect-failure,reset,5xx
    num_retries: 2
    per_try_timeout: 800ms
    retry_back_off:
      base_interval: 100ms
      max_interval: 1s

这里的重点不是照搬数值,而是确保总请求时限大于单次尝试时限,同时对尝试次数设硬上限。对于非幂等写请求,应关闭代理层自动重试,或者先引入幂等键。

配置发布必须具备刹车能力

负载均衡配置属于生产代码,应经过解析、验证、灰度和自动回滚,而不是一次性推送到全部实例。可以这样设计发布流水线:

set -euo pipefail

# 将命令替换为所用负载均衡器的真实校验工具。
envoy --mode validate -c envoy.yaml

# 先发布到小比例实例,并观察错误率、重试率和饱和度。
./deploy-lb-config.sh --canary-percent 1 envoy.yaml
./check-slo.sh --window 5m

# 只有金丝雀指标正常时才扩大范围。
./deploy-lb-config.sh --canary-percent 10 envoy.yaml
./check-slo.sh --window 10m
./deploy-lb-config.sh --all envoy.yaml

其中 deploy-lb-config.shcheck-slo.sh 是示意接口,需要接入团队自己的部署与监控系统。关键控制点包括:

  • 在提交阶段执行配置语法与语义校验。
  • 用流量回放或集成测试验证路由、超时和失败响应。
  • 先让 1% 实例加载新配置,避免同时破坏全部入口。
  • 将错误率、重试率、请求量和后端饱和度设为自动回滚条件。
  • 保留上一份已验证配置,并确保回滚不依赖已经故障的控制面。

监控也不能只盯可用率。重试风暴早期,最终成功率可能尚未明显下降,但每个成功请求的尝试次数已经上升。建议至少记录 request_attempts_totalretries_totalretry_exhausted_total、后端并发连接数和队列等待时间,并按客户端、路由、状态码和配置版本切分。

落地时检查这几件事

GitHub 的这次长时间中断说明,负载均衡器和重试策略共同构成一个闭环系统。只验证“新配置能否加载”,并不足以证明它在错误响应和流量激增时仍然安全。

上线前可以按以下清单检查:

  • 配置变更是否经过静态校验、灰度发布和自动回滚?
  • 客户端是否只重试明确的瞬时错误?
  • 是否同时设置单次超时、总超时、最大尝试次数和随机抖动?
  • 多层代理与 SDK 是否重复执行重试?
  • 写请求是否具备幂等键或服务端去重?
  • 是否能按配置版本观察错误率和重试放大倍数?
  • 修复入口配置后,系统是否有清理积压和限制恢复流量的机制?
  • 认证等共享依赖不可用时,非关键功能能否降级而不是持续施压?

重试能掩盖短暂抖动,却不能创造容量。真正稳健的设计会给重试设预算、给配置发布设灰度、给共享依赖设隔离,并在故障发生时主动减少工作量。这样,局部配置错误才不至于扩展成持续数小时的系统性事故。


相关推荐