Skip to content

필사 모드: 5 节点 Raft 集群中 3 个节点为何同时死亡 — 解读 Coinbase 2026 年 5 月 7 日故障

中文
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

引言 — 别人的事后复盘比教科书更准的时候

分布式系统类文章里,八成都是从同一句话开始的:"Raft 用 2f+1 个节点容忍 f 个故障。"这话没错,也会出现在考试里,实务中也没有人因为不知道这句话而出事。真正出事的人,是栽在那句话没写出来的前提上。

2026 年 6 月 1 日,Coinbase 公开了关于 5 月 7 日故障的事后复盘。这篇文章值得一读,不是因为它讲的是"我们犯了个蠢错误"这种故事。恰恰相反 — 它讲的是一个刻意做出的、有文档记录的、在其领域内站得住脚的架构选择,正面撞上了那个选择本身当作不存在的故障模式。而 Coinbase 至今也没有撤回这个选择。

本文以那份事后复盘为依据来读这起事件。文中所有时间点和数字都出自 Coinbase 自己,如果来源不同,我会标明。

发生了什么

一切始于物理现象,而非软件。按 Coinbase 的说法,美东时间 5 月 7 日晚上 7 点 20 分,AWS us-east-1 区域可用区 use1-az4 内的一处数据机房中,多台冷水机组同时发生故障。冷却中断后,相应机架进入热保护关机,该建筑内的 EC2 实例和 EBS 卷随之离线。

AWS 自己的说法描绘的是同一幅图景。根据 The Register 与 Network World 引用的 AWS 状态仪表盘措辞,AWS 在太平洋时间下午 5 点 25 分的更新中写道,受影响硬件上的 EC2 实例和 EBS 卷因"热事件期间的电源丧失"而受到影响,并在次日早上的更新中说明,温度超出了运行阈值,服务器为保护硬件而自动关机。这不是软件能够绕过的那类故障 — 只有冷却恢复,硬件才能重新通电。

Coinbase 公开的时间线如下,均为美东时间。

5 月 7 日
  7:20 PM  use1-az4 内一处数据机房中多台冷水机组同时故障
           冷却丧失 -> 相应机架热保护关机 -> EC2 / EBS 离线
  7:48 PM  Coinbase 大部分交易中断
  9:29 PM  AWS 终止置放组内的 EC2 实例
           撮合引擎 5 个节点中 3 个宕机 -> 失去法定人数

5 月 8 日
 12:06 AM  法定人数恢复
  2:25 AM  撮合引擎以仅撤单(cancel-only)模式开放
  3:00 AM  手动重新分配分区,把主题从受损 broker 上迁出
  3:49 AM  全部品种交易恢复正常
  5:30 AM  coinbase.com / 移动应用交易以降级状态恢复
  9:30 AM  P0 / P1 主题完全恢复
  9:53 AM  coinbase.com / 移动应用完全恢复
  2:00 PM  剩余事件流主题积压清空

Coinbase 自己的总结是这样的 — 交易、存款、取款在内,客户所依赖的大部分服务,在"大约 8 小时"内不可用或处于降级状态,而全部系统完全恢复又多花了"额外 12 小时"。作为参照,从晚上 7 点 48 分(交易中断)到凌晨 3 点 49 分(全部品种恢复)正好是 8 小时 1 分钟,这个"大约 8 小时"与时间线是吻合的。

法定人数算术里藏着的前提 — 故障的独立性

Coinbase Exchange 的撮合引擎是一个基于 Raft 的复制集群,运行在 AWS 集群置放组(Cluster Placement Group)内。节点数为 5,按 Raft 的算术,能容忍最多 2 个故障。只要 3 个存活,就构成多数,就能继续运转。

3 个死了。而且是同时死的。

