控制面自己把自己吃掉了:一次扩容任务与失效的退避,如何让 AWS us-east-1 瘫痪七小时

6 minute read

Published:

English version 英文版

这是 🔥 Incident Recap 系列第四篇。

先看一个框定整起事故的数字:6 小时 52 分钟。这是从太平洋时间早上 7:30(一次自动扩容活动触发事故)到下午 2:22(处于事故核心的网络设备完全恢复)之间的间隔。但更重要的数字是另一个:零。零台正在运行的 EC2 实例宕机。零个 S3 对象通过正常数据路径变得不可达。零张 DynamoDB 表停止服务。2021 年最大的一次云宕机并没有打垮云本身,它打垮的是管理云的那套机器——然后,因为这套机器里也包括 AWS 用来看见自己、修复自己的工具,它又打垮了 AWS 应对自身事故的能力。

这就是本次事故的形状,也是它值得进入这个系列的原因。Meta BGP 宕机(第 1 篇)把工程师锁在了自己的机房门外。Cloudflare 正则宕机(第 2 篇)是一个坏 pattern 被十一个叠加原因放大。Fastly 宕机(第 3 篇)是一次合法配置变更唤醒了共享代码里的潜伏 Bug。这一次是第四种形状:一个自我喂养的反馈回路,发生在一张网络上——那张网络的任务是连接 AWS 的两个一半——以及一道护栏(客户端的重试退避)失效了:它工作了多年,却恰恰在这一天没有生效。

背景:AWS 是两张网络,而连接它们的桥是承重结构

AWS 于 2021 年 12 月 10 日发布的官方 postmortem,开篇就是一个大多数 AWS 客户永远不需要知道的架构事实。绝大多数 AWS 服务、以及全部客户应用,都运行在主 AWS 网络里。但 AWS 还运行着一张内部网络,上面托管着基础性服务:监控、内部 DNS、鉴权服务,以及 EC2 控制面的一部分。两张网络之间由多台地理上隔离的网络设备连接,这些设备提供额外的路由与网络地址转换(NAT),让服务可以在内部网络与主网络之间通信。

这个设计的三个属性决定了之后发生的一切。

第一,内部网络是基础性的,不是辅助性的。内部 DNS 是每个服务都要查的电话簿。鉴权是每次控制面调用都要过的关口。监控是运维团队的眼睛。EC2 控制面是创建、描述、终止实例的那套机器。这些都不是可以优雅降级几个小时的服务;它们是其他服务默认永远在线的服务。

第二,这座桥是共享的、有限的资源。两张网络之间的 NAT 与路由设备有容量上限,每一次跨网络连接都要消耗其中一部分。AWS 说它把这部分容量扩得很大、用了多台地理隔离的设备——这是真实的冗余,对故事很重要,因为这次事故不是某一台设备坏了,而是总的连接负载超过了这些设备能处理的总量。设备间的冗余帮不上忙,因为问题出在负载本身,而且负载摊到了所有设备上。

第三,内部网络的客户端数量庞大且全是自动化的。AWS 报告里说的”内部网络内的大量客户端”不是坐在键盘前的人,而是服务、代理与控制面组件——它们作为正常运转的一部分,不断向主网络发起连接。一旦有什么东西改变了它们的连接行为,变化的不是某一个客户端行为失常,而是整个人口同时移位。

把这三点放在一起看,这次事故几乎就是它的触发条件的机械推论。

时间线(均为太平洋时间,2021 年 12 月 7 日)

