一位获授权的工程师,按既有操作手册,执行了一条常规命令——只是有一个输入填错了。这一关不讲"要小心",讲的是当小心失效之后,系统还剩什么。
2017 年 2 月 28 日上午,美国东部(US-EAST-1)区域。S3 团队正在排查一个不算紧急的问题:计费系统跑得比预期慢 [S1]。
处理方式很常规——下线计费流程用到的某个 S3 子系统的少量服务器,重新调度。这类操作有既定的操作手册(playbook),执行的人有授权,动作本身是 S3 的核心运维流程之一 [S1]。
9:37AM PST,命令执行。其中一个输入参数填错了,被移除的服务器比预期多得多 [S1]。
被误删的那批服务器,同时支撑着另外两个 S3 子系统 [S1]。它们的名字值得记住,因为它们的关系是这一关的技术骨架:
移除了相当大比例的容量,导致这两个子系统都必须完整重启。重启期间,S3 无法服务请求 [S1]。
同区域内依赖 S3 的其它 AWS 服务随之受影响。官方复盘点名的是:S3 控制台、EC2 新实例启动、EBS 卷(当需要从 S3 快照取数据时)、AWS Lambda [S1]。
从事故开始一直到 11:37AM PST,AWS 无法在自己的状态页上更新各服务状态,因为 SHD 管理控制台依赖 S3 [S1]。团队只能改用推特账号和状态页顶部的横幅文字通报 [S1]。
也就是说:那个专门用来在出事时告诉大家"出了什么事"的系统,被同一场事故一起放倒了——整整两个小时。这就是依赖自举问题:你的应急工具建在了它要报告的那个东西上面。
假设你是「秒抢」的 SRE,负责设计集群容量的下线工具。你完全知道 AWS 这一课。但护栏不是免费的——每一道护栏都会让日常运维变慢、变烦,而烦到一定程度,人会绕过它。下面三个决策,每一个都有真实的代价。
B 优先,因为它是唯一一个「人错了也不生效」的护栏。AWS 事后做的正是这件事:增加安全检查,当移除会让任一子系统低于其最小所需容量时,禁止移除,并明确说这将阻止一次错误输入再度引发同类事件 [S1]。注意它的性质——A 和 C 都要求人在正确的时刻做出正确反应,B 不需要人参与判断。在同等成本下,能自动执行的约束优先于需要人配合的约束。
A 有价值,但它不能保证拦截。把「将移除 N 台、剩余 M 台」打印出来,确实有机会让人看见异常大的 N 或异常小的 M 而停手——这正是影响预览的价值。但它的拦截率取决于人在那一刻的注意力:确认界面会忠实地打印那个错误的数字,而执行者往往会确认它,因为他相信自己填的是对的。这和关卡一里"再看一眼窗口标题"是同一类问题:确认环节提供的是发现错误的机会,不是拦截错误的保证——在疲劳、预期偏差或时间压力下,人往往看见了却没停手。所以它适合作为 B 的可读性补充,而不是唯一防线:概率性的人工护栏,替代不了确定性的硬约束。
C 是 AWS 实际做的另一半,价值很大但排第二。AWS 的原话是这个工具「允许移除的容量太多、太快」,因此他们把它改成更慢地移除容量 [S1]。速率限制的作用是把"瞬间的错误"变成"有时间发现的错误"——它不阻止你犯错,它给你留出后悔的窗口。之所以排在 B 后面:如果最小保留量守住了,速率限制只是锦上添花;反过来则不然。
D 是最常见、也最没用的一类"改进"。这次执行命令的人本来就是获授权的、按既有手册操作的 [S1]。收紧权限会把操作集中到更少的人身上,让这些人更疲惫、更常在深夜被叫起来——关卡一已经演示过疲劳的代价。把安全寄托在"只让厉害的人碰",等于承认你没有护栏。
B。这正是本次事故最反直觉的一课。AWS 的系统本来就是按"容量会失效"设计的,移除并替换容量是核心运维流程 [S1]。真正的问题是:在较大的区域里,这两个子系统已经很多年没有被完整重启过;而 S3 这些年增长巨大,重启并跑完元数据完整性校验,比预期花的时间长 [S1]。一个从未被完整重启过的系统,它的重启时长是一个未知数——而未知数会在最糟的时刻被测量。计时是关键:演练的产出不是"能启动",是那个数字。
A 是这一关最危险的选项,因为它的推理看起来完全正确。"三年没出事"确实是稳定性的证据——但它同时也是无知的证据:你不知道它重启要多久,不知道启动时的依赖顺序对不对,不知道那些年里悄悄增长的数据量会让校验跑多久。「很久没重启过」不是稳定的勋章,是一笔在计息的债。它迟早要被重启,区别只在于是你选的时间,还是它选的时间。
C 是正确的长期方向,也正是 AWS 的规划。他们把服务拆成称为 cell 的小分区,以缩小爆炸半径并改善恢复能力;index 子系统的进一步分区本已列入年内计划,事故后提前立即启动 [S1]。它之所以只能算次优,是因为拆分是季度级工程,而 B 是本周就能做的——并且拆完之后你依然需要演练,才知道拆得对不对。
D 是"看起来做了事"的典型。没有被执行过的预案,其中的每一步都是假设:命令还在不在、依赖顺序对不对、耗时多少、谁有权限。关卡一的 GitLab 已经证明过——他们有五层备份,问题是从没验证过其中四层。文档不会在凌晨三点替你发现自己写错了。第 16 章会把这条铁律正式命名。
A。AWS 有两个小时说不出话,原因就是 SHD 管理控制台依赖 S3 [S1];事后他们把 SHD 管理控制台改成跨多个 AWS 区域运行 [S1]。原则很简单:报告故障的系统,不能和被报告的系统共享故障域。这条同样适用于告警通道——如果你的告警要经过正在挂掉的那个消息队列,它就不是告警。
B 混淆了两件事:"用户访问不了业务"和"用户不知道发生了什么"。事故期间用户会离开业务站点,转而去专门找状态页;这正是状态页流量的最高峰,也正是它最不该挂的时刻。关卡一里 GitLab 的公开监控站点同样没扛住事故期间涌来的用户(见关卡一的来源清单)——而沉默会被解读成"他们自己也不知道"。
C 是有用的兜底,而且 AWS 当时正是这么做的——改用推特账号和横幅文字通报 [S1]。但请注意它的定位:这是降级方案,不是设计。手动通道依赖于有人腾出手来发消息,而事故初期恰恰是所有人最忙的时候。把它作为唯一方案,等于把对外沟通的及时性押在人的余力上。
D 只解决了一半,而且是不重要的那一半。静态化确实减少了状态页对应用和数据库的依赖,但它仍然和主集群共享机房、网络、电力和你的运维操作——而这次事故的起因正是一条运维命令。共享故障域的判据不是"依赖了哪些服务",是"哪一次操作能同时打倒它们"。
命令执行,参数填错,超预期的服务器被移除 [S1]。
在此之前一直无法更新状态页——SHD 管理控制台依赖 S3 [S1]。沉默了两个小时。
index 子系统激活了足够容量,开始服务 GET / LIST / DELETE 请求 [S1]。
index 子系统完全恢复,GET / LIST / DELETE API 正常 [S1]。
placement 子系统恢复完成(它要等 index 可用才能开始)。此时 S3 运行正常 [S1]。
部分受影响的其它服务在中断期间积压了工作,需要额外时间才完全恢复 [S1]——积压的代价,第 9 章已经教过你怎么算。
此刻的「秒抢」:约 100 万用户、95 个人(第 7 章刚被重试风暴打过一次)。你已经有了限流、熔断、缓存和一堆定时任务——也就是说,你已经有了足够多的"一条命令能捅出大娄子"的地方。
| AWS 的失效点 | 「秒抢」的对应物 | 本周动作 | 关联 |
|---|---|---|---|
| 工具允许移除过多容量 | 缩容脚本、K8s 副本数调整、批量下线实例 | 给所有容量变更工具加最小保留量硬拒绝,并把单次变更幅度限死(如一次最多 20%) | 本关 D1 |
| 核心子系统多年未重启 | 缓存集群、消息队列、网关配置中心 | 把「完整重启并计时」列入季度演练;记录的是耗时,不是成功与否 | 第 16 章 |
| 串联依赖放大故障 | 下单 → 库存 → 支付的串联链路(第 7 章的级联故障 No.09) | 画一张依赖图,标出「哪些组件挂了会让另一个组件无法启动」——注意区分"降级"和"起不来" | 第 7 · 13 章 |
| 状态页依赖被报告的系统 | 监控大盘、告警通道、值班手册、状态页 | 逐个问一遍:「主机房整个断电时,它还在吗?」不在的挪出去 | 第 14 · 15 章 |
最后留一个可以马上做的小练习:打开你们的运维脚本目录,找出所有会删除、缩容、清空、重置的命令,数一数其中有几个在执行前会告诉你"这一下会影响多少"。
如果答案是零,那么你和 2017 年 2 月 28 日上午的距离,就是一个参数。
逐句核对清单见构建仓库 sources/FACTS-level2.md;来源原文留档见 sources/raw/aws-s3-2017-summary.txt。关于本次事故更大影响面的第三方统计未被采信为事实依据。