这里重要的不是"3 个死了,所以 5 节点不够用"。就算把节点数增加到 7 个,同样的事情也会发生。真正的问题在于那套算术背后的前提。"5 节点容忍 2 个故障"这句话的精确形式是5 节点容忍 2 个相互独立的故障。而集群置放组正是刻意抹掉这种独立性的东西。

AWS 自己的置放组文档把这个权衡原原本本地写了下来。文档把置放策略分为:

  • Cluster — 在单个可用区内把实例"紧密地打包在一起"(packs instances close together)。目的是获得像 HPC 那样、节点间通信紧密耦合的工作负载所需的低延迟网络性能。
  • Partition — 把实例划分为多个逻辑分区,使不同分区中的实例组不共享同一硬件。文档举的例子是 Hadoop、Cassandra 和 Kafka。
  • Spread — 把少量实例严格分布到彼此不同的硬件上,以"减少相关故障"(reduce correlated failures)。

文档中还有这样一句话 — 如果完全不使用置放组,EC2 默认会尝试把实例分散到各处硬件上,以"最小化相关故障"(minimize correlated failures)。

也就是说,"correlated failure"这个词在 AWS 文档里,出现在描述 Spread 和默认行为的地方 — 而不是描述 Cluster 的地方。Cluster 站在这个说法的对立面。这是一笔用独立性换取延迟的交易,而这笔交易,文档的第一页就写明了。

所以,这起事件可以用一句话概括:法定人数数的是节点,故障数的不是节点。 故障数的是故障域。置放组内的 5 个节点,在 Raft 眼里是 5,在冷水机组眼里是 1。

而 Coinbase 并没有撤回这个选择

这是这份事后复盘里最有意思的地方。这类文章通常都会写成"我们制造了一个单点故障,我们对此感到后悔"。Coinbase 没有这么写。

事后复盘明确指出,同地部署(co-location)是一个刻意的选择,并给出了理由 — 一个要满足严肃市场对延迟和吞吐量要求的撮合引擎,无法承受投票集群成员之间跨越可用区的网络跳数。文中写道,分布式共识的物理学,与运行一个公平、有流动性的订单簿的经济学,指向的是同一个答案 — 同地部署。

这个说法值得认真对待。在 Raft 中,提交延迟取决于 leader 收到多数 follower 响应所需的时间,也就是投票成员之间的往返延迟。跨越可用区会拉长这个往返,而这笔成本要在每一次提交上支付。对于那些在低延迟交易系统里靠内核调优抠出微秒级延迟的人来说,这不是一个可以商量的约束。(Coinbase 没有公开具体的延迟数字,我在这里也不会凭空编一个。)

所以,Coinbase 自己的诊断,落点不在同地部署本身,而在它旁边。用事后复盘的话说,架构上的问题在于缺少自动故障转移到另一个可用区的能力。问题不在于用了置放组,而在于没有一条能自动走出置放组的路。

这不是文字游戏,它在实务上会导向一个完全不同的处方。"别用置放组"这句话对这个团队没用 — 那样撮合引擎就达不到所需的性能。相反,"如果你刻意耦合了一个故障域,那就把逃生的门造出来,并定期把它打开"是一条可执行的建议。事实上,Coinbase 改进项目清单的第一条正是这个 — 改进撮合引擎的热备跨区待机(warm cross-zone standby)设计,并按固定周期开展生产环境故障转移演练,让最大的几家交易所参与维护窗口期的演练,其余的则按沙箱周期轮转。

只在灾难时才会跑的代码,实际上是从未被测试过的代码

事后复盘里,我盯着看得最久的一句话其实是这个。为了恢复法定人数,Coinbase 不得不在故障处理过程中部署一次紧急代码变更,其内容是移除一个启动时的假设 — 该假设要求全部五个节点都必须可解析(resolve)。

请把这句话慢慢读一遍。他们想把集群救回来,可集群启动不了。因为启动代码把自己出生时的拓扑当成了事实。要全部五个节点都能解析出名字,才能启动 — 平时为真,部署时也为真,预发布环境里同样为真。只有在三个节点化为灰烬的那一晚,它才为假。也就是说,这个假设是那种在其他每一天都正确、只在真正需要的那一天出错的代码。

