土法炼钢兴趣小组的算法知识备份

【分布式系统百科】大规模故障复盘:从真实事故中学习分布式系统设计

文章导航

分类入口
分布式系统
标签入口
#故障复盘#事故分析#可靠性#SRE#分布式系统

目录

故障对比时间线

一、43 秒的操作,24 小时的恢复

2018 年 10 月 21 日 22:52 UTC,GitHub 美国东海岸数据中心之间的网络连接中断了 43 秒。这 43 秒内,数据库故障转移(Failover)系统 Orchestrator 检测到主节点不可达,按照设计逻辑将一个从节点提升为新的主节点。网络恢复后,集群中出现了两个”主节点”,各自接受了不同的写入请求。数据开始分叉。

这 43 秒造成的数据不一致,GitHub 花了 24 小时才完全修复。在此期间,webhook 不触发,GitHub Pages 不构建,Issues 和 Pull Requests 的状态与实际代码不匹配。全球数千万开发者受到影响。

这个事故体现了分布式系统故障的一个核心特征:触发条件和恢复成本之间存在巨大的不对称性。一个短暂的网络分区触发了自动化故障转移,自动化故障转移制造了数据分叉,数据分叉需要逐行比对和人工决策才能修复。每一层的复杂度都在放大。

本文选取 8 个真实的大规模分布式系统故障案例进行深度分析。这些案例来自 GitHub、Cloudflare、AWS、Google Cloud、Facebook、Roblox 等基础设施提供商,覆盖数据库故障转移、网络配置变更、操作失误、基础设施级联等不同类型的故障触发模式。每个案例都有公开的故障复盘(Postmortem)报告,本文基于这些一手材料提炼根因、传播路径和改进措施。

目标不是猎奇,而是回答一个工程问题:这些事故暴露了分布式系统设计中的哪些共性弱点?

二、故障分析框架

在进入具体案例之前,需要先建立分析框架。一份好的故障复盘报告不只是”发生了什么”的时间线流水账,而是对故障机理的结构化拆解。

根因与促成因素

每次事故通常有一个直接根因(Root Cause),但同时存在多个促成因素(Contributing Factors)。以 GitHub 2018 事故为例:直接根因是 43 秒的网络中断,但促成因素包括 Orchestrator 的自动提升策略、跨数据中心的主从拓扑、缺乏自动数据一致性校验工具等。只修复根因而忽略促成因素,类似的事故会以不同的触发方式再次发生。

故障传播路径

故障很少停留在触发点。一个数据库主节点的切换可能导致应用层缓存失效,缓存失效引发数据库查询风暴,查询风暴打垮数据库连接池。理解传播路径,才能知道在哪里设置断路器(Circuit Breaker)和隔离边界。

检测时间与恢复时间

平均检测时间(MTTD,Mean Time To Detect)衡量从故障发生到团队意识到问题的时间间隔。平均恢复时间(MTTR,Mean Time To Recover)衡量从检测到问题到恢复正常服务的时间间隔。两个指标反映的是完全不同的能力:MTTD 反映监控和告警体系的覆盖程度,MTTR 反映恢复流程的成熟度和基础设施的可操作性。

爆炸半径

爆炸半径(Blast Radius)描述一次故障影响的范围——受影响的用户数、服务数、区域数。爆炸半径取决于系统架构的隔离程度。单元化架构(Cell-based Architecture)可以将爆炸半径限制在一个单元内,而全局共享控制平面(Shared Control Plane)的设计则意味着控制平面一旦故障,所有单元都受影响。

瑞士奶酪模型

瑞士奶酪模型(Swiss Cheese Model)最初用于航空安全分析,同样适用于分布式系统故障。该模型将系统的防御措施比作多层奶酪切片,每层切片都有孔洞(即缺陷),单一层的孔洞不会导致事故。只有当多层切片的孔洞恰好对齐时,故障才能穿透所有防御层,造成事故。

flowchart TD
    subgraph Swiss["瑞士奶酪模型:多层防御"]
        N["网络层防御"]
        I["基础设施层防御"]
        A["应用层防御"]
        D["数据层防御"]
        H["人工操作层防御"]
    end
    F["故障源"] --> N
    N -->|"孔洞对齐"| I
    I -->|"孔洞对齐"| A
    A -->|"孔洞对齐"| D
    D -->|"孔洞对齐"| H
    H -->|"所有层穿透"| E["事故发生"]

上图展示了分布式系统中典型的五层防御结构。每一层(网络、基础设施、应用、数据、人工操作)都有各自的缺陷”孔洞”,正常情况下单层缺陷会被其他层拦截。当多层孔洞恰好在同一方向对齐时,故障从源头一路穿透所有防御层,最终演变为面向用户的事故。本文后续分析的每一个案例都可以用这个模型来识别哪些防御层失效、为何失效。

以下是一份结构化的故障复盘报告模板:

# 故障复盘报告模板
incident_id: "INC-2024-001"
title: "xxx 服务中断"
severity: P1
duration: "2024-01-15 14:00 ~ 18:30 UTC"
impact: "影响 30% 用户的写入操作,持续 4.5 小时"
authors: ["oncall-engineer-a", "oncall-engineer-b"]