时间(PST)事件
上午 7:30一次自动扩容活动(针对主网络内某 AWS 服务)触发内部网络内大量客户端的异常行为。连接活动激增,压垮了内部网络与主 AWS 网络之间的网络设备。
上午 7:30–7:33Route 53 API 开始受损;EC2 API 从 7:33 起出现错误率与延迟上升;Support Contact Center 从 7:33 起无法创建工单。
上午 7:30 起拥塞立即影响 AWS 内部运维团队的实时监控数据。运维人员失去可见性,退回用日志排查。
上午 8:22AWS 开始能够更新 Service Health Dashboard——此前拥塞导致它无法故障转移到备用区域。
上午 9:28团队完成把内部 DNS 流量迁离拥塞路径的工作。DNS 解析错误完全恢复。数个受影响服务改善,但拥塞未解决,运维人员仍然看不到监控数据。
中午 12:35缓解过程中,运维人员关闭 EventBridge 的事件投递,以降低受影响网络设备上的负载。
下午 1:15随着拥塞缓解,EC2 API 错误率与延迟开始改善。
下午 1:34拥塞显著改善。
下午 1:35API Gateway 开始恢复;大多数容器相关 API 错误率恢复正常。
下午 2:22所有网络设备完全恢复。Console 访问完全恢复。
下午 2:25Support Contact Center 工单创建恢复。
下午 2:30Route 53 API 恢复。
下午 2:35EventBridge 事件投递重新启用。
下午 2:40新 EC2 实例启动恢复——EC2 API 最后一个恢复的能力。
下午 4:28STS 完全恢复。
下午 4:37API Gateway 大致恢复。
下午 4:41Amazon Connect 恢复正常运行。
下午 5:00Fargate API 错误率开始恢复正常。
下午 6:40EventBridge 事件投递延迟随着积压处理完毕而结束。

读这张表有两个注意事项,因为 postmortem 里的数据值得仔细对待。第一,”网络设备下午 2:22 完全恢复”与”事故下午 2:22 结束”不是同一个断言。STS 直到下午 4:28 才完全恢复,API Gateway 到 4:37,EventBridge 还在处理积压直到 6:40。网络在 2:22 恢复了;服务在之后四个小时里各自拖着自己的尾巴恢复。第二,上午 9:28 的 DNS 里程碑是这次事故里最容易被误读的时间戳。DNS 错误在 9:28 完全恢复了——拥塞没有。在 9:28 就停下阅读的人会把故事读反:DNS 迁移是第一次成功的缓解,不是事故的结束。

级联:一项扩容任务、一波激增、一条自我喂养的回路

graph TD; A["Automated scaling activity in main network"] -->|"triggers unexpected behavior"| B["Many internal-network clients change behavior"]; B -->|"surge of new connections"| C["NAT and routing devices overwhelmed"]; C -->|"delays and errors between networks"| D["Services retry and open more connections"]; D -->|"more load on the same devices"| C; C -->|"monitoring data stops flowing"| E["Operators lose real-time visibility"]; E -->|"diagnosis falls back to logs"| F["DNS errors identified as a large traffic class"]; F -->|"DNS traffic moved off congested paths"| G["DNS errors recover at 9:28 AM"]; G -->|"load reduced but loop continues"| C; C -->|"top sources isolated and heavy services disabled"| H["Congestion improves at 1:34 PM"]; H -->|"devices fully recover"| I["All network devices recovered at 2:22 PM"];

这张图里有一个回路,而这个回路就是事故本身。其余的一切,要么是这个回路的后果,要么是打破它的尝试。

机制 1:重试放大——拥塞自己制造自己的负载

Postmortem 里的因果句值得全文引用,因为它包含了全部机制:“This resulted in a large surge of connection activity that overwhelmed the networking devices between the internal network and the main AWS network, resulting in delays for communication between these networks. These delays increased latency and errors for services communicating between these networks, resulting in even more connection attempts and retries. This led to persistent congestion and performance issues on the devices connecting the two networks.”(”这导致连接活动大幅激增,压垮了内部网络与主 AWS 网络之间的网络设备,造成两网之间通信延迟。这些延迟增加了跨网通信服务的延迟与错误,进而导致更多的连接尝试与重试。这造成了连接两网的设备上持续的拥塞与性能问题。”)

把这个回路一步步拆开,因为每一步都是对一个不同假设的破坏。

第一步:激增。 一次自动扩容活动——AWS 没有说被扩容的是哪个服务——触发了”内部网络内大量客户端的异常行为”。激增的是连接活动,不是字节数。这个区分很重要。NAT 设备要做连接跟踪:每个新连接都要在设备上消耗状态。一波新连接激增是对 NAT 设备来说最昂贵的一种负载,因为它不只是吞吐量——它是状态创建,速度超过了设备表项与处理器所能承受的设计值。AWS 用的词是设备被”压垮”(overwhelmed),压垮它的是新连接的速率,而不是流经已建立连接的数据量。

第二步:延迟变成错误。 设备一旦被压垮,两网之间的通信就开始变慢、开始失败。主网络里的某个服务调用内部 DNS、鉴权服务或控制面 API——调用变慢了,或者超时了,或者报错了。从调用方看,依赖没有响应。

