一次合法的配置变更,85% 的网络瘫痪:Fastly 2021 年 6 月 8 日宕机与那只沉睡了 27 天的 Bug
Published:
先看数字:85%。2021 年 6 月 8 日,Fastly 边缘网络的 85% 开始返回错误——不是某一个客户的站点、不是某一个区域,而是几乎整张网络同时出问题。扳机不是攻击、不是流量洪峰、不是当天早上的一次坏部署,而是一位客户推送的一次合法配置变更,唤醒了一个自 5 月 12 日起就躺在 Fastly 已部署代码里的软件 Bug——27 天的沉睡,被一次普通的、经过审批的、格式正确的输入终结。
这是 🔥 Incident Recap 系列第三篇,也是迄今为止三篇里最让人不安的一篇。Meta BGP 宕机(第 1 篇)有一个戏剧性且解释完整的机制:一条错误命令撤掉了网络自己的路由,还把工程师锁在了自己的机房门外。Cloudflare 正则宕机(第 2 篇)公布了出事的 pattern、一步步数清了它的求值步数、列出了十一个叠加原因。Fastly 这次宕机则有某种更奇怪的东西:一篇对时间线异常坦率、异常迅速的 postmortem,却对 Bug 本身几乎只字不提。Fastly 从未公布这个 Bug 是什么、那条配置里写了什么、是哪位客户推送的。它公布的东西——时间线、百分比、恢复顺序、修复方向——足以精确重建这次故障的形状,也足以明确标出公开记录在哪里止步。这条边界很重要,本篇会始终让它可见:来自 Fastly 帖子的事实就写成事实;其余的一律标为分析。
背景:CDN 是一台共享机器,靠每个客户自己的转向来区分
Fastly 在同一篇帖子里对自己的描述很短:它的边缘云平台帮助热门数字业务交付快速、安全、可扩展的在线体验。把这句话拆成机制来看,因为整次事故就活在架构里。
内容分发网络(CDN)把服务器——存在点(POP)——放到离用户近的地方,从那里提供缓存内容,而不是让每一次请求都回到客户的源站。这一设计的两个属性决定了这次事故:
第一,边缘是共享的。每个 POP 都为所有客户运行同一套边缘软件。你的站点流量和另一家公司的站点流量穿过同样的代码、跑在同样的机器上,彼此的分隔靠的是配置,而不是硬件。配置就是让一支共享舰队表现得像成千上万个不同前门的那个东西:每个客户的服务定义告诉边缘,如何为该客户路由、缓存、转换请求。
第二,一次配置变更就是一次生产部署。客户推送配置变更时,那不是归档到某处的一张便条——而是边缘舰队即将执行的输入,而且是全球执行。配置必须到达各个 POP 并在那里生效,因为一个提供过期配置的 CDN,就是一个在提供错误站点行为的 CDN。配置传播的速度是客户付费购买的功能。但 6 月 8 日证明,它同时也是一个爆炸半径的决定:新配置做什么,就在所有地方、很快地做什么。
把这两件事放在一起,事故的核心悖论就解开了:一个客户的变更怎么能搞垮服务成千上万客户的网络的 85%?因为这条变更没有搞坏该客户自己的站点。它触发了共享代码路径里的一个 Bug——所有客户流量都要流经的代码——而 Bug 的影响并不局限于触发它的那个客户。输入是分租户的,故障却在公共部分。
时间线(UTC,2021 年 6 月 8 日)
| 时间(UTC) | 事件 |
|---|---|
| 2021 年 5 月 12 日 | Fastly 开始一次软件部署,引入了这个 Bug。 |
| 6 月 8 日清晨 | 一位客户推送了一次合法的配置变更,其中包含了触发 Bug 的特定条件。 |
| 09:47 | 全球 disruption 开始。 |
| 09:48 | Fastly 监控发现全球 disruption——一分钟内。 |
| 09:58 | 状态帖发布。 |
| 10:27 | Fastly 工程团队定位到出事的客户配置。 |
| 10:36 | 受影响服务开始恢复。 |
| 11:00 | 多数服务恢复。 |
| 12:35 | 事故得到缓解(mitigated)。 |
| 12:44 | 状态帖标记为已解决。 |
| 17:25 | 永久 Bug 修复开始部署。 |
先做一处必要的对账,因为 Fastly 自己的数字需要小心对待。帖子标题性的说法是“49 分钟内,95% 的网络恢复正常运行”。09:47 加 49 分钟是 10:36——时间线上说的是受影响服务开始恢复的时刻,而不是恢复完成的时刻。时间线自己标出的更广泛恢复要晚得多:11:00 多数服务恢复,12:35 才算缓解。这些数字并不互相矛盾,它们是不同的阈值——网络恢复正常运行、服务恢复、事故缓解——但绝不能把它们压成一句“49 分钟修好了”。诚实的读法是:触发输入在事发后约 49 分钟内被中和,多数服务约在 11:00 回来,事故直到近三小时后才被宣布缓解。三个数字都是 Fastly 的;本篇让它们保持分开。
故障传播:一个输入、一个共享故障、一个全球症状
这张图故意画得很简单,因为故障在顶层本来就很简单。深度在为什么每一步成为可能。这就是本系列的质量线所在:不是复述时间线,而是讲清每一步背后的机制。
放大器 1:共命——一个租户的输入,所有人的数据路径
把一次配置变更放大成 85% 宕机的机制,是经由共享代码的共命(fate-sharing)。在多租户边缘里,隔离边界本应是配置:你的配置指挥你的流量,我的配置指挥我的流量,谁也不该影响谁。这条边界只对正确的代码成立。而解释配置的代码里的 Bug,不在任何一个租户的边界之内——它在所有租户的下面。触发配置到达时,它没有搞乱某一个客户的路由,而是把共享解释器推进了一个故障模式,此后穿过这条代码路径的每一次请求都会撞上同一个故障,不管它属于哪个客户。
这正是这次宕机“广泛而严重”却没有任何一个客户做错事的精确含义。客户推送的不是畸形变更;Fastly 强调它是合法的。合法性由配置系统判定——形状、模式、权限——而这条变更全部通过了。故障不在输入里,而在共享代码如何处理某一类合法输入。爆炸半径由共享代码路径定义、而不是由租户边界定义的系统,永远留着这样一个故障模式:任何一个租户都可能在无意中递上找到它的那个输入。
注意这条放大器没有断言什么。Fastly 没有说 Bug 搞垮了进程、污染了缓存、或者让 POP 下线——本篇也不会凭空编造这个细节。已公布的、可观察的事实更窄,也已经足够:网络以 85% 的规模返回错误。解释这个规模的机制是共享代码路径;内部故障本身没有公开,下面的“Postmortem 没说什么”一节会直说这一点。
放大器 2:为什么是 27 天——潜伏 Bug 就是触发条件还没被试过的 Bug
一个 5 月 12 日引入、6 月 8 日才第一次触发的 Bug,不是在 6 月 8 日“出现”的。它在生产环境里活了 27 天,处理着每一次到达它那条代码路径的请求,并且全部返回了正确结果——因为没有一次请求携带它处理不了的那种特定配置条件。一旦数清输入空间,这种潜伏就不再神秘,而是理所当然。
一条 CDN 边缘代码路径的输入不是一种,而是一个叉乘:成千上万客户,每人许多设置,每项设置许多取值,而且每天都有客户推送变更,组合还在不断增加。测试可以覆盖代码路径,也可以覆盖有代表性的配置,但不可能枚举这个叉乘——空间是组合级的,每一次客户变更都会加入测试套件从未跑过的新组合。一个只在某一种特定组合上触发的 Bug,因此同时具有两个性质:无法靠枚举测出来,又对日常流量完全隐形。27 天的安静不是部署安全的证据,而是触发组合尚未出现的证据。Fastly 工程与基础设施高级副总裁 Nick Rockwell 在帖子里用另一种方式写了同一句话:“即便触发条件很特定,我们本应预见到它。”
“合法”这个词把这一点说得更尖锐。如果变更是畸形的,校验也许能拦住它——畸形输入正是配置系统生来要拒绝的故障模式。而一条触发了平台 Bug 的合法变更,会按设计绕过所有输入侧护栏,因为护栏评判的是输入,而输入没有问题。故障在下一层:平台对某一类输入的处理,而它的测试从未演练过这一类。所以“客户推送了一次合法变更”既不是为平台开脱,也不是指责客户,而是对一类输入校验防不住的故障的精确描述:正确的输入,错误的处理。
放大器 3:发现用 1 分钟,定位用 40 分钟——全球性症状没有地址
时间线里最有教益的一段,是 09:48 到 10:27 之间。监控一分钟内就抓住了 disruption——错误率同时在各处飙升。定位到出事的客户配置却又花了 39 分钟。为什么看到结果比找到原因快四十倍?
因为一个全球性、同时发生的症状没有位置信号。一个 POP 挂了,你就去查那个 POP;一个区域劣化,你就去查那个区域;错误率在同一瞬间于所有地方跳升,故障一定在某个全球性的东西里——共享软件,或者共享输入——而“所有地方”这个信息本身,并没有告诉你是哪一个全球输入。搜索空间是事发窗口之前推送到全舰队的每一次变更:软件部署,以及每个客户的每一次配置变更。症状指向的是一个类别(全球输入),而不是一个客户;团队必须把事发时间与配置推送时间戳对上、筛出候选变更、确认哪一个能复现故障——而这期间网络一直是断的,搜索的每一分钟都是宕机的每一分钟。
这种不对称是结构性的,不是当天团队的问题。发现的速度随监控扩展:一个阈值、一个告警、一分钟。定位的速度随候选全球输入的数量扩展,而且它需要另一种工具:全舰队配置变更的完整、带时间戳、可查询的日志,以及一种办法——不用再次推向生产就能拿可疑配置对着当前代码做测试。40 分钟就是 2021 年 6 月这套工具和流程花掉的时间。任何一个跑边缘的运营者读到这里都该问自己:如果明天我们的某位客户推送了下一个触发配置,从“各处都在报错”到“就是这个配置、这个客户”,需要几分钟?这个数字,而不是发现的数字,才是真正决定宕机长度的恢复预算。
放大器 4:禁用配置 9 分钟恢复了服务——但那不是修复
从 10:27 定位到 10:36 开始恢复,只隔了 9 分钟,机制值得原样说清:拿掉触发输入,Bug 就不再被触发。Bug 还在代码里,代码路径还是共享的。但在特定的配置条件消失后,没有任何请求再携带触发它的组合,舰队对实际收到的输入恢复了正确行为。这就是为什么缓解不需要一次代码部署、而且一旦定位到配置就能很快:杠杆是数据(配置),不是代码。
边界同样精确,Fastly 的时间线也把它摆出来了。被禁用的配置是一种被撤销的输入,而不是一个被修好的系统。推送它的客户不重新启用就没事;一旦重新启用,宕机就会重演。Bug 仍然躺在那里,等着任何一个能重建触发条件的未来配置。所以永久修复是另一件独立的事、发生在数小时之后——代码修复的部署 17:25 才开始、事故 12:35 缓解之后——而“12:35 缓解”和“17:25 开始部署修复”是两个绝不能合并的里程碑。缓解是靠缩小系统被允许做的事来换回可用性;修复是恢复完整能力。把两者混为一谈,就是在 10:36 宣布胜利、然后被同一类 Bug 再次 surprise 的办法。
放大器 5:长尾——边缘恢复不等于系统恢复
时间线里最后一个机制,是“服务开始恢复”(10:36)、“多数恢复”(11:00)、“缓解”(12:35)之间的空隙。这一节是分析,不是 Fastly 的说法:Fastly 在帖子里没有解释这段空隙。但这个形状对所有 CDN 事故都很熟悉,而且它直接来自 CDN 是什么。
边缘停止返回错误时,系统并没有回到宕机前的状态。宕机期间缓存内容过期或被绕过;流量回来时,超出正常比例的请求缓存未命中、涌向源站,源站在边缘重新填满缓存的同时承受着一波它并非为此 sizing 的 surge。错误率可以先恢复,而延迟持续偏高、源站持续发热;运营者要等到第二波平息,才能诚实地宣布事故缓解。时间线里两个小时的尾巴与这个第二阶段吻合,也是为什么诚实的恢复指标是“缓解”、而不是“第一个服务恢复”。只引用第一个恢复数字的事故报告,都是把恢复的开始当成恢复的结束,报给所有下游。
Fastly 改了什么——以及每一项改不了什么
Fastly 的帖子列了短期行动和长期方向。对照上面的机制逐项读,包括每一项改不了的部分:
- 把永久 Bug 修复部署到全网。 6 月 8 日 17:25 开始。去掉的是这个 Bug,不是这一类:下一处共享代码里的潜伏 Bug、被下一个未经测试的配置组合触发,完全不受这次修复的影响。修 Bug 是修一个实例,不是给这一类上控制。
- 对流程和实践做完整 postmortem,搞清 QA 为什么没发现这个 Bug。 这是正确的调查,瞄准的正是放大器 2 的问题:测代码路径不等于测配置叉乘。注意边界:6 月 8 日的帖子承诺了这次调查,并没有公布它的结论。2026 年的读者无法从这篇帖子验证 QA 后来改了什么,因为这篇帖子是事故总结,不是后续。
- 评估缩短缓解时间的办法。 瞄准的是放大器 3 的空隙:从发现到定位的四十分钟,以及决定这段时间的机器(配置变更归因、可疑配置测试)。更快的缓解防不住下一次触发;它限定下一次触发能烧多久。
- 在 WebAssembly 与 Compute@Edge 的隔离能力上,从底层建设更大的韧性。 这是结构性的赌注,值得精确地读。隔离收容的是执行故障:如果一个租户的代码跑在自己的 WebAssembly 沙箱里,该租户代码的故障就无法搞垮共享进程、污染邻居状态。相对共享进程模型,这是真实的收容改进。但它本身修不了 6 月 8 日的形状:平台自己处理配置的 Bug,位于租户沙箱之下或之旁,还配着一条全球分发通道。隔离限定的是租户与租户之间的伤害,不限定平台与所有人之间的伤害,也不解决配置组合测试或定位速度。Fastly 的措辞很谨慎——“建设更大的韧性”,不是“防止复发”——本篇保持同样的谨慎。
作为一个集合,这套修复对其形状是诚实的:修实例、调查测试缺口、缩短下一次恢复、在一个相关但不同的故障类上投资收容。相对本系列里的 Cloudflare postmortem,缺的是一个直接瞄准配置爆炸半径本身的控制——分阶段推送配置、先把配置 canary 到一部分 POP 再全球生效、或者把单个客户配置的作用域限定到让它的故障无法经由共享代码传播。Fastly 的帖子没有提到任何一项,本篇也不会把它们算成 Fastly 的承诺。它们是任何跑同样架构的运营者显而易见的下一个问题,这里作为问题列出,而不是作为 Fastly 的承诺。
值得偷走的教训
- 在多租户系统里,爆炸半径由共享代码定义,而不是由租户边界定义。 如果所有租户都流经同一个解释器、同一个校验器、同一个配置处理器,那里的 Bug 就是一个带分租户扳机的全球 Bug。先审计你的共享路径;那里就是一个客户的意外变成所有人宕机的地方。
- 合法输入才是最危险的输入类。 畸形输入会被为它而建的护栏拒绝。被平台 Bug 错误处理的合法输入会通过所有关卡,因为关卡评判的是输入,而输入没有问题。测试必须覆盖平台对合法但不寻常组合的行为,而不只是对非法输入的拒绝。
- 潜伏 Bug 的安静期什么也证明不了。 27 天行为正确不是 27 天安全的证据,而是触发组合 27 天没有出现。当有人引用“已经跑了几周都没事”时,明确指出它对组合覆盖率不提供任何信息。
- 发现速度和定位速度是两笔不同的预算——两笔都要量。 一分钟发现、四十分钟定位,是全球性症状的正常形状,而四十这个数字才决定宕机长度。如果你只看发现时间的仪表盘,你量的只是容易的那一半。把配置变更归因做成工具:带时间戳、可查询、不用推生产就能测试。
- 靠拿掉输入来缓解,又快又不完整——标注清楚。 禁用一条配置、一条规则、一个功能,是靠缩小能力换回可用性。把它报成缓解,让 Bug 保持 open,不要让恢复时间戳把修复时间戳吃掉。
- 恢复有两个阶段;引用第二个。 边缘起来不等于系统起来。缓存重新填充、源站 surge、拖到“缓解”的长尾,都是事故的一部分;只引用第一个恢复数字,就是向所有下游低估这次宕机。
- 隔离收容的是租户,不是平台。 WebAssembly 沙箱、进程隔离、分租户 worker,都是对付 noisy-neighbor 执行故障的真实防御。但它们修不了平台层解释租户输入的 Bug,也修不了这份输入的全球分发。在宣称覆盖之前,先说清每一道防御护的是哪一层。
Postmortem 没说什么
这一节是本篇的诚实账本,而且它比往常长,因为 Fastly 的帖子比往常短。帖子没有披露:Bug 是什么(除了“软件 Bug”,没有代码、没有故障描述);客户配置里写了什么;是哪位客户推送的;影响了多少客户或请求(只有一个 85% 的网络数字);QA 为什么漏掉它(承诺了后续调查,结论不在这篇帖子里);承诺的后续行动是否完成(帖子给的是修复部署的开始时间,不是完成确认)。当时的报道补了一些用户可见的细节——错误页面、知名受影响站点的名单——但本篇的纪律是一手来源,所以那些细节在这里不作为既定事实出现;上面引用的 Fastly 数字,就是本篇依赖的全部定量记录。
与 Incident Recap 第 2 篇的对照就是要点所在,不是抱怨。Cloudflare 公布了出事的正则、数清了它的求值步数、列了十一个叠加原因、给修复标了日期。Fastly 公布了时间线、两个百分比、一封道歉信、一个方向。两篇帖子都诚实;诚实的深度不同,而深度差本身就是信息。Postmortem 的深度决定了行业其余人能从中学到多少。Fastly 的帖子把架构一课教得异常清楚——共享代码、全球配置、定位时间;它没有教 Bug 本身那一课,因为 Bug 不在里面。想要第二课的读者在这里找不到,本篇宁可直说,也不编造。
关于这篇帖子还有一句话值得说,因为尽管事故严重,它在工程师圈子里仍被温和地记住。Rockwell 的结尾——“即便触发条件很特定,我们本应预见到它”——拒绝了事故写作里最诱人的一句话:条件很特定,所以宕机不可预见。触发条件的特定性不等于故障类别的不可预见性。每一次潜伏 Bug 宕机都有特定的条件;“潜伏”就是这个意思。如果你面对的是一个组合级的组合空间、而你的测试没有覆盖它,那么类别是可预见的,哪怕实例不是。这就是本系列用来衡量每一篇 postmortem 的标准,包括 Fastly 自己的这一篇。
Sources
- Nick Rockwell, “Summary of June 8 outage,” Fastly Blog, June 8, 2021. https://www.fastly.com/blog/summary-of-june-8-outage — 本篇全部定量内容的一手来源(除非另有说明):5 月 12 日引入 Bug 的部署、6 月 8 日合法的客户配置变更、全网 85% 返回错误、一分钟内发现、完整 UTC 时间线(09:47 开始、09:48 发现、09:58 状态帖、10:27 定位到配置、10:36 开始恢复、11:00 多数恢复、12:35 缓解、12:44 状态解决、17:25 修复开始部署)、“49 分钟内 95% 网络恢复正常运行”的说法、短期行动、WebAssembly / Compute@Edge 隔离方向,以及结尾“我们本应预见到它”那句话。博客引用的 09:58 UTC 状态帖在研究时无法直接取回(状态页返回访问拒绝),因此本篇没有任何断言依赖于它。
- 标为分析的章节——共命机制、对 27 天沉睡的组合级解释、发现与定位的不对称、拿掉输入的缓解机制、缓存重填的恢复长尾解释、以及围绕 WebAssembly 隔离划出的边界——是作者基于博客事实和标准 CDN 架构的推理,不是 Fastly 的说法。Fastly 沉默的地方(Bug 内部机制、配置内容、受影响客户数量、QA 结论),本篇直说沉默,而不是填补空白。
Share on
Twitter Facebook LinkedIn☕ Buy me a coffee! 💝
If you found this article helpful, consider buying me a coffee to support my work! 🚀