timeline:
  - time: "14:00"
    event: "配置变更部署到生产环境"
  - time: "14:03"
    event: "监控告警触发:写入延迟 > 500ms"
  - time: "14:10"
    event: "值班工程师开始排查"
  - time: "14:25"
    event: "定位到配置变更为触发原因"
  - time: "14:30"
    event: "回滚配置变更"
  - time: "14:45"
    event: "写入延迟恢复正常"
  - time: "18:30"
    event: "完成积压数据处理,确认全部恢复"

root_cause: "配置变更将写入流量的路由权重设为 0,导致所有写入请求落到单个节点"
contributing_factors:
  - "配置变更缺少灰度发布流程"
  - "配置文件缺少参数范围校验"
  - "监控告警阈值过高,检测延迟 3 分钟"

action_items:
  - owner: "team-infra"
    action: "为配置变更添加金丝雀发布流程"
    priority: P1
    deadline: "2024-02-01"
  - owner: "team-platform"
    action: "添加配置参数范围校验"
    priority: P1
    deadline: "2024-02-15"

这个模板的关键在于区分”时间线”和”分析”。时间线记录事实,分析部分拆解因果关系。两者混在一起会导致复盘报告变成叙事文学而非工程文档。

级联故障传播模型

在理解了单个事故的分析框架后,还需要关注故障在系统间的传播路径。下图展示了一个典型的微服务架构中级联故障的传播过程:

flowchart TD
    LB["负载均衡器"] --> GW["API 网关"]
    GW --> SA["服务 A"]
    GW --> SB["服务 B"]
    SA --> DB["数据库"]
    SA --> Cache["缓存"]
    Cache -->|"缓存未命中"| DB
    DB -->|"过载"| Cascade["级联故障"]
    SB --> SA
    Cascade -->|"连接池耗尽"| SA
    SA -->|"响应超时"| GW
    GW -->|"请求堆积"| LB

    style Cascade fill:#f66,stroke:#c00,color:#fff
    style DB fill:#faa,stroke:#c00

该图展示了级联故障的典型传播链路:当数据库过载时,缓存未命中的请求直接压到数据库,进一步加重负载并触发级联故障。级联故障耗尽服务 A 的连接池后,响应超时逐层向上传播,最终导致负载均衡器处请求堆积,整个系统不可用。值得注意的是,服务 B 对服务 A 的依赖关系使得故障影响范围进一步扩大——这种隐式的服务间依赖在后续的多个真实案例中反复出现。

三、GitHub 2018:数据库主从切换引发的数据不一致

背景

GitHub 的核心数据存储使用 MySQL,采用主从复制(Primary-Replica Replication)架构。为了实现高可用,GitHub 部署了 Orchestrator——一个开源的 MySQL 高可用管理工具,能在主节点故障时自动将从节点提升为新的主节点。

GitHub 的 MySQL 集群分布在美国东海岸的多个数据中心。主节点位于一个数据中心,从节点分布在同一数据中心及其他数据中心。Orchestrator 持续监控主从复制状态,一旦检测到主节点不可达,就会发起自动故障转移。

触发:43 秒网络分区

2018 年 10 月 21 日 22:52 UTC,数据中心之间的网络连接中断了 43 秒。Orchestrator 在位于另一个数据中心的节点上运行,它检测到主节点不可达后,按照策略将该数据中心的一个从节点提升为新的主节点。

问题在于:网络分区是暂时的。43 秒后网络恢复,但此时集群中已经存在两个主节点。旧主节点在网络中断期间继续接受写入请求(因为应用层的连接池还持有到旧主节点的连接),新主节点也开始接受写入请求。两个主节点各自产生了不同的写入,MySQL 的二进制日志(binlog)出现了分叉。

数据分叉的严重性

这不是简单的”两个节点数据版本不同”的问题。由于 MySQL 使用自增主键(auto-increment ID),两个主节点可能为不同的行分配了相同的主键值。合并数据不能简单地取并集,需要逐行比对,确认每一行数据的正确版本。部分数据在两个主节点上都有修改,需要人工判断哪个版本是正确的。

恢复过程

GitHub 做了一个关键决策:选择数据正确性而非服务可用性。他们没有选择快速恢复服务然后在后台修复数据,而是先停止写入,确保数据完全一致后再恢复服务。

恢复步骤包括:

  1. 锁定所有 MySQL 集群为只读模式
  2. 确定每个集群中哪个节点的数据更完整
  3. 将另一个节点的独有写入提取出来
  4. 人工审核冲突数据,逐条决定保留哪个版本
  5. 将合并后的数据写回主节点
  6. 重建所有从节点的复制关系
  7. 逐步恢复写入

整个过程持续约 24 小时。期间 GitHub 对用户可见的状态页面持续更新,明确告知用户数据可能不一致。

时间线摘要