这种事为什么会发生并不神秘。正常路径每秒执行成千上万次,恢复路径几年才执行一次。而从不执行的代码,就是从未被测试过的代码。当我们说"我们有一套恢复流程"时,实际上多半意思是"我们有一份描述恢复流程的文档"。文档不会被编译。

Coinbase 那天晚上实际做的事情清单,把这一点体现得很具体 — 部署紧急代码变更,在受损置放组之外搭建一个新的节点组,并小心地走完一套流程来恢复 3-of-5 的法定人数。这三件事没有一件是按一个按钮就能完成的。这三件事几乎都是那天晚上第一次真正做。

这也是为什么"定期开展生产环境故障转移演练"会出现在改进项目里。演练的目的与其说是让人熟悉流程,不如说是确保恢复路径是一段真正会被执行的代码。一次都没跑过的故障转移,不是故障转移,而是一个假设。

托管服务不会消除风险,只会转移风险

第二种故障模式来自完全不同的一层。Coinbase 大部分的事件流处理,运行在 AWS 的托管 Kafka——MSK 之上。

托管 Kafka 的承诺很清楚。用事后复盘自己的总结来说:如果单个 broker 挂了,服务应当自动重新选举分区 leader,并继续用存活的 broker 处理流量;如果整个可用区宕掉,结果应当是容量下降,而不是不可用

结果并非如此。按 Coinbase 的说法,AWS MSK 控制平面的一个缺陷阻止了分区 leader 的自动重新选举,两个 MSK 集群卡在"healing"状态,生产者(producer)无法写入。

这里有两点值得指出。

第一,这是灰色故障,不是崩溃。 "healing"不是死亡。要是死亡反而简单 — 健康检查变红,告警响起,故障转移接管。"healing"是系统在对自己说"我现在正在恢复",而这句话可能是真的,也可能永远不会变成真的。与此同时,生产者写不进去。仪表盘既不是绿色也不是红色,而是黄色,黄色是没有自动化跟着的。

第二,无法自己修复才是真正的成本。 如果是自行运维的 Kafka,本可以选择手动强制重新选举 leader,或者重启 controller。而在 MSK 上,这根拉杆不在自己手里。Coinbase 最终做的是,在凌晨 3 点与 AWS 工程团队实时协作,手动重新分配分区,把主题从受损的 broker 上迁走。改进项目之一,就是为"MSK 没有向我们开放的那些 Kafka 控制手段"构建工具和运行手册(runbook)。这句话极其精准地概括了托管服务真正的账单 — 把运维负担交给供应商,供应商修不好的时候,你也修不好。届时你的恢复时间,由供应商的响应速度决定,而不是你自己的工程能力。

而这第二种故障模式实际代价有多高,时间线本身就说明了。法定人数在凌晨 12 点 06 分恢复,但市场直到凌晨 3 点 49 分才开盘。 中间相差 3 小时 43 分钟。Coinbase 直接指出,这段延迟主要是 MSK 问题造成的。让共识集群恢复过来,和让服务恢复过来,从来就不是同一件事。

最诚实的一段 — 就算是 3-AZ 也挡不住这次故障

事后复盘里有一句话,大部分二手报道都悄悄漏掉了。Coinbase 承认,它的某个 Kafka 集群采用的是 2-AZ 配置,这扩大了影响范围(blast radius)并拉长了恢复时间 — 但紧接着又补了一句:MSK 控制平面的缺陷对 2-AZ 集群和 3-AZ 集群造成的影响大致相似。

这不是自我辩护,而是精确。删掉这句话,这起事件能学到的东西就少了一半。