第三步:调用方重试——而重试就是新连接。 这一步把一次激增变成了风暴。一个行为良好的客户端遇到错误会重试;一个连接尝试超时的客户端会打开一个新连接去重试。每一次重试都在给已经不堪重负的设备增加负载,这让下一次尝试更可能失败,从而产生更多重试。系统承受的负载不再是原始激增加上正常流量,而是原始激增加上正常流量再加上失败所产生的重试流量——只要失败率居高不下,这个量就会持续增长。系统现在开始自己制造负载。这就是拥塞持续的原因:它不是一个过去了的尖峰,而是一个由重试回路维持的新均衡。只要客户端重试的速度快于设备排空队列的速度,拥塞就会自我维持。

什么能打破回路。 只有两件事:减少供给的负载,或者增加排空它的容量。AWS 在几个小时里缓慢地两件事都做了——把流量最大的来源隔离到专用的网络设备上(从共享设备上卸掉负载)、关闭一些重流量网络服务(直接卸掉负载)、上线额外的网络容量(增加排空能力)。上午 9:28 的 DNS 迁移是第一步卸负载:内部 DNS 是一个很大的流量类别,把它迁离拥塞路径卸掉了一块供给负载。它帮助了数个服务,但没有打破回路,因为剩下的负载——其他所有东西的重试流量——仍然高于设备能排空的水平。

什么打破不了回路。 只加容量、不卸负载,是在跟重试放大器赛跑:每一单位新增容量都会被重试以同样快的速度吃掉,因为重试是由失败产生的,而失败会一直持续到队列排空。干等也不行,因为回路是自我维持的——只要失败率居高不下,负载不会自己衰减。回路必须被主动打破,要么卸掉负载,要么停掉重试。这与本系列 Cloudflare 与 Fastly 事故的教训是同一条,只是换了外衣:一个故障模式会增加自身负载的系统,是不会自己恢复的。

机制 2:没有生效的退避——一条运行多年的代码路径里的潜伏 Bug

Postmortem 里关于预防的那段话是整篇最重要的段落,也是措辞最谨慎的一段。AWS 写道,其网络客户端“have well tested request back-off behaviors that are designed to allow our systems to recover from these sorts of congestion events, but, a latent issue prevented these clients from adequately backing off during this event. This code path has been in production for many years but the automated scaling activity triggered a previously unobserved behavior.”(”具备经过充分测试的请求退避行为,设计上能让系统从这类拥塞事件中恢复,但一个潜伏问题导致这些客户端在这次事件中未能充分退避。这条代码路径已在生产环境运行多年,但自动扩容活动触发了一种此前从未观察到的行为。”)

把这段话精确地读成三个部分,因为它是一个三段式断言。

护栏存在,而且经过测试。 退避——失败堆积时拉长重试间隔,而不是猛击失效的依赖——正是针对上面描述的重试放大回路的标准防御。AWS 说其客户端的退避行为”经过充分测试”。放在上下文里这不是营销语言;它让下一句成为意外而非失职认定:防御是真实的,而且它曾经起效。

护栏有个潜伏的洞,而这次触发条件找到了它。 一条”已在生产环境运行多年”的代码路径里的”潜伏问题”,导致客户端”未能充分退避”。扩容活动”触发了一种此前从未观察到的行为”——客户端在扩容事件面前的某种行为,是此前任何拥塞事件都没有产生过的。合起来读:退避逻辑处理了它被测试过的那些故障模式,而扩容触发条件把客户端置于一种退避逻辑无法识别为”需要退避”状态的状态——最可能的情况是,客户端不断打开新连接(”异常行为”与”连接活动大幅激增”),而不是以退避可以节流的方式重试已有请求。退避节流的是请求的重试;它未必能节流一个被触发进入新行为的客户端群体所产生的新连接。如果故障模式是”客户端以设备无法跟踪的速率打开连接”,那么按请求计时的退避就是形状错误的防御,它会静默地失效。

