引言
在分布式系统中,多个节点之间达成一致是核心问题之一。当你在多个服务器上运行数据库、配置管理或服务发现系统时,必须确保即使部分节点发生故障,整个系统仍然能正确工作。共识算法(Consensus Algorithm)正是解决这一问题的基石。
Paxos 是最早的共识算法之一,但因其极高的理解门槛和实现复杂度而饱受诟病。2013 年,Diego Ongaro 和 John Ousterhout 提出了 Raft 算法——一种专门为可理解性设计的共识算法,迅速成为工业界的事实标准。如今,etcd、Consul、TiKV、CockroachDB 等关键系统均采用 Raft 作为核心共识引擎。
本文将从分布式系统的一致性困境出发,深入剖析 Raft 算法的核心机制与工程实践,让你真正理解这个"让多台计算机像一台计算机一样工作"的魔法。
一、为什么需要共识算法?
1.1 分布式系统的故障模型
分布式系统面临多种故障类型:
- 崩溃故障(Crash Fault):节点突然停止工作,不再响应消息
- 网络分区(Network Partition):节点之间网络中断,形成孤岛
- 拜占庭故障(Byzantine Fault):节点可能任意行为,包括发送错误信息(Raft 不解决此类故障)
Raft 主要处理崩溃故障和网络分区,假设节点不会主动作恶。
1.2 一致性的层次
不同应用场景对一致性有不同要求:
- 强一致性(Linearizability):任何读取都能看到最新写入的结果
- 顺序一致性(Sequential Consistency):所有节点看到的操作顺序一致,但不保证实时性
- 最终一致性(Eventual Consistency):如果没有新写入,最终所有节点会趋于一致
Raft 提供的是强一致性——通过 Leader 选举和日志复制,确保所有节点的状态机以相同顺序执行相同的命令。
二、Raft 算法核心机制
2.1 节点角色与状态转换
Raft 集群中的节点有三种角色:
- Leader(领导者):处理所有客户端请求,管理日志复制。正常工作时始终只有一个 Leader
- Follower(跟随者):被动接收 Leader 的请求,不主动发起通信
- Candidate(候选人):选举过程中临时角色,发起投票请求
节点状态转换如下:Follower 在选举超时后变为 Candidate → Candidate 发起选举 → 获得多数票则成为 Leader → 如果收到更高 term 的消息则退化为 Follower。
2.2 术语(Term)机制
Term 是 Raft 的逻辑时钟,每个 term 都有一个编号(单调递增)。Term 的作用是:
- 标识 Leader 的"任期",防止旧 Leader 干扰新 Leader
- 当节点发现自己的 term 小于消息中的 term 时,自动更新并退为 Follower
- Candidate 和 Leader 在收到更高 term 的消息时立即退为 Follower
Term 机制本质上是一种"版本号",确保系统总是向最新的状态演进。
2.3 Leader 选举
Leader 选举是 Raft 的第一步。选举流程如下:
- 所有节点初始为 Follower 状态
- 如果 Follower 在"选举超时"(通常 150-300ms,随机化)内没有收到 Leader 的心跳,它就假设 Leader 已经崩溃
- Follower 自增 term,变为 Candidate,向所有其他节点发送 RequestVote RPC
- 每个节点在同一 term 内只能投一票(先到先得原则)
- 如果 Candidate 获得超过半数节点的投票,成为 Leader
- 如果选举超时仍未产生 Leader(选票分裂),进入下一轮 term 重新选举
随机化选举超时是 Raft 的关键设计——它大幅降低了选票分裂的概率。假设两个节点同时发起选举,由于超时时间不同(如 150ms 和 280ms),下一次选举中它们不太可能再次同时超时。
2.4 日志复制
Leader 选举完成后,系统进入正常工作模式。日志复制的流程:
- 客户端向 Leader 提交命令(如 SET x=5)
- Leader 将命令追加到本地日志(尚未提交)
- Leader 通过 AppendEntries RPC 将日志条目并行发送给所有 Follower
- Follower 将日志写入本地后回复 ACK
- 当 Leader 收到超过半数的 ACK 后,提交该条目(apply 到状态机)
- Leader 通知 Follower 提交该条目
这个机制保证了:一旦一个条目被提交,它就不会丢失——因为至少有一个已确认的 Follower 拥有该条目,未来的 Leader 必然在这些节点之中。
2.5 日志匹配特性
Raft 保证以下日志匹配特性:
- 如果两个日志条目有相同的 index 和 term,则它们存储相同的命令
- 如果两个日志条目有相同的 index 和 term,则之前的所有条目都相同
第二条保证来自 AppendEntries RPC 的"一致性检查"——Leader 在 AppendEntries 中携带前一个日志的 index 和 term,Follower 检查匹配后才接受新条目。如果不匹配,Follower 拒绝并回退,Leader 逐步找到分歧点进行覆盖。
2.6 安全性保证
Raft 的核心安全保证是状态机安全性:如果某个节点将某个 index 的日志条目应用到了状态机,那么没有任何节点会在同一 index 应用不同的条目。
为了保证这一点,Raft 引入了几个限制:
- 选举限制:Candidate 的日志必须至少与其他节点一样新(投票者拒绝投票给日志过旧的 Candidate)
- Leader 不提交之前 term 的条目:Leader 只提交当前 term 的条目,之前 term 的条目通过"连带提交"的方式间接提交
第二条限制是为了防止"已提交的日志被覆盖"——假设旧 Leader 复制了一个条目但尚未通知提交就崩溃了,新 Leader 不会主动提交这个条目,而是在提交新 term 的条目时"带走"它。
三、Raft 集群成员变更
实际运维中经常需要添加或移除节点,这被称为集群成员变更(Membership Change)。
3.1 联合共识(Joint Consensus)
如果直接切换配置,可能导致同一个 term 内出现两个不同的多数派(Majority)。Raft 作者提出的解决方案是联合共识:
- Leader 同时使用新旧配置的联合配置(Cold ∪ Cnew)
- 所有决策(选举和提交)需要同时获得 Cold 和 Cnew 中的多数同意
- 联合配置提交后,切换到新配置
这种方式保证了在任何时刻,旧配置和新配置的多数派至少有一个节点重叠,从而避免脑裂。
3.2 单节点变更
实践中更常用的是单节点变更(一次只添加或移除一个节点),由 Diego Ongaro 在博士论文中提出。这种方法更简单且同样安全——只要证明每次单节点变更不会导致两个不相交的多数派即可。
四、日志压缩与快照
随着系统运行,日志会无限增长。Raft 通过快照(Snapshot)机制解决这个问题:
- 当日志达到一定大小时,Leader 创建一个快照,包含状态机的当前状态
- 快照包含最后一个被应用的日志条目的 index 和 term(即 last included index/term)
- 快照创建后,此前的日志可以被安全删除
- 如果新加入的节点或严重落后的 Follower 需要的日志已被删除,Leader 通过 InstallRPC 发送整个快照
快照机制对 Leader 的性能至关重要——它避免了日志无限增长导致的内存压力和恢复延迟。
五、线性一致性与 Read Index
Raft 的日志复制保证了写入的线性一致性,但读取也需要特殊处理。
5.1 为什么直接读取 Leader 不够?
如果网络分区发生,旧 Leader 被隔离但仍认为自己是 Leader,此时可能返回过时的数据。Raft 提供了两种安全的线性读取方案:
5.2 Read Index
- Leader 记录当前的 commit index 作为 read index
- Leader 向 Follower 发送一次心跳,确认自己仍是 Leader
- Leader 等待状态机至少应用到 read index
- 返回读取结果
5.3 Lease Read
Read Index 仍需要一次网络往返(心跳)。Lease Read 利用 Leader 的"租约"(在选举超时内,不可能有新 Leader 产生),在租约有效期内直接读取状态机而不需心跳确认。
TiKV 的实现中使用了 Lease Read 来优化读取性能。
六、工程实践与优化
6.1 预投票(Pre-Vote)
一个刚启动或网络恢复的节点如果频繁发起选举(因为它发现没有 Leader),会导致当前 Leader 退化为 Follower。Pre-Vote 机制要求节点先进行一轮预投票——只有预投票成功才真正发起选举,避免不必要的 term 膨胀。
6.2 流水线(Pipeline)复制
标准 Raft 中 Leader 必须等待上一个 AppendEntries 的响应后才能发送下一个。流水线优化允许 Leader 连续发送多个 AppendEntries 而不等待每个响应,大幅提升吞吐量。
6.3 Batch 优化
将多个客户端请求打包成一个日志条目,减少 RPC 调用数量。etcd 中实现了多层 Batch:写请求先进入一个缓存区,批量写入 WAL(Write-Ahead Log),再批量提交到 Raft 模块。
6.4 Checkpoint 与 Fast Election
新 Leader 产生后,可以延迟一小段时间再接受客户端请求(等待可能的未提交日志完成),或者对一些"已知落后太多"的 Follower 使用快照同步而非逐条复制。
七、Raft vs Paxos
| 维度 | Paxos | Raft |
|---|---|---|
| 可理解性 | 极低,论文难以理解 | 高,设计目标即为可理解性 |
| 实现复杂度 | Multi-Paxos 实现极为复杂 | 模块化设计,容易正确实现 |
| 性能 | 理论上相同 | 优化后与传统 Multi-Paxos 相当 |
| 工业应用 | Chubby、Spanner | etcd、TiKV、CockroachDB、Consul |
| Leader 选举 | 不固定,可流水线 | 强 Leader,更简单清晰 |
Raft 的优势不在于性能或功能上的差异,而在于工程可及性——理解和实现 Raft 的成本远低于 Paxos,使得更多团队能够构建可靠的分布式系统。
八、动手实现:最简 Raft
理解 Raft 最好的方式是自己实现。以下是一个最简 Leader 选举的核心逻辑(伪代码):
// 节点状态\nenum State { Follower, Candidate, Leader }\n\nclass RaftNode {\n int currentTerm = 0;\n int votedFor = -1;\n State state = Follower;\n int electionTimeout; // 150-300ms 随机\n\n // 超时处理\n void onElectionTimeout() {\n state = Candidate;\n currentTerm ;\n votedFor = selfId;\n // 向所有节点发送 RequestVote\n for (node in peers) {\n sendRequestVote(node, currentTerm, lastLogIndex, lastLogTerm);\n }\n resetElectionTimer();\n }\n\n // RequestVote 投票逻辑\n bool grantVote(int candidateTerm, int candidateId, \n int lastLogIndex, int lastLogTerm) {\n if (candidateTerm

发表评论 取消回复