这里带出的教训是:数据平面的冗余,修不好控制平面的故障。 加一个第三可用区,意味着"多了一个还有额外 broker 的地方"。但如果问题不是缺 broker,而是本该选出 leader 的那个东西选不出 leader,那多接一个可用区,只是多堆出一堆选不出 leader 的 broker 而已。冗余只有在控制器还活着的时候才值钱。

值得注意的是,Coinbase 仍然写明会把那个 2-AZ 集群迁到 3-AZ。这个决定是对的 — 3-AZ 依然比 2-AZ 好,"这次没帮上忙"不等于"下次也帮不上忙"。但他们没有把这一步包装成"这次故障的解决方案"。改进项目不假装自己解决了根本原因的事后复盘,比想象中要少见。

重读时间线,单一原因的叙事就站不住了

这起事件目前大体是这样被讲述的:"Raft 集群失去了法定人数,所以 Coinbase 宕机了。"干净利落,容易记住,但和时间线对不上。

交易在晚上 7 点 48 分中断。撮合引擎在晚上 9 点 29 分失去法定人数。交易早在法定人数崩溃前 1 小时 41 分钟,就已经停了。

那么晚上 7 点 48 分究竟是什么让交易停下来的?事后复盘没有明说。但在描述 MSK 问题时,它提到这场波及阻断了手续费服务,手续费服务被阻断后,报价的生成也随之受阻,因此大多数客户体验到的这起事件,不是一次 Kafka 故障,而是撮合出错、报价出错。部分账本流水线、结算,以及若干数据流水线也以同样的方式受到影响。

旁证指向那个方向,但既然事后复盘没有明确说明晚上 7 点 48 分的因果关系,我也不会替它下定论。这里能有把握说出来的话更谨慎,也更有用 — 仅凭时间线本身,就足以证伪单一原因的叙事。 而这正是 Coinbase 自己的框架 — 是两种故障模式,各自本可独立恢复,却在重叠之后相互放大,才把一起局部的云厂商事件推大成了持续数小时的平台级故障。

大型故障实际上就是这样发生的。不是一个根本原因,而是两个平时互不相干的缺陷,在同一个夜晚碰到了一起。事后,我们挑出其中画面感最强的那一个("共识集群失去了法定人数"),拿来当整起事件的名字。就在那一刻,另一半的教训消失了。顺带一提,警惕这种事后重构,正是无责备事后复盘文化真正想守住的东西。

可用区不是故障的最小单位

最后,这起事件还动摇了一个心智模型。

Coinbase 在事后复盘里明确写下了他们自己(以及大多数 AWS 客户)据以设计的原则 — 一整个可用区都可能整体宕掉,设计得当的系统应当靠剩下的可用区继续提供服务。而这类故障,本应是超大规模云厂商设计为在可用区边界内吸收的那种。

但真正死掉的并不是一个可用区,而是一处数据机房 — 比可用区还要小。

这里有一层讽刺。故障比可用区还小,却依然带走了 5 节点集群中的 3 个。因为置放组的影响范围,同样比可用区还小。两个小圆圈叠在了一起。"按可用区来思考"这个习惯,不仅会让人对比可用区更大的故障(整个区域级别的控制平面问题)视而不见,也会让人对比可用区更小的故障视而不见。真正该问的问题,不是自己的副本分布在多少个可用区里,而是自己的副本分布在多少个真实的故障域里。而一旦用上集群置放组,这个数字就会远小于可用区的数量。

这也正是像CAP 定理这样的抽象,在实务中常常显得无力的原因。理论上,P(分区)只是一个随机变量;现实中,它是冷水机组的管道走向。

那么实务中该改什么

以下是可以从这起事件里诚实地带走的东西。

数的应该是故障域,而不是副本数。 "我们的 etcd 是 5 节点"不是一个答案。真正该问的是"是否存在一个能让这 5 个节点同时死掉的单一事件"。同一个机架、同一台交换机、同一路供电、同一处数据机房、同一个置放组、同一个 Kubernetes 节点组、同一个 AMI、同一条部署流水线。这份清单上任何一处重叠,法定人数的算术就会相应地失真。