AWS 划出的边界,以及它没有划出的边界。 AWS 说它正在”开发针对这一问题的修复”,预计”在接下来的两周内部署”,并且”还部署了额外的网络配置,即使面对类似的拥塞事件也能保护可能受影响的网络设备”。这是两项不同的修复,区分本身就是机制教训。代码修复针对潜伏的退避漏洞——下一次遇到这种情况客户端会退避。网络配置则是在设备侧的防御,即使类似拥塞再次发生也能保护设备——一种不依赖客户端行为正确的防御。后者是两者中更有结构意义的一项,因为它是纵深防御:它假定客户端将来还可能行为失常,并在设备处限定损害,而不是信任客户端。但要注意,两项修复都没有做的是:在架构层面,在内部网络的客户端与共享 NAT 设备之间加一道隔板。修复让已知的洞变小了;它没有消除那种共享命运结构——一个人群的连接激增可以饿死所有人的桥。而且 AWS 明确说了,修复在发文时尚未部署——postmortem 承诺两周内完成,而仅凭 postmortem 本身,本文无法核实它是否按时落地。

这里还有一条诚实边界,本文会像对待 Fastly 那样让它保持可见。AWS 从未说明触发事故的是哪个服务的扩容活动,从未在技术细节上描述潜伏的退避问题,也从未给出连接数或设备容量数字。上面给出的机制——连接速率激增压垮 NAT 状态、重试放大、退避未能在新连接路径上生效——是作者根据 AWS 自己的因果句、以及 NAT 设备与退避机制的工作方式所做的重建。它与 AWS 公布的每一句话一致,也不与其中任何一句矛盾。但确切的 Bug 不在 postmortem 里,本文不会假装它在。

机制 3:事故让修事故的人失明——可观测性与故障共命

这次事故里的第二个回路是组织层面的,也是回路一烧了七个小时的原因。“This congestion immediately impacted the availability of real-time monitoring data for our internal operations teams, which impaired their ability to find the source of congestion and resolve it. Operators instead relied on logs to understand what was happening.”(”这次拥塞立即影响了我们内部运维团队实时监控数据的可用性,妨碍了他们找到拥塞源头并解决它。运维人员转而依靠日志来了解发生了什么。”)

监控系统住在内部网络里——而正是这张网络通往主网络的桥发生了拥塞。AWS 用来观测拥塞的工具,本身就被堵在拥塞后面。这就是可观测性层面的共命:故障干掉了测量故障的仪器。运维人员并非绝对失明——他们有日志,而日志正是揭示内部 DNS 错误升高的东西——但日志是一种事后取证工具,不是一种实时工具。它告诉你发生了什么;它无法以你引导缓解所需的速率告诉你正在发生什么。本该几分钟完成的诊断,变成了翻日志、手工关联所需要的时间。

DNS 迁移就是这次缓慢诊断的回报,也是它的极限。日志显示内部 DNS 错误升高后,团队把内部 DNS 流量迁离拥塞路径,到上午 9:28 DNS 解析错误完全恢复。但 postmortem 明确指出,“monitoring data was still not visible to our operations team so they had to continue resolving the issue with reduced system visibility.”(”监控数据对我们的运维团队仍然不可见,因此他们不得不在系统可见性降低的情况下继续解决问题。”)DNS 修复恢复了一个服务;它没有恢复可观测性。团队在剩余的事故时间里——隔离主要流量来源、关闭重流量服务、增加容量——都是在可见性降低的情况下工作的,也就是说,他们判断每个动作是否生效的能力变弱了。

为什么 DNS 修复只用了两小时,恢复却用了七小时。 Postmortem 给了三个原因,每个都是机制,不是借口。第一,就是上面说的监控失明:理解问题的能力受限。第二,“our internal deployment systems, which run in our internal network, were impacted, which further slowed our remediation efforts”(”我们的内部部署系统运行在内部网络中,也受到了影响,这进一步拖慢了我们的缓解工作”)——用来部署修复与配置变更的机器本身也被堵在拥塞的桥后面,所以每一次缓解动作都比在健康的日子里更慢才能生效。事故损坏了修理工的工具。第三,“because many AWS services on the main AWS network and AWS customer applications were still operating normally, we wanted to be extremely deliberate while making changes to avoid impacting functioning workloads”(”由于主 AWS 网络上的许多 AWS 服务与客户应用仍在正常运行,我们在做变更时希望格外谨慎,避免影响仍在正常运行的工作负载”)——系统一半是健康的,对桥的一次草率变更可能打垮仍在工作的那一半。谨慎是对的;它也很慢。这七小时就是这三项之和:失明的诊断、受损的修复工具、以及对一个半健康系统的合理谨慎。