时间 (UTC) 事件
22:52 数据中心间网络中断
22:54 Orchestrator 发起自动故障转移
23:02 工程团队收到告警
23:07 确认出现双主节点
23:09 停止自动故障转移,开始评估数据分叉范围
次日 00:00 开始数据一致性校验
次日 06:30 部分服务恢复(只读模式)
次日 16:24 数据一致性校验完成
次日 23:03 所有积压任务处理完毕,全面恢复
sequenceDiagram
    participant Net as 网络
    participant Orch as Orchestrator
    participant Old as 旧主节点
    participant New as 新主节点
    participant Eng as 工程师

    Note over Net,Eng: 正常运行阶段
    Net->>Old: 正常流量

    Note over Net: 22:52 网络分区开始
    Net--xOld: 连接中断(43秒)

    Note over Orch: 22:54 检测到主节点不可达
    Orch->>New: 提升为新主节点

    Note over Old,New: 双主节点并行写入(脑裂)
    Old->>Old: 继续接受写入
    New->>New: 开始接受写入

    Note over Net: 22:52+43s 网络恢复

    Note over Eng: 23:02 收到告警
    Eng->>Eng: 开始排查

    Note over Eng: 23:07 确认双主
    Eng->>Old: 停止自动故障转移

    Note over Eng: 次日 16:24 数据校验完成
    Note over Eng: 次日 23:03 全面恢复

上图清晰地呈现了这次事故的时间线:从 22:52 网络分区到 43 秒后恢复,期间 Orchestrator 已经完成了故障转移,导致双主节点并行写入的脑裂状态。从工程师 23:02 收到告警到次日 23:03 全面恢复,中间长达 24 小时的恢复过程主要消耗在数据一致性校验上。这一时间对比深刻体现了”触发成本与恢复成本的不对称性”这一分布式系统故障的核心特征。

关键教训

Orchestrator 的行为是正确的——按照它的设计逻辑,检测到主节点不可达就应该提升从节点。 问题在于这个”正确的行为”在特定场景下会制造更大的问题。自动化故障转移的前提假设是:网络分区是持久性的,旧主节点不会恢复。当分区是短暂的,自动化反而制造了脑裂(Split-Brain)。

改进方向包括:

四、Cloudflare 2020:骨干网路由配置导致全球中断

背景

Cloudflare 运营着一个覆盖全球 200 多个城市的边缘网络。各数据中心之间通过骨干网(Backbone Network)互联。骨干网承载数据中心之间的流量转发,如果骨干网不可用,各数据中心只能通过公共互联网转发流量,带宽和延迟都会急剧恶化。

触发:路由规则变更

2020 年 7 月 17 日,一名工程师对亚特兰大数据中心的骨干网路由器执行了一条路由规则变更。该变更的意图是调整特定前缀(prefix)的流量路径。但由于规则编写错误,新规则导致骨干网上的流量被错误地路由到一个不存在的下一跳(next-hop),实质上等同于黑洞路由(Black Hole Routing)——流量进入骨干网后被丢弃。

传播与影响

这条错误规则通过骨干网的路由协议传播到其他数据中心的路由器。在几秒钟内,Cloudflare 骨干网的大部分路由表被污染。数据中心之间无法通过骨干网通信,回退到公共互联网路径,但公共互联网的带宽远不足以承载 Cloudflare 的全部流量。

影响持续约 27 分钟,全球约 50% 的 Cloudflare 流量受到影响。对于依赖 Cloudflare CDN 和 DDoS 防护的网站来说,这意味着约一半的用户请求超时或失败。

安全机制为何失效

Cloudflare 的路由变更流程包含多层安全检查:

  1. 变更需要同行评审(Peer Review)
  2. 变更在暂存环境(Staging)中验证
  3. 变更按区域分批发布

但这次事故绕过了第三层防线。路由规则的变更通过骨干网的路由协议自动传播,不受”分批发布”的控制。换言之,即使工程师只在一个数据中心的路由器上执行变更,路由协议会在秒级内将该规则同步到所有骨干网路由器。

恢复

工程团队在 3 分钟内检测到流量异常,但定位根因花了更长时间。最终通过撤回错误的路由规则并强制重置骨干网路由器的路由表来恢复。从检测到恢复,总计 27 分钟。

关键教训

五、AWS us-east-1:反复发生的故障模式

AWS 的 us-east-1 区域(北弗吉尼亚)是最老、最大、最复杂的区域,也是故障频率最高的区域。三次典型事故揭示了一个共性模式:复杂度本身就是故障源

S3 宕机(2017 年 2 月 28 日)

一名获授权的工程师在对 S3 的计费子系统进行例行维护时,需要移除少量服务器。他执行了一条工具命令,但输入参数错误,导致移除的服务器数量远超预期。

# 意图:移除计费子系统的少量服务器
# 实际效果:移除了索引子系统和分配子系统的大量服务器
# 具体命令未公开,但 AWS 事后确认是"输入变量错误"

被移除的服务器中包括 S3 的索引子系统(index subsystem)和分配子系统(placement subsystem)。索引子系统负责查找对象的存储位置,分配子系统负责分配新的存储空间。两个子系统同时下线后,S3 的 GET 和 PUT 操作全部失败。

由于互联网上大量服务将静态资源托管在 S3 上,S3 的中断引发了连锁反应。AWS 自身的服务仪表盘(Service Health Dashboard)也依赖 S3 存储图标文件,导致仪表盘本身无法正确渲染——用户去查看 AWS 的状态页面,结果状态页面自己也挂了。

