值得了解的三个故障概念
欢迎
分布式系统以特定模式发生故障。一旦你掌握了这些模式,每一份事后复盘报告都会变成一种识别练习,而非一场谜团。
三个概念涵盖了生产环境故障分析中最重要的部分:
单点故障 (SPOF):其故障会导致更大系统宕机的组件。通常隐蔽:所有人都依赖的 DNS 服务器;所有服务都向其续期的证书;唯一的数据库主节点。
级联故障:一个组件的故障触发另一个组件的故障,进而触发更多故障。缓慢的数据库导致 API 层超时,超时引发重试,重试进一步加重数据库负载,从而导致更多超时。爆炸范围不断扩散。
爆炸半径:当系统中的一个部件失效时,有多少部分系统会随之宕机。架构选择要么限制要么不限制爆炸半径。单点故障(SPOF)具有无界的爆炸半径。采用舱壁隔离的服务具有有界的爆炸半径。
完成本课后,你将能够:
- 通过检查识别架构中的单点故障(SPOF)
- 识别级联故障模式:惊群效应(thundering herd)、重试风暴(retry storm)、死亡队列(queue of death)
- 阅读真实时间线,并将触发因素与触发因素所暴露的潜在缺陷区分开来
- 编写无指责(blameless)的行动项,针对系统而非个人,涵盖预防 / 检测 / 恢复
- 将舱壁(bulkheads)和断路器(circuit breakers)视为限制爆炸半径的工具进行推理
发现单点故障
分层架构检查
考虑一个小型 Web 架构:
- DNS:api.example.com -> 由一家 DNS 提供商托管的单个名称服务器 IP 203.0.113.10
- CDN:api.example.com 前端仅使用单一 CDN 供应商
- 入口:负载均衡器后方的两台反向代理机器
- 后端:分布在两个可用区中的六个 API 副本(每个可用区三个)
- 数据库:一个主节点 + 一个只读副本,位于同一可用区
- 缓存:Redis 集群,三个节点分布在相同的两个可用区内
问题:哪些组件是单点故障(SPOF)?提示:SPOF 并不总是显而易见的“单台机器”类型。如果三个机器组成的集群全部位于同一个可用区,那么对于该可用区的故障而言,它就是一个 SPOF。
三种经典级联模式
故障通过依赖关系传播
模式 1:惊群效应(Thundering herd)。 共享资源(缓存、锁、数据库)发生故障或重启。所有依赖该资源的客户端同时发起重试。这股洪流压垮了刚刚恢复的服务;重试请求的堆积速度超过了恢复所能吸收的速度;恢复过程永远无法完成。
模式 2:重试风暴(Retry storm)。 下游服务响应变慢。上游调用方没有选择失败,而是发起重试。重试使原始负载成倍增加。变慢的服务因此更加迟缓,从而触发更多的重试。最终,负载甚至超过了该服务在健康状态下的处理能力。
模式 3:死亡队列(Queue of death)。 一个没有背压机制的处理队列,其接收速度超过了处理速度。队列无限增长。内存耗尽;消费者崩溃;重启后;发现队列变得更大;再次崩溃。
共同点:微小的初始扰动触发了正反馈循环。系统自身的响应放大了故障,而不是将其抑制。
阻尼机制
带抖动的指数退避。 重试的客户端每次等待时间更长,并带有随机偏移量。防止同步的重试浪潮。
熔断器。 调用方跟踪下游的失败率。超过阈值后,调用方在冷却期内停止调用,并立即使自身请求失败。防止无效工作,让下游得以恢复。
舱壁隔离。 按依赖项隔离资源。数据库使用连接池 A,缓存使用独立的连接池 B。缓慢的数据库无法耗尽所有连接;缓存调用继续。
负载丢弃。 当过载时,在边缘丢弃请求,而不是接受它们并缓慢失败。1 毫秒内的 429 优于 30 秒内的 500。
背压。 当消费者无法跟上时,减慢生产者。队列变得有界;发送方阻塞;工作的原始来源感受到摩擦。
诊断级联故障
一个团队的 API 层在例行数据库故障转移期间崩溃。时间线:
- 14:00:00 — 操作员提升备用数据库为主库。预计不可用时间:约 10 秒。
- 14:00:08 — 主库不可用。API 层请求开始因数据库连接错误而失败。
- 14:00:08 — API 层进行重试(默认配置:5 次重试,无退避策略,间隔 100 毫秒)。
- 14:00:11 — 备用库提升完成,开始接受新连接。
- 14:00:11 — API 层同时建立数千个新的数据库连接(每个副本 × 每个并发请求 × 每次重试)。
- 14:00:13 — 新主库的连接池耗尽;新连接被拒绝。
- 14:00:13-14:05:00 — API 层副本耗尽连接池,抛出异常,崩溃,重启,循环往复。
- 14:05:00 — 操作员手动停止 API 层流量;数据库恢复稳定。
- 14:10:00 — 流量逐步恢复完成。总停机时间:约 10 分钟(预期约为 10 秒)。
DNS SERVFAIL:两个叠加的缺陷
一个真实形态的复盘
以下是某次真实事件的脱敏版本。供应商名称已更改,IP 地址已匿名化;但事件的形态、时间线及经验教训均为真实。
摘要
站点 example.com 在所有公共 DNS 解析器上返回 SERVFAIL,持续约 3-4 小时。同一 DNS 主服务器上的其他 46 个区域均未受影响。根本原因:两个叠加的缺陷。
1. 供应商 A(一家二级 DNS 提供商)添加了一个新的内部同步 IP,该 IP 不在主服务器的 allow-axfr-ips 允许列表中。
2. example.com 区域存在一个多年前的违反 RFC 标准的 CNAME 冲突(demo.example.com 在同一标签下同时拥有 CNAME 和 MX/TXT 记录),导致供应商 A 在新鲜 AXFR 时拒绝该区域。
时间线(UTC)
- ~15:00 — 供应商 A 在其基础设施中添加了新的同步 IP 198.51.100.42
- 15:02 — 主 DNS 日志中首次出现针对 198.51.100.42 的 AXFR-out denied(未对此信号设置告警)
- ~18:00 — 达到 SOA 过期窗口;供应商 A 从缓存中丢弃了 example.com 区域
- ~18:30 — 外部检测到 SERVFAIL
- ~19:45 — 确定根本原因
- 20:00 — 将 198.51.100.42 添加到 allow-axfr-ips;重启主服务器
- 20:05 — 发送 NOTIFY;启动 AXFR;区域仍然 SERVFAIL(CNAME 冲突)
- 20:07 — check-zone 显示 1 个错误:demo.example.com 上的 CNAME 冲突
- 20:09 — CNAME 记录已替换为 A 记录;区域检查通过(0 个错误)
- 20:10 — 已发送 NOTIFY;AXFR 完成;供应商 A 开始提供该区域服务
- 20:11 — dig @8.8.8.8 example.com A 返回正确的 IP — 已解决
为什么只有 example.com 受影响?
所有 47 个区域共享同一个 DNS 主服务器。AXFR IP 封锁影响了所有区域。但只有 example.com 存在 CNAME 冲突,且只有在拒绝访问生效的那一刻,example.com 才需要新的 AXFR。其他区域要么在拒绝访问之前已经刷新,要么当时尚不需要刷新。
潜在缺陷
demo.example.com 处的 CNAME 冲突已存在多年。它之所以能正常工作,是因为主服务器从其数据库提供区域服务(对 RFC 违规较为宽容),而供应商 A 正在使用违规引入之前的陈旧缓存数据提供服务。当供应商 A 丢弃其缓存并需要新数据时,该违规问题便暴露出来。
触发因素
供应商 A 悄悄添加了一个新的同步 IP。主节点的白名单中并未包含该 IP,导致 AXFR 请求被拒绝。三个小时后(SOA 过期时间到达),供应商 A 删除了该区域。当系统尝试恢复时,这一潜在缺陷暴露了出来。
撰写无责行动项
无责原则:针对系统,而非个人
无责行动项应指明系统应如何改进,而非个人应如何改变。“培训操作员”属于有责指责;“添加一项自动化检查,以便在部署前捕获此类问题”则属于无责改进。
优秀的无责行动项通常集中在以下三个维度:
- 预防:使不良情况更难发生或变得不可能
- 检测:若问题发生,能更早地察觉
- 恢复:当问题发生时,将损害降至最低
每项内容应明确:(1)具体的系统变更,(2)负责团队,以及(3)其所属的维度。
随船沉没的隔舱
借鉴自船舶工程
船舶配备有防水舱壁:将船体分隔成不同舱室的垂直墙壁。一个舱室进水不会导致整艘船沉没;另一个舱室失效也不会影响其余部分。
分布式系统借用了同样的术语和同样的理念。
舱壁模式:按依赖项隔离资源。一个调用三个下游 API 的服务使用三个独立的连接池、三个独立的线程池和三个独立的重试预算。缓慢或失败的下游无法消耗分配给另外两个下游的资源。
没有舱壁:一个缓慢的依赖项耗尽共享线程池;对其他依赖项的调用因等待线程而阻塞;整个服务变得无响应。
有舱壁:一个缓慢的依赖项耗尽其专属池;对它的调用快速失败;对其他依赖项的调用正常继续;爆炸半径被限制在失败的依赖项范围内。
熔断器
熔断器模式(Circuit breaker pattern):一种围绕下游依赖的状态化包装器,用于跟踪失败率。它包含三种状态:
- 闭合(正常):调用正常通过。失败会被计数。
- 打开(触发):当失败率超过阈值(例如,过去 30 秒内失败率超过 50%)时,熔断器打开。调用会立即失败,而不会尝试访问依赖服务。这避免了调用方浪费工作,也防止了不健康的依赖服务继续承受负载。
- 半开(测试):在冷却期结束后,熔断器允许少量调用通过。如果这些调用成功,熔断器将恢复为闭合状态(正常)。如果失败,它将重新打开并进入下一个冷却期。
关键洞察:熔断器在已知不健康的期间防止了无效努力,& 让下游服务有机会在不持续承受负载的情况下恢复。
舱壁模式(Bulkheads)限制了爆炸半径。熔断器(Circuit breakers)防止爆炸持续自我维持。
限制爆炸半径
你的 API 服务调用了四个下游服务:用户服务(User Service)、推荐服务(Recommendation Service)、通知服务(Notification Service)& 第三方支付 API。团队听说“推荐服务最近有点不稳定”,并希望确保当它失败时,系统的其余部分保持健康。
今天,该服务使用一个包含 200 个线程的单一共享线程池和一个单一共享 HTTP 连接池。所有四个下游服务都在争夺这些资源。目前没有任何熔断器。
设计故障模式评审
综合总结
你已学会通过检查识别单点故障(SPOF),识别级联故障模式,在解读事后分析(postmortem)时区分触发因素与潜在缺陷,编写涵盖预防 / 检测 / 恢复的无责行动项,以及通过舱壁隔离 + 熔断器 + 优雅降级来限制爆炸半径。
应用以上全部五项技能。
你的团队即将上线一个新服务 search.example.com,该服务依赖三个下游服务:主搜索索引(index.example.com)、分析服务(analytics.example.com)以及推荐服务(recs.example.com)。团队希望你在上线前主导一次“故障模式评审”。
本课程后续方向
本课程后续方向
你现在能够识别 SPOF(单点故障),识别级联故障,有效地阅读事后分析报告,撰写无责行动项,并通过设计来限制爆炸半径。
本课程的最后一课(cs_distsys_observability_and_capacity)将教你测量哪些指标,以便在用户发现问题之前提前察觉问题。内容包括健康检查、版本端点、代理层的四个黄金信号,以及突发容量决策如何与观测数据相关联。
配套课程:geometry_of_failure_modes_and_blast_radius 推导了介数中心性(哪个图节点是瓶颈)和最小割(爆炸半径的上限)。
做得很好。继续前进。