通信故障是同一模式的另一层。 同样的共命也打到了面向客户的一侧。拥塞妨碍了 Service Health Dashboard 向备用区域故障转移,导致 AWS 直到上午 8:22——事故开始后近一小时——才能更新 Dashboard;而更新时用的是一个全局横幅,AWS 自己承认这”让一些客户难以找到相关信息”。同样依赖内部网络的 Support Contact Center,从上午 7:33 到下午 2:25 无法创建工单。模式在每一层都一样:内部网络是生产系统、修复系统与通信系统的共同依赖,事故同时削弱了这三者。AWS 承诺的修复——新版 Service Health Dashboard 与”一个主动跨多个 AWS 区域运行的新支持系统架构”——正是针对这一点:让通信路径独立于正在故障的区域。

到底坏了什么、服务 by 服务——以及什么没坏

Postmortem 的服务影响部分,是控制面/数据面区分最清晰的陈述,值得当作一张表来读,而不是一段叙事,因为模式本身就是重点。

服务 / 能力影响保持正常的部分
EC2 API(启动、描述实例)从上午 7:33 起错误率与延迟上升;下午 1:15 开始改善;新实例启动下午 2:40 恢复正在运行的 EC2 实例不受影响
RDS、EMR、Workspaces无法创建新资源(被 EC2 启动阻塞)已有资源继续运行
Elastic Load Balancers新负载均衡器配置时间变长;向已有负载均衡器注册实例延迟已有负载均衡器保持健康
Route 53 API上午 7:30 至下午 2:30 受损;无法修改 DNS 记录已有 DNS 记录与 DNS 查询应答不受影响
AWS Console 登录事故期间登录失败下午 2:22 完全恢复
STS(OIDC 第三方身份提供商)延迟升高;使用 STS 的服务(如 Redshift)登录失败下午 4:28 完全恢复
CloudWatch监控延迟;少量监控数据未被采集、部分指标可能缺失——
S3 与 DynamoDB(直接访问)不受影响数据面访问全程正常
S3 / DynamoDB 经 VPC Endpoints 访问受损直接访问不受影响
LambdaAPI 与函数调用全程正常运行——
API Gateway错误率升高;服务器进入需要替换才能恢复服务的状态;自动回收被阻塞,直到 EC2 API 开始恢复;下午 4:37 大致恢复,此后数小时仍有低量错误与限流——
EventBridge初期错误升高,上午 9:28 改善;中午 12:35 关闭事件投递,下午 2:35 重新启用;投递延迟升高直到下午 6:40 积压处理完毕——
Fargate / ECS / EKSAPI 错误率与延迟升高;被终止或故障的任务无法重启;大多数 API 下午 1:35 恢复正常;Fargate API 错误率从下午 5:00 开始恢复正常;4 vCPU 任务规格的容量不足错误持续数小时已有任务与 Pod 继续正常运行
Amazon Connect电话、聊天、任务联系的失败率升高;下午 4:41 恢复正常运行——

有两个模式值得强调,因为它们是可迁移的教训。

控制面/数据面分离守住了——这就是事故可以承受的原因。 数据面是服务流量的路径:EC2 实例运行、S3 提供对象、DynamoDB 回答查询、Lambda 执行函数、已有负载均衡器在做均衡。控制面是改变事物的路径:启动实例、创建资源、修改 DNS、注册目标、签发凭证。事故期间,数据面基本完好,控制面严重受损。这种分离是一种有意的架构属性——数据面不需要为每次请求都依赖内部网络——正是它让这次事故尽管波及面广,却没有打垮互联网。但分离在一个方向上是不完整的,而这个方向恰恰重要:控制面依赖内部网络,而内部网络的桥是一个单一的共享 choke point。更干净的分离会给控制面一条通往其依赖的独立路径,或者至少加一道隔板,让一个人群的连接激增无法饿死所有人的桥。

恢复本身也有自己的依赖回路。 API Gateway 是最尖锐的例子。它的服务器”最终进入了一种需要被替换才能成功提供请求的状态”,而替换”通常通过自动回收流程完成,但这在 EC2 API 开始恢复之前是不可能的”。需要新实例的服务拿不到实例,因为提供实例的服务宕机了。Fargate 是同样的形状:被终止的任务在 EC2 控制面恢复之前无法重启,而恢复之后,积压的重启本身又造成了数小时的错误率升高与容量不足错误。控制面宕机的恢复不是回到正常,而是一次二次激增——所有排队的创建、重启、注册同时涌向一个刚刚恢复的系统。EventBridge 直到下午 6:40 还在处理的积压,是事件层面的同一形状。从下午 2:22 到 6:40 的事故尾巴,就是这次二次激增的代价,也是”网络设备完全恢复”不等于事故结束的原因。