恢复耗时约 4 小时。这些子系统在设计时没有考虑过如此大规模的节点同时下线,重启过程需要重建大量的元数据索引,速度远慢于预期。

Kinesis 故障(2020 年 11 月 25 日)

AWS Kinesis 数据流服务的前端服务器集群需要扩容。运维团队向集群中添加了新的服务器。按照正常流程,新服务器加入后需要与集群中的其他成员建立通信。但新服务器的加入触发了前端服务器的线程耗尽(Thread Exhaustion)问题。

每台前端服务器需要为集群中的每个成员维护一个通信线程。当集群规模增大时,每台服务器需要的线程数也增加。这次扩容后,线程数超过了操作系统的限制,导致前端服务器无法处理新的请求。

Kinesis 的故障级联影响了依赖它的 CloudWatch(监控服务)和 Lambda(无服务器计算)。CloudWatch 不可用意味着运维团队无法通过常规监控手段观察系统状态,只能依赖底层日志和临时搭建的监控通道。

网络连接中断(2021 年 12 月 7 日)

AWS 内部网络的自动化扩容系统在处理 us-east-1 主网络与内部服务网络之间的流量时触发了一个潜在缺陷。这个缺陷导致内部网络设备之间的通信出现异常,大量 AWS 服务之间的 API 调用超时。

由于 AWS 的许多服务之间存在深层依赖链,网络层面的问题快速传播到应用层面。EC2 实例的启动、EBS 卷的挂载、RDS 数据库的连接都受到影响。

us-east-1 为何更容易出故障

分析这三次事故可以发现一个结构性原因:

  1. 最老的区域:us-east-1 是 AWS 最早的区域,历史包袱最重。早期设计的服务间依赖关系不够清晰,积累了大量隐式耦合
  2. 最大的规模:最多的客户、最多的服务实例、最高的流量。规模放大了每一个潜在问题的影响范围
  3. 最深的依赖链:大量 AWS 内部服务率先在 us-east-1 部署,服务之间的依赖链最长最复杂。一个底层服务的异常可能通过 5-6 层依赖传播到面向用户的服务
  4. 客户的默认选择:许多客户将 us-east-1 作为默认区域,进一步增加了该区域的负载和复杂度

六、Google Cloud 2019:网络配置变更引发全球故障

背景

Google 运营着全球最大的私有网络之一。Google Cloud、YouTube、Gmail、Google Search 等服务共享这个底层网络基础设施。网络配置变更通过一个集中式的网络配置管理系统(Network Configuration Management System)下发到各区域的网络设备。

触发:配置变更范围失控

2019 年 6 月 2 日,一项网络配置变更原本计划应用于少数几个区域的路由器。但由于配置管理系统中的一个缺陷,这项变更被应用到了远超预期范围的网络设备上。

受影响的配置改变了网络流量的路由规则,导致大量流量被导向错误的网络路径。部分路径容量不足以承载被导入的流量,造成丢包和延迟飙升。

影响范围

这次事故影响了 Google 的多个产品线:

故障持续约 4 小时 15 分钟。峰值期间 Google Cloud 部分区域的错误率超过 30%。

技术细节

Google 的网络配置管理系统采用声明式模型:工程师定义目标状态,系统计算差异并生成配置命令下发到设备。这次事故中,系统在计算”哪些设备需要更新”时出现了错误——目标设备集合的过滤条件写法有误,导致选中了不应该被修改的设备。

配置管理系统本身有灰度发布(Canary Release)机制:先在少量设备上应用,验证无异常后再扩大范围。但这次的过滤条件错误恰好使得灰度阶段选中的设备不在受影响范围内(灰度设备表现正常),而后续批量下发时才命中了出问题的设备。灰度发布机制的有效性取决于灰度样本是否具有代表性——这次显然不具有。

关键教训

七、Facebook 2021:BGP 配置删除导致全球宕机

背景

Facebook 运营着一个全球骨干网,连接分布在全球的数据中心。骨干网内部使用边界网关协议(BGP,Border Gateway Protocol)向互联网通告 Facebook 拥有的 IP 地址前缀。外部网络依赖这些 BGP 通告来确定”发往 Facebook 的流量应该走哪条路径”。

Facebook 的 DNS 服务器也部署在这些数据中心内部,通过 BGP 通告的 IP 地址对外提供服务。

触发:维护命令撤回 BGP 路由

2021 年 10 月 4 日,Facebook 的工程团队执行了一项骨干网维护操作。该操作的目的是评估骨干网的容量状况。执行维护命令时,一个缺陷导致该命令同时撤回了所有 BGP 路由通告。

# 简化示意:BGP 路由撤回的效果
# 正常状态:Facebook 向互联网通告自己的 IP 前缀
announce 157.240.0.0/16    # Facebook 主要 IP 段
announce 129.134.0.0/16    # Facebook 其他 IP 段
announce 185.89.218.0/23   # WhatsApp IP 段

# 故障状态:所有前缀从全球路由表中消失
withdraw 157.240.0.0/16
withdraw 129.134.0.0/16
withdraw 185.89.218.0/23
# 结果:全球任何网络都不知道如何到达 Facebook 的服务器

