在Raft实现中发现Bug
摘要
Antithesis发布了一篇博客文章,详细介绍了在多个开源Raft共识实现(包括HashiCorp Raft和OpenRaft)中发现的Bug,强调了测试分布式系统的困难以及对更好工具的需求。
<p><a href="https://lobste.rs/s/3he4yj/finding_bugs_raft_implementations">评论</a></p>
查看缓存全文
缓存时间: 2026/07/27 17:47
# 在 Raft 实现中发现错误 | Antithesis 来源:https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/
## 引言 (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#introduction)
TW Lim 头像
TW Lim
技术文档工程师
Raft 共识协议是互联网的基础之一。Raft 提供了 TLA+ 形式化规范 (https://github.com/ongardie/raft.tla) 以及一份具体、详细的实现指南 (https://raft.github.io/raft.pdf),已有数百个团队根据该指南的说明创建了算法的开源实现。它是迄今为止生产系统中使用最广泛的共识算法。
尽管该论文以其广为人知的易读风格而闻名,**我们却在我们测试的*每一个* Raft 实现中都发现了错误**,包括 HashiCorp Raft、Aeron Cluster、OpenRaft 和 MicroRaft——尽管它们在形式化方法、仔细的代码审查、单元测试以及生产环境中多年的测试上投入了大量精力。我们发现的问题表现为违反了 Raft 的主要不变量(论文中称为*状态机安全性*,其他地方通常称为*全序交付*)。如果你正在使用某个 Raft 实现,你可能需要检查一下它是否有错误。我们已经向上游提交了错误报告。
这并非旨在批评 Raft、其作者、其实现者或任何特定实现。即使是有了形式化规范和详细的实现指南,编写 Raft 实现也并非易事。相反,这是一个关于分布式系统中正确性的故事——错误的不可避免性、单一方法的不足,以及习得性无助带来的高昂代价。
这是一篇长文。第一部分是一篇立场文章,但其余部分是对我们发现的问题的详细分析,以一个实现为例,讨论我们如何发现它们,以及*为什么*我们认为它们存在。
## 为什么这很重要 (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#why-this-matters)
如果你从事分布式系统工作,那么你曾在某个时候参与过紧急事件处理会议(如果你还没有,别担心,你会的),处理类似于这种情况 (https://blog.cloudflare.com/a-byzantine-failure-in-the-real-world/) 或这种情况 (https://www.cockroachlabs.com/docs/advisories/a162085) 的故障——这两者都是由共识失败引起的。当共识问题导致事故时,它必然影响深远,并且查明根本原因、复现和修复都极其痛苦。
许多共识问题仍然“未解决”,因为它们难以复现。我们很难确切知道这些问题的真实世界后果是什么,因为共识协议位于软件栈的底层。它们是在破坏某人的个人 D&D 游戏存档吗?是核电站的安全风险?还是导致德国火车延误?这完全取决于协议部署在哪里。可以确定的是,这些错误极大地浪费了开发者的时间,并影响了数百万甚至数亿的用户。
然而,我们接受共识问题和其他深层分布式系统错误是生活的一部分——但曾几何时,我们也接受霍乱和等待轮到自己使用大型机是生活的一部分。开发者值得更好的。每个依赖我们编写软件的人都值得更好的。
### 错误并非生活的一部分 (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#bugs-are-not-a-fact-of-life)
长期以来,我们一直认为共识错误是不可避免的,因为共识协议真的非常非常难以正确测试。要真正、彻底地测试一个 Raft 实现——或任何分布式系统——你需要有效地想象整个软件栈中可能出错的所有事情,并编写一个测试来检查它是否会破坏你的共识协议。在关键系统中,几乎不可能编写足够的测试来提供这种保证。
我们通过两种方式绕过这个问题。首先,我们将测试强加给用户,以故障、下游错误、紧急会议等形式。一个开发团队不可能编写足够的测试,但只要有足够的部署、足够的用户以及足够的系统配置,最终你会找到所有问题。其次,我们依赖形式化验证,这就是今天构建共识算法的方式。我们定义一个可以在数学上*证明*是正确的模型,然后我们……将这个完美的、柏拉图式的东西翻译成代码。大多数 Raft 实现都是这样构建的。https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#fn-body-1 这里有程度之分。Raft 协议是为数不多的满足最严格“形式化验证”标准的共识协议之一,它拥有手动证明、模型检查以及机械化的正确性证明。据我们所知,每个公开的共识协议都有手动证明/书面正确性论证,但只有 Raft、Paxos 和 MultiPaxos 拥有机械化证明。
### 规范不是代码 (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#specifications-are-not-code)
形式化验证是一个很好的起点,它有助于确认核心设计是合理的。如果从形式化规范开始,实现分布式共识协议所导致的错误数量将比任何 Raft 实现现在的错误数量少很多个数量级。https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#fn-body-2 反之,人们可能会好奇,如果 FoundationDB 团队从形式化规范开始,他们可能会做什么。但这里的问题展示了从形式化规范到实际生产代码所固有的困难。只要实现是由不可靠、不完美的程序员(无论是人类还是 LLM)完成的,那么实际代码以及使用它的系统,其稳固性就只能达到实现者假设的程度——而不是形式化验证的模型。此外,形式化规范很少真正完整——以 Raft 为例,TLA 规范涵盖了核心协议,但没有涉及像 `installSnapshot`、副本替换或处理网络数据包损坏等细节。
### 测试实际上可以很简单 (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#testing-can-actually-be-simple)
发现这些错误并不需要对共识或分布式系统有特别深入的了解。我们使用一种简单的方法发现了它们,一个初级工程师可以在不到一天的时间内实现这种方法,这实际上是我们能想到的最简单的状态机复制工作负载。关键在于,这是在系统运行在 Antithesis 中时发生的,同时承受了现实世界中会发生的各种故障。我们想强调这一点,因为我们在测试复杂系统时经常遇到习得性无助。我们的行业认为(有一定道理)为分布式系统编写测试需要比我们拥有的更多的专业知识、时间或 tokens。这导致了持续的测试不足,进而导致大量的开发者时间浪费在救火上(更不用说精神上和情感上的疲惫了)。开发者值得更好的。每个依赖我们编写软件的人都值得更好的。
## 测试 Raft (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#testing-raft)
Marco Primi 头像
Marco Primi
分布式系统工程师
### 关于 Antithesis (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#about-antithesis)
Antithesis 是一个自主测试平台,它在确定性模拟环境中运行分布式系统,使用随机生成的输入,并注入激进的故障。要使用 Antithesis,你需要将完整的分布式系统部署到模拟环境中,同时部署一个工作负载——一个驱动被测系统的客户端。Antithesis 将被测系统暴露在它在生产环境中会经历的不可预测的波动中,这一切都在模拟环境的安全范围内。通过随机化输入和故障,并智能地搜索系统的状态空间,以观察系统不变量是否曾被违反。我们维护一个内部课程,包含我们用来基准测试 Antithesis 漏洞发现性能的系统和错误。https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#fn-body-3 我们一直在向课程中添加共识基准,因此我们已经测试了很多 Raft 实现。构建独特事物的一部分困难在于,你还需要构建一种衡量其性能的方法。
### 关于 Raft (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#about-raft)
分布式系统中一种常见的架构模式是状态机复制(SMR):系统中的所有副本都运行相同确定性状态机的副本,并且相同的命令序列被传递给所有副本以转换数据/状态。这导致副本以软步进方式前进——应用 N 个命令后的状态在所有副本中是*一致的*,并且所有副本*最终*会应用所有命令。分布式数据库(FoundationDB、Aerospike 等)、消息队列(Kafka、NATS 等)、键值存储(etcd、Zookeeper)、区块链(Bitcoin、Ethereum)以及许多其他系统都基于 SMR。实现 SMR 的一个基本要求是所有副本*必须*以相同的顺序接收相同的命令集——全序交付。全序交付描述起来很简单,但在面对网络波动、进程崩溃和磁盘故障时很难架构。由于全序交付对于 SMR 的正常运行需要*保证*,大多数系统依赖于少数几种消息抽象之一,其中原子广播(也称为全序广播)是最常见的。Raft 被认为是最友好的原子广播协议,Paxos 和 viewstamped replication 也在广泛使用。
### 我们的测试方法 (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#our-testing-approach)
为了测试 Raft 实现,我们在 Antithesis 中运行一个三节点的 Raft 集群,使用一个我们称之为“区块链(Chain of Blocks)”的简单工作负载。区块链测试了 Raft 最重要的不变量:在应用 N 个命令后,所有副本的状态匹配。它由两个非常简单的组件组成:
- 一个单一状态的状态机,它只对传入命令的字节进行哈希。在每次应用命令后,状态由 `SHA256(state || command)` 组成。这足以让我们观察到副本之间的状态分歧。
- 一个无状态客户端,其唯一责任是提交由随机字节数组组成的新命令。
你可以在此处 (https://antithesis.com/docs/resources/chain-of-blocks/) 阅读关于此工作负载的更多信息,并查看示例实现。
像 Raft 这样的组件由专家开发和审查,并经过多年的实战考验。因此,对我们来说,这种单一、简单的测试策略——一个初级工程师可以在一个下午写完(或者 Claude 可以用几个 tokens 写完)——在 Antithesis 中运行时仍然能发现错误,这是非常引人注目的。
## 结果 (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#results)
在所有情况下,网络分区和波动足以产生分歧的例子(即不需要节点关闭/重启,或磁盘损坏,或其他故障)。
以下是一个来自日志的示例片段,显示了状态机分歧:
```
[node1]: Applied block 244 (2ad8d...) state: 01fd753a => faf6ba1e
[node2]: Applied block 244 (c92fb...) state: 01fd753a => dab0977c
[node3]: Applied block 244 (c92fb...) state: 01fd753a => dab0977c
```
在应用了 243 个区块后,所有副本的哈希值匹配(0x01fd753a)。然后节点1应用了与节点2和节点3不同的第244个命令(0x2ad8d... 对比 0xc92fb...)。这导致在应用了244个命令后出现不同的状态哈希(0xfaf6ba1e 对比 0xdab0977c),违反了 Raft(以及底层原子广播)的主要安全属性:所有副本以相同的顺序应用相同的命令序列。
举例来说,以下是我们在一个著名的开源 Raft 实现中发现错误的详细情况。我们强调,我们在其他实现中也发现了类似的错误,但这里我们深入而非宽泛,只展示一组错误,以控制本文的长度。我们计划在后续更新中补充我们处理过的其他 Raft 实现中的错误详情。
### HashiCorp Raft (https://antithesis.com/blog/2026/finding-bugs-in-raft-implementations/#hashicorp-raft)
Rohan Padhye 头像
Rohan Padhye
研究员
HashiCorp Raft 是一个成熟、流行的开源 Raft 实现,也是支持广泛使用的生产基础设施工具(如 Consul、Nomad 和 Vault)的基础共识引擎。*我们不认为 HashiCorp Raft 比我们测试的其他实现更不可靠,或者 HashiCorp Raft 中的错误比其他实现中的错误更严重*——它们都违反了相同的状态机安全核心属性。
我们总共发现了三个不同的错误:一个导致大量安全违规(即如上所述的数据分歧),另外两个导致活性违规(某些节点或整个集群无法取得进展,除非操作员干预)。我们的错误报告以及我们使用的区块链实现位于此处 (https://github.com/antithesishq/hashicorp-raft-poc)。
当使用 Antithesis 进行*仅仅一小时的*测试时,我们会看到如下报告:
#### Raft 技术入门
为了更好地理解这些错误的性质,我们应该定义一些在 Raft 论文 (https://raft.github.io/raft.pdf) 中常用的术语:
- “任期(Term)”、“领导者(Leader)”、“跟随者(Follower)”、“选举(Election)” + “RequestVote” RPC:Raft 通过将协议划分为从 term=1 开始的严格递增的*任期*来确保分布式节点集群之间的共识。在每个任期内,节点尝试通过多数投票选举恰好一个节点作为*领导者*;所有其他节点称为*跟随者*。领导者选举使用一个称为 *RequestVote* 的远程过程调用(RPC)进行。
- “日志(Log)”、“提交(Commit)”、“复制(Replication)” + “AppendEntries” RPC:每个节点维护一个数据条目的复制*日志*(例如,客户端发出的命令),日志的某个前缀被称为已*提交*;也就是说,当一个条目*提交*时,节点的状态机才会更新。当客户端向集群发送某个命令时,只有当前领导者可以处理此请求,并且领导者通过称为 *AppendEntries* 的 RPC 将关联的数据条目*复制*给所有跟随者。当集群中的多数节点在其日志中复制了相同的数据条目时,领导者将*提交*该条目到其自己的状态机,并向其跟随者广播这一事实。如果某些跟随者节点由于网络故障而落后,或者它们的条目日志包含来自先前任期的未提交数据,则当前领导者始终可以通过更多的 AppendEntries RPC 来追赶它们。AppendEntries RPC 也用作定期的心跳机制,以广播领导者处于活动状态。
- “压缩(Compaction)”、“快照(Snapshot)” + “InstallSnapshot” RPC:当日志条目增长过大时,Raft 节点可以选择*压缩*日志中所有已提交的条目,并仅在磁盘上存储直到该点的状态机*快照*。如果领导者节点需要通过 AppendEntries 向跟随者复制数据条目,但其日志已被压缩,它可以改为发出 *InstallSnapshot* RPC 来将整个状态机传输给跟随者。
#### **错误 1 - 异步心跳导致共识中断** (https://github.com/hashicorp/raft/issues/695)
这是我们发现的最严重的错误。
相似文章
发现缺陷
这篇博客文章探讨了生成式测试与单元测试在发现缺陷方面的有效性,以Rust的regex crate为例。它展示了一个自定义模糊测试器如何发现了多个缺陷,并提供了改进测试方法的技术。
记录了不断破坏我的RAG系统的故障模式:分块、过期索引、混合搜索等
一位开发者分享了调试RAG系统时遇到的故障模式,包括分块、过期索引和混合搜索的问题,以及滑动窗口分块和上下文检索等实用修复方法。
大多数生产环境中的 RAG 应用都在自信地胡说八道,而这一现象却鲜有人讨论
文章指出了生产环境中 RAG 系统的一种关键故障模式:由于版本控制问题和缺乏不确定性机制,系统会生成自信但错误的回答。文章建议通过引入路由层、检索评分和幻觉检测等架构改进来缓解这些错误。
令人愉悦的 Rust 集成测试
一篇博客文章,演示如何使用 RAII 和 testcontainers-rs 库,通过运行时管理应用程序基础设施,在 Rust 中编写令人愉悦的集成测试。
我用Rust构建了一个自托管的上下文赌博机装置,并部署在一个实时的AI交易产品上。在发现运行时错误之前,先找到了自己配置中的两个错误。
宣布两个开源Rust项目:Lycan(一种用于上下文赌博机的图执行语言)和Syntra(一个自托管的Docker设备,用于服务Lycan胶囊)。作者在自己的实时AI交易产品上自用测试,发现数据管道错误(而非算法问题)主导了适配工作。