如果你刻意耦合了一个故障域,就把逃生的门造出来,并且真的打开它。 同地部署本身不是罪过 — 这正是 Coinbase 自己的诊断。问题在于耦合之后从没有把逃生路径自动化。如果你为了延迟出卖了独立性,那笔交易换来的,就是"迟早要靠人手去拆解的一个夜晚"。提前把那个夜晚预约下来,并且真的演练一遍。

在启动代码里找出并删掉拓扑假设。 "全部 N 个节点都必须可解析才能启动"这样的条件,平时看起来像个健康检查,灾难时就变成一把锁。要知道能不能在退化的集群上启动,唯一的办法就是真的在退化的集群上启动一次。

对托管服务问这个问题 — 如果坏掉的是控制平面,我自己能做什么。 如果答案是"提交一张支持工单",那就是你的 RTO。能不能接受这个 RTO,才是托管与自运维之间真正的分界线,比"运维负担"这个笼统的说法要具体得多。

把数据平面的冗余和控制平面的冗余分开看。 增加可用区属于前者。面对控制器动不了的那类故障,它一分钱都不值。

并且,不要从这次事件里学过头了。 这一条也许最重要。Coinbase 是一家运营着无法承受投票成员之间跨可用区跳数的撮合引擎的交易所。如果你的服务没有这种约束 —— 大多数服务确实没有 —— 那你本来就没有理由使用集群置放组,AWS 的默认行为(分散到各处硬件以最小化相关故障)已经是正确的选择。如果你从这份事后复盘里学到的是"为了低延迟应该把节点挤在一起",那你学错了方向。真正该学的是相反的东西 — 就连一个有真实理由把节点挤在一起的人,都为此付出了这样的代价。

结语 — 以及这篇文章不知道的事

Coinbase 事后复盘的最后一段有这样一层意思:他们的标准并不取决于 AWS 是否出了故障,而是完全取决于自己有没有建出一套足够扎实、能扛住这种故障的系统。这句话概括了本文的全部。云厂商的故障不是你自己故障的借口 — 它只是对你写进设计文档里的那些假设的一次打分。

接下来,说说这篇文章的局限。

我们只有一方的说法。 查看 AWS 的事后摘要(post-event summary)列表会发现,这起 2026 年 5 月的热故障事件没有官方摘要,MSK 控制平面缺陷也没有官方摘要(最新的一条是 2025 年 10 月的 DynamoDB 事件)。所以,本文中关于 MSK 缺陷的一切内容 —— leader 重新选举被阻断、集群卡在"healing"状态、2-AZ 与 3-AZ 受到的影响大致相似 —— 都只是Coinbase 一方的说法,不是 AWS 已核实或确认的事实。截至 6 月 1 日,Coinbase 自己也写道,他们仍在与 AWS 一起对这个缺陷做根本原因分析。所以这部分内容,适合当作暂定结论来读。

而且,我们并不知道晚上 7 点 48 分究竟发生了什么。 如前所述,事后复盘没有说明第一次交易中断的因果关系。有旁证,但没有证实。

留下这样的空白,是这个体裁的现实。事后复盘不是真相的副本,而是撰写它的那个组织,在那一刻所知道、并且能够说出来的东西的副本。即便如此,Coinbase 这篇文章依然值得一读 — 它在为自己的选择辩护的同时,也写下了这个选择的代价,甚至写明自己的改进项目并没有解决根本原因。同时做到这两点的文章并不常见。

参考资料

현재 단락 (1/84)

分布式系统类文章里,八成都是从同一句话开始的:"Raft 用 2f+1 个节点容忍 f 个故障。"这话没错,也会出现在考试里,实务中也没有人因为不知道这句话而出事。真正出事的人,是栽在那句话没写出来的...

작성 글자: 0원문 글자: 9,021작성 단락: 0/84