当 BGP 路由被撤回后,全球的互联网路由器从路由表中删除了指向 Facebook 的条目。任何发往 Facebook IP 地址的数据包都无法到达目的地。

DNS 连锁失效

Facebook 的 DNS 服务器运行在自己的数据中心内,使用 Facebook 自己的 IP 地址。BGP 路由撤回后,这些 DNS 服务器虽然仍在运行,但外部网络已经无法访问它们。

用户在浏览器中输入 facebook.com 时,DNS 解析请求无法到达 Facebook 的权威 DNS 服务器(Authoritative DNS Server),解析失败。即使用户之前已经访问过 Facebook 并且本地有 DNS 缓存,缓存过期后(TTL 通常较短)也会解析失败。

这意味着不仅 Facebook 本身不可访问,所有使用 Facebook 域名的服务——包括 Instagram、WhatsApp、Messenger、Oculus——全部不可访问。

恢复之难

恢复过程暴露了一个严重的循环依赖问题:

  1. 远程访问不可用:工程师通常通过 VPN 或堡垒机远程登录数据中心设备。但 VPN 和堡垒机的域名解析依赖 Facebook 的 DNS,DNS 不可用导致远程访问也不可用
  2. 门禁系统受影响:部分数据中心的门禁系统依赖内部网络服务进行身份验证。网络故障后,门禁系统的认证功能受到影响,工程师进入数据中心的速度变慢
  3. 内部工具不可用:Facebook 内部的通信工具(Workplace)、工单系统、文档系统都依赖自家基础设施,全部不可用。团队间的协调只能依靠手机和个人邮箱

最终,工程师必须物理前往数据中心,手动登录路由器终端,恢复 BGP 配置。由于数据中心分布在不同地理位置,加上门禁系统的延迟,这个过程耗费了大量时间。

整个故障持续约 6 小时。影响 Facebook、Instagram、WhatsApp 的全球 35 亿用户。

关键教训

# BGP 路由变更安全检查(伪代码示意)
def validate_bgp_change(current_routes, proposed_changes):
    withdrawals = [c for c in proposed_changes if c.action == "withdraw"]
    total_prefixes = len(current_routes)
    withdraw_count = len(withdrawals)

    # 规则 1:禁止撤回所有前缀
    if withdraw_count == total_prefixes:
        raise SafetyError("禁止撤回所有 BGP 前缀")

    # 规则 2:撤回超过 50% 的前缀需要额外确认
    if withdraw_count > total_prefixes * 0.5:
        require_manual_confirmation(
            f"即将撤回 {withdraw_count}/{total_prefixes} 条前缀,需要二次确认"
        )

    # 规则 3:检查关键前缀(DNS 服务器所在前缀)是否被撤回
    critical_prefixes = get_critical_prefixes()  # DNS, 管理网络等
    for w in withdrawals:
        if w.prefix in critical_prefixes:
            raise SafetyError(f"禁止撤回关键前缀: {w.prefix}")

八、Roblox 2021:73 小时的持续故障

背景

Roblox 是一个拥有超过 5000 万日活用户(DAU,Daily Active Users)的在线游戏平台。其后端基础设施依赖 HashiCorp Consul 作为服务发现(Service Discovery)和配置管理的核心组件。Consul 集群的健康状况直接决定了 Roblox 所有后端服务能否正常通信。

触发:多因素叠加

2021 年 10 月 28 日,Roblox 开始出现服务异常。事后分析表明,故障由多个因素共同触发:

  1. Consul 集群压力:Roblox 的流量增长导致 Consul 集群承受的读写负载持续增加。Consul 底层使用的键值存储在高写入负载下性能退化
  2. 流式复制的缺陷:Consul 新引入的流式复制(Streaming Replication)功能存在一个性能缺陷,在特定负载模式下会导致 Consul 服务器的 CPU 使用率飙升
  3. BoltDB 性能退化:Consul 使用 BoltDB 作为底层存储引擎。BoltDB 在高写入负载下会出现写放大(Write Amplification)问题,磁盘 I/O 成为瓶颈

三个因素叠加导致 Consul 集群的响应时间急剧增加。当 Consul 响应变慢时,依赖 Consul 的服务健康检查开始超时,服务实例被标记为不健康,服务间调用失败。

为何持续 73 小时

这是本文分析的所有案例中持续时间最长的故障。原因在于每次修复尝试都会暴露新的问题:

第一阶段(0-12 小时):定位问题。团队最初将问题归因于流量峰值,尝试通过扩容 Consul 集群来解决。但扩容引入的新节点需要同步数据,同步过程进一步加重了集群负载。

第二阶段(12-36 小时):识别 BoltDB 瓶颈。团队发现 Consul 服务器的磁盘 I/O 持续处于饱和状态,定位到 BoltDB 的写放大问题。尝试通过替换存储引擎来解决,但替换过程需要数据迁移,而数据迁移本身又会产生大量写入。