AWS 改了什么——以及每项改变的限度

  1. 立即关闭触发事故的扩容活动,在全部修复部署完成之前不再恢复。 这是通过移除输入来缓解,与 Fastly 关闭触发配置的形状相同。它快,也是不完整的:系统只有在扩容活动保持关闭期间才是安全的,而这项活动大概是 AWS 想要重新做的。Postmortem 说系统”扩容充足,因此近期不需要恢复这些活动”——这是关于余量的陈述,不是修复。
  2. 修复潜伏的退避问题,预计两周内部署。 消除客户端退避里的这个洞。它消除不了这一类:下一次自动化触发的、下一次此前从未观察到的客户端行为,不受这次修复的影响。退避修复是对一个故障模式实例的补丁,不是对这一类的控制。
  3. 部署额外的网络配置,即使面对类似拥塞事件也能保护受影响的网络设备。 这是设备侧的纵深防御,也是 postmortem 里结构上最有意思的一项修复:它在设备处限定损害,而不是信任客户端行为。但它本身既不增加容量,也不增加人群之间的隔离——防止过载不等于隔离客户端群体,而 postmortem 没有描述桥上按人群隔离的措施。
  4. 新版 Service Health Dashboard 与跨多个区域主动运行的支持系统架构。 针对通信层面的共命:Dashboard 与支持系统不应依赖正在故障的区域。这修复的是通信故障,不修复内部网络的桥,不应被读作修复了后者。

值得偷走的教训

  1. 一个会增加自身负载的故障模式,是不会自己恢复的。 重试就是负载。失败率上升时,重试流量随之上升,一个只是过载的系统会变成自我维持的拥塞。任何重试策略都需要一个负载预算,以及一个在你已有的故障模式下真正会生效的退避——而且必须拿它去对付你没见过的故障模式做测试,因为到来的恰恰会是没测试过的那一个。
  2. 退避是一道护栏,不是一份保证——而且它可能静默地失效。 AWS 的客户端有经过充分测试的退避,而一个潜伏问题让它没能生效。教训不是”退避没用”,而是客户端侧的优雅机制只在它覆盖了客户端实际能进入的状态时才有效。审计状态,而不只是审计计时器的值。并且在客户端防御之后再放一道设备侧防御,因为客户端最终会以你没预测到的方式行为失常。
  3. 你的可观测性不能与它观测的系统共命。 如果你的监控、告警与部署工具都依赖与故障系统相同的网络、相同的 DNS 或相同的控制面,故障会同时让你失明、并缴掉你的修复工具。监控与修复路径需要一条独立的通往被观测系统的路——带外、跨区域,或者至少在一条不会被它本应检测的故障堵住的路径上。
  4. 控制面/数据面分离是你最有价值的隔板——把它做完。 这次分离是事故可以承受的原因:正在运行的工作负载从未宕机。但控制面自己的依赖没有得到同样的分离对待。把控制面的每一个依赖都列出来,问一句:当控制面自己的网络拥塞时,它会怎样?一个在自身事故期间无法被触达、修复或观测的控制面,就是一个穿着冗余外衣的单点故障。
  5. 恢复会带来二次激增——为它做计划。 排队的启动、重启、注册与积压事件,都会在控制面恢复的那一刻同时到达。事故不是在根因修复时结束,而是在积压排空时结束。按积压来给恢复定容量,而不只是按稳态,并且预期在 headline 恢复时间之后数小时仍有错误升高。
  6. 一个半健康的系统会约束自己的修复。 数据面还在提供流量时,每一次缓解都必须权衡打破仍在工作部分的 risk。这种审慎是对的,也很慢。买速度的办法不是在当下少审慎,而是提前准备好经过审批、经过测试的缓解 playbook——包括像 DNS 迁移这样的卸负载动作——让谨慎的路径同时也是快速的路径。
  7. “此前从未观察到的行为”不是借口;它是潜伏 Bug 的正常形状。 一条在生产环境运行多年的代码路径、经过充分测试的退避、一个没人见过的触发条件——结果仍然是七小时宕机。存活时间长不是覆盖率的证据。如果你的信心建立在”它运行多年都没事”上,问一句它实际被演练过哪些状态,以及一个新的触发条件能把它带进哪个此前从未进入过的状态。