第三阶段(36-60 小时):流式复制缺陷。在部分缓解 BoltDB 问题后,团队发现 Consul 的 CPU 使用率仍然异常高。进一步排查定位到流式复制功能的性能缺陷。禁用流式复制后 CPU 使用率下降,但这又导致 Consul 退回到效率更低的轮询模式,网络带宽消耗增加。

第四阶段(60-73 小时):逐步恢复。团队最终选择了一个综合方案:禁用流式复制、优化 BoltDB 的配置参数、减少写入频率、逐步恢复服务。整个恢复过程需要在不中断已恢复服务的前提下分批进行。

关键教训

九、更多案例速览

除了上述 6 个深度分析的案例,以下事故同样包含有价值的教训。

Fastly 2021:单客户配置触发全球 CDN 故障

2021 年 6 月 8 日,Fastly 的一位客户修改了自己的服务配置。这条配置触发了 Fastly 软件中一个潜在的缺陷(bug),导致全球 85% 的 Fastly 节点返回错误。故障持续约 49 分钟。

关键点在于:客户的配置本身是合法的,Fastly 的 API 接受了这条配置并将其分发到全球节点。问题在于 Fastly 的软件在处理这条特定配置时触发了一条从未被测试覆盖的代码路径。这类由合法输入触发的缺陷被称为边缘用例缺陷(Edge Case Bug),传统的功能测试很难覆盖。

GitLab 2017:数据库删除事故

2017 年 1 月 31 日,GitLab 的一名工程师在处理数据库复制延迟问题时,在错误的终端窗口中执行了 rm -rf 命令,删除了生产数据库目录中约 300 GB 的数据。

# 工程师原本打算在从库(db2)上执行
$ rm -rf /var/opt/gitlab/postgresql/data

# 实际执行的终端连接的是主库(db1)
# 6 秒后中断命令,但已删除约 300 GB 数据

事后发现 GitLab 的 5 种备份机制中有 4 种处于失效状态:LVM 快照未配置、常规备份的 cron 任务有语法错误导致未执行、数据库备份未生成、S3 备份功能未启用。唯一有效的是一份 6 小时前的 pg_dump 备份。数据恢复耗时约 18 小时。

教训:备份机制必须定期验证,未经验证的备份等于没有备份。

Azure 2023:巴西南部区域中断

2023 年,Azure 巴西南部区域因数据中心冷却系统故障导致服务器过热关机。冷却系统的监控告警虽然触发了,但告警被错误分类为低优先级,处理延迟。等到温度超过阈值时,自动保护机制关闭了大量服务器。

教训:监控告警的优先级分类错误和物理基础设施故障一样危险。

Slack 2021:数据库基础设施故障

2021 年初,Slack 的数据库基础设施出现连接问题,导致消息发送延迟和部分功能不可用。故障期间,Slack 的状态页面显示”部分降级”(Partial Degradation),但用户体验的实际影响远大于”部分”所暗示的程度。

教训:状态页面的分级描述需要与用户感知的实际影响对齐,否则会损害用户对状态页面的信任。

十、共性分析与建设性建议

将上述 8 个案例放在一起,可以提炼出几个维度的共性规律。

案例综合对比

下表从根因类别、检测时间、恢复时间和核心教训四个维度对本文分析的 8 个关键事故进行横向对比:

事故 根因类别 检测时间 恢复时间 核心教训
GitHub 2018 自动故障转移/脑裂 约 10 分钟 约 24 小时 短暂网络分区下自动故障转移可能制造更大问题,需增加延迟窗口
Cloudflare 2020 网络配置变更/黑洞路由 约 3 分钟 约 27 分钟 路由协议自动传播使爆炸半径无限大,需在协议层增加准入控制
AWS S3 2017 操作失误/参数错误 数分钟内 约 4 小时 关键操作需参数范围校验,子系统需设计大规模故障下的快速恢复能力
AWS Kinesis 2020 基础设施容量/线程耗尽 数十分钟 约 10 小时以上 集群扩容需考虑线程/连接等系统资源上限,监控不可依赖被监控系统
Google Cloud 2019 配置范围失控/过滤条件错误 数分钟内 约 4 小时 15 分钟 灰度样本必须具有代表性,声明式配置需变更预览功能
Facebook 2021 BGP 路由全量撤回 数分钟内 约 6 小时 带外管理平面不可或缺,恢复手段不能依赖故障系统本身
Roblox 2021 多因素叠加/控制平面过载 数小时 约 73 小时 基础设施组件需独立压力测试,多因素故障的调试复杂度远超单因素
Fastly 2021 软件边缘用例缺陷 约 1 分钟 约 49 分钟 合法输入触发的边缘用例缺陷难以通过传统测试覆盖,需模糊测试补充

从表中可以看出几个显著规律:检测时间普遍在分钟级别,但恢复时间从 27 分钟到 73 小时跨越三个数量级。恢复时间的长短主要取决于两个因素——数据一致性修复的复杂度(如 GitHub 需逐行校验)和故障的多因素叠加程度(如 Roblox 每修复一个问题就暴露新问题)。

典型架构失效模式

在上述案例中,Facebook BGP 事故和 AWS S3 事故代表了两类最具代表性的架构失效模式,值得进一步剖析其架构层面的脆弱点。

Facebook BGP 事故的架构脆弱点

Facebook 的架构中存在一条致命的循环依赖链:用户流量依赖 DNS 解析获取服务器 IP 地址,DNS 服务器部署在 Facebook 自有数据中心内,数据中心的可达性依赖 BGP 路由通告,BGP 路由由骨干网路由器维护。当 BGP 路由被全量撤回时,这条链路从底层断裂:外部网络无法到达 DNS 服务器,DNS 解析失败,所有域名(facebook.com、instagram.com、whatsapp.com)均不可访问。更严重的是,工程师的远程管理通道(VPN、堡垒机)同样依赖这条链路,导致恢复手段与故障系统形成循环依赖。最终只能通过物理前往数据中心手动操作路由器来恢复。这一架构暴露的核心问题是:管理平面(Management Plane)与数据平面(Data Plane)共享同一基础设施,缺少独立的带外管理路径。

AWS S3 事故的架构脆弱点

AWS S3 的架构依赖两个关键子系统:索引子系统(负责查找对象存储位置)和分配子系统(负责分配新存储空间)。这两个子系统在设计时未充分考虑大规模节点同时下线的场景,缺乏快速重建元数据索引的能力,导致恢复耗时远超预期。更深层的问题是 S3 作为 AWS 生态中的”隐式基础设施”——大量 AWS 内部服务和外部客户将静态资源、日志、配置文件托管在 S3 上,但这些依赖关系未被显式管理。S3 故障时,连 AWS 自身的服务状态仪表盘都因为图标文件存储在 S3 上而无法正常渲染。这一案例揭示的核心问题是:当一个服务成为事实上的”平台级依赖”时,其可用性要求远高于普通服务,需要独立于自身的状态报告机制和更保守的操作流程。

触发模式分类

触发类型 占比 典型案例
配置变更 约 50% Cloudflare 2020、Google Cloud 2019、Facebook 2021
操作失误 约 20% AWS S3 2017、GitLab 2017
基础设施容量 约 20% Roblox 2021、AWS Kinesis 2020
软件缺陷 约 10% Fastly 2021

从更系统的角度审视,这些故障的根因可以归纳为以下四层分类体系。第一层是变更引入型(Change-Induced),包括配置变更范围失控(Google Cloud 2019)、路由规则语义错误(Cloudflare 2020)、维护操作副作用(Facebook 2021),其共性特征是变更的预期效果与实际效果存在偏差,且缺乏有效的变更预览和范围约束机制。第二层是操作失误型(Human Error),包括参数输入错误(AWS S3 2017)、终端窗口混淆(GitLab 2017),其共性特征是缺少操作层面的防呆设计(Poka-Yoke),如参数范围校验、环境视觉区分等。第三层是容量突破型(Capacity Breach),包括线程资源耗尽(AWS Kinesis 2020)、存储引擎写放大(Roblox 2021),其共性特征是系统在正常负载下运行良好但在负载增长到特定阈值后行为急剧退化,属于非线性失效模式。第四层是潜伏缺陷型(Latent Defect),包括边缘用例触发的软件缺陷(Fastly 2021)、自动化在特定条件下的错误行为(GitHub 2018 的 Orchestrator),其共性特征是缺陷在部署后长期潜伏,直到特定的输入模式或环境条件触发才暴露。这四层分类不是互斥的——Roblox 2021 同时涉及容量突破和潜伏缺陷,Facebook 2021 兼具变更引入和架构设计的循环依赖问题。

配置变更是最常见的故障触发源。这不是巧合——配置变更的频率远高于代码部署,但配置变更的安全机制往往弱于代码部署。代码部署通常有完整的 CI/CD 流水线(单元测试、集成测试、灰度发布),而配置变更可能只经过人工审核。

传播路径共性

隐式依赖:故障传播最危险的路径是通过隐式依赖(Implicit Dependency)。AWS S3 事故中,大量服务对 S3 有隐式依赖(静态资源托管、日志存储),但这些依赖关系没有被显式管理。Facebook 事故中,门禁系统对内部 DNS 的依赖也是隐式的。

共享控制平面:当多个数据平面(Data Plane)共享同一个控制平面(Control Plane)时,控制平面的故障会同时影响所有数据平面。Roblox 事故中,Consul 就是这样一个共享控制平面。

雷群效应(Thundering Herd):故障恢复后,积压的请求同时涌入可能导致刚恢复的系统再次过载。多个案例的恢复过程中都提到了”分批恢复”的策略,目的就是避免雷群效应。

检测与恢复的瓶颈

监控盲区:AWS Kinesis 2020 事故中,Kinesis 的故障导致 CloudWatch 不可用,而 CloudWatch 正是 AWS 运维团队用来监控 Kinesis 的工具。监控系统对被监控系统有依赖,这是一个经典的循环依赖问题。

恢复手段依赖故障系统:Facebook 2021 事故的核心矛盾是修复网络故障需要访问网络设备,但网络故障本身切断了访问路径。这是”恢复手段依赖故障系统”的典型表现。