Postmortem 没有说什么

这是本文的诚实账本。AWS 的 postmortem 没有披露:触发事故的是哪个服务的扩容活动;潜伏退避问题的技术细节(除了一句”一个潜伏问题导致这些客户端未能充分退避”之外,没有代码、没有故障描述);连接数、设备容量或激增的量级;主要流量来源是如何识别的、缓解期间关闭了哪些服务;承诺的退避修复是否在两周窗口内部署;除了服务层面的描述之外,任何客户影响数量。当时的报道补充了一些用户可见的细节——受影响的消费服务、亚马逊内部的仓储与物流影响——但本文的纪律是一手来源,所以这些细节不会在这里被当作既定事实呈现;上面引用的 AWS 数字就是本文依赖的全部定量记录。

与本系列其他篇目的对照是有启发性的。Cloudflare 公布了出事的正则并数清了它的步数。Fastly 公布了时间线、对 Bug 保持沉默。AWS 公布了架构、因果回路与逐服务的、粒度异常细的影响——而对触发条件的身份与 Bug 的内部机制保持沉默。它存在的深度是真实的:重试放大机制、监控失明、恢复缓慢的三个原因、漫长的服务尾巴,都陈述得足够清楚,可以从中学习。它的沉默也是真实的,而且沉默的位置永远是同一个地方:把一次常规扩容活动变成七小时宕机的那条确切代码里的确切故障。想要那个教训的读者不会在这里找到它,本文宁可这样说,也不愿编造。

AWS 说过的话里值得留下的,是框定整篇 postmortem 对其自身系统态度的那句:客户端有经过充分测试的退避,代码路径运行了多年,触发条件此前从未被观察到——拥塞还是发生了。本系列对每一篇 postmortem 坚持的标准,正是 Fastly 说出、AWS 用行动证明的那条:一次故障不会因为它的触发条件具体就变得不可预见。如果组合空间是组合式的,如果你的客户端能进入退避覆盖不到的状态,如果你的可观测性与你的故障共命,那么这个类别是可预见的,哪怕这个实例不是。

Sources

  • Amazon Web Services, “Summary of the AWS Service Event in the Northern Virginia (US-EAST-1) Region,” December 10, 2021. https://aws.amazon.com/message/12721/ — 本篇所有定量内容的一手来源(除非另有说明):双网络架构与之间的 NAT/路由设备;太平洋时间上午 7:30 的自动扩容触发与连接激增;重试放大的因果链;实时监控立即受损与退回日志排查;上午 9:28 的内部 DNS 迁移及其限度;缓解进展缓慢的三个原因;下午 1:34 拥塞改善与 2:22 设备完全恢复;潜伏的退避问题与两周修复预期;保护设备的网络配置;逐服务影响时间(EC2 API 从上午 7:33 起、新实例启动下午 2:40 恢复;Route 53 API 上午 7:30 至下午 2:30;Console 下午 2:22 恢复;STS 下午 4:28 完全恢复;API Gateway 下午 4:37 大致恢复;EventBridge 中午 12:35 关闭投递、下午 2:35 重新启用、延迟至下午 6:40;Fargate API 错误率从下午 5:00 开始恢复正常;Amazon Connect 下午 4:41 恢复正常;Support Contact Center 上午 7:33 至下午 2:25 受损);Service Health Dashboard 故障转移受损与上午 8:22 更新;承诺的新版 Dashboard 与多区域支持系统架构。
  • 标注为分析的部分——NAT 连接状态视角下连接激增为何是最昂贵的负载类别、重试放大均衡、关于新连接与请求重试的退避形状论证、可观测性共命的解读、服务表背后的控制面/数据面分离解读、恢复二次激增的解读——是作者根据 postmortem 的事实与标准网络及分布式系统架构所做的推理,不是 AWS 的陈述。AWS 保持沉默的地方(触发服务、退避 Bug 的内部机制、激增量级、被关闭服务的身份、修复部署的确认),本文如实说明,而不填补空白。