压力下的人类决策:多个案例中,工程师在高压环境下做出的决策延长了故障时间。GitHub 团队选择”正确性优先”是一个正确但耗时的决策。Roblox 团队在第一阶段选择扩容而非降载,加重了问题。建立预定义的决策矩阵可以减少压力下的决策延迟。

建设性改进建议

变更管理:配置变更的金丝雀发布

配置变更需要与代码部署同等严格的发布流程。关键要素包括:

# 配置变更金丝雀发布策略
canary_config:
  stages:
    - name: "单节点验证"
      targets: 1
      bake_time: 10m
      rollback_trigger:
        - error_rate > 0.1%
        - latency_p99 > baseline * 2
    - name: "单可用区验证"
      targets: "az-us-east-1a"
      bake_time: 30m
    - name: "单区域发布"
      targets: "region-us-east-1"
      bake_time: 2h
    - name: "全球发布"
      targets: "all"
      bake_time: 24h
  rollback:
    auto: true
    max_rollback_time: 60s

爆炸半径控制:单元化架构

单元化架构将系统划分为多个独立的单元(Cell),每个单元服务一部分用户,拥有独立的数据存储、计算资源和控制平面。一个单元的故障不会影响其他单元。AWS 在多次事故后大力推进单元化架构,将其称为”Availability Zone Independence”。

依赖管理:消除控制平面的循环依赖

明确列出系统的依赖图谱(Dependency Graph),特别关注以下模式:

发现上述模式时,需要通过引入带外路径来打破循环。

恢复能力:带外管理平面

带外管理平面(Out-of-Band Management Plane)是一套独立于主生产网络的管理基础设施,包含独立的网络链路、独立的 DNS 解析路径、独立的认证系统、独立的通信工具。当主生产网络完全不可用时,运维团队仍然可以通过带外管理平面访问和控制基础设施。

测试:故障注入与演练

定期进行故障注入测试(参见本系列第 56 篇混沌工程)。重点测试场景包括:

文化:无责复盘

无责复盘(Blameless Postmortem)的核心原则是:聚焦系统和流程的改进,而非追究个人责任。GitLab 2017 事故中,那名在错误终端执行命令的工程师不应该被指责——应该被质疑的是为什么生产环境的终端窗口和测试环境的终端窗口在外观上没有区别,为什么备份系统有 4 种处于失效状态而无人察觉。

无责不等于无为。每次复盘必须产出具体的、可追踪的改进行动项(Action Items),并分配明确的负责人和截止日期。

工程实践清单

以下是从这些案例中提炼的具体工程实践:

实践 目标 关联案例
配置变更灰度发布 限制配置变更的爆炸半径 Cloudflare、Google Cloud
变更预览(dry-run) 变更执行前显示影响范围 Google Cloud 2019
参数范围校验 拒绝明显异常的输入值 AWS S3 2017
BGP 前缀撤回限制 禁止一次性撤回所有路由 Facebook 2021
带外管理网络 确保故障时仍可访问基础设施 Facebook 2021
备份验证自动化 定期验证备份的可恢复性 GitLab 2017
基础设施压力测试 验证基础设施组件的容量上限 Roblox 2021
监控系统独立部署 避免监控与被监控的循环依赖 AWS Kinesis 2020
生产环境视觉区分 终端窗口区分生产/测试环境 GitLab 2017
无责复盘流程 聚焦系统改进而非追责 所有案例

参考文献

  1. GitHub. “October 21 post-incident analysis.” GitHub Blog, 2018. https://github.blog/2018-10-30-oct21-post-incident-analysis/
  2. Cloudflare. “Cloudflare outage on July 17, 2020.” Cloudflare Blog, 2020. https://blog.cloudflare.com/cloudflare-outage-on-july-17-2020/
  3. Amazon Web Services. “Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) Region.” AWS Post-Event Summaries, 2017. https://aws.amazon.com/message/41926/
  4. Amazon Web Services. “Summary of the Amazon Kinesis Event in the Northern Virginia (US-EAST-1) Region.” AWS Post-Event Summaries, 2020. https://aws.amazon.com/message/11201/
  5. Amazon Web Services. “Summary of the AWS Service Event in the Northern Virginia (US-EAST-1) Region.” AWS Post-Event Summaries, 2021. https://aws.amazon.com/message/12721/
  6. Google Cloud. “An update on Sunday’s service disruption.” Google Cloud Blog, 2019. https://cloud.google.com/blog/topics/inside-google-cloud/an-update-on-sundays-service-disruption
  7. Facebook Engineering. “More details about the October 4 outage.” Facebook Engineering Blog, 2021. https://engineering.fb.com/2021/10/05/networking-traffic/outage-details/
  8. Roblox. “Roblox Return to Service 10/28-10/31 2021.” Roblox Blog, 2022. https://blog.roblox.com/2022/01/roblox-return-to-service-10-28-10-31-2021/
  9. Fastly. “Summary of June 8 outage.” Fastly Blog, 2021. https://www.fastly.com/blog/summary-of-june-8-outage
  10. GitLab. “Postmortem of database outage of January 31.” GitLab Blog, 2017. https://about.gitlab.com/blog/2017/02/10/postmortem-of-database-outage-of-january-31/

prev: 混沌工程 next: RPC 框架内核

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .