引言:为什么需要共识算法?
在分布式系统中,多个节点就某个值达成一致(Consent)是最基础也是最具挑战性的问题。想象这样一个场景:一个键值存储集群有 5 个节点,客户端向节点 A 写入 x=5,向节点 B 读取 x。当节点故障、网络分区、消息延迟交错出现时,集群如何对外呈现一个"一致性"的状态?这就是共识算法要解决的核心问题。
Raft 是一种以可理解性(Understandability)为首要设计目标的共识算法,由斯坦福大学的 Diego Ongaro 和 John Ousterhout 在 2014 年的论文《In Search of an Understandable Consensus Algorithm》中提出。相比 Paxos,Raft 通过"分解问题"和"减少状态空间"两条路径大幅降低了理解门槛,同时提供了与 Paxos 等价的安全性和性能保证。etcd、Consul、TiKV、CockroachDB 等关键基础设施都基于 Raft 构建。
一、Raft 的设计哲学:强领导与问题分解
Raft 将共识问题分解为三个相对独立的子问题:
1. Leader Election(领导人选举)当现有 Leader 失效时,必须选出一个新 Leader。Raft 使用随机化超时机制避免选举冲突,确保在大多数场景下仅需一轮投票就能选出 Leader。
2. Log Replication(日志复制)Leader 接受来自客户端的写请求,将其封装为日志条目(Log Entry),并行复制到所有 Follower 节点。当多数派(Majority)确认接收后,Leader 提交(Commit)该条目并向客户端返回成功。
3. Safety(安全性保证)确保所有节点按相同顺序应用相同的状态机指令。关键保证包括:Leader 完整性(一个 Leader 一定包含之前所有已提交的日志)、选举限制(不包含全部已提交日志的节点不可能当选 Leader)。
二、节点状态机:Leader、Follower、Candidate
Raft 中的每个节点在任何时刻都处于以下三种状态之一:
| 状态 | 角色职责 | 触发转换 |
|---|---|---|
| Leader | 处理所有客户端请求;定期发送心跳(AppendEntries)以维持权威;管理日志复制 | 当选后成为 Leader;发现更高 term 则退位为 Follower |
| Follower | 被动响应 Leader 和 Candidate 的请求;选举超时后转换为 Candidate | 默认初始状态;收到有效 Leader 心跳则重置超时 |
| Candidate | 发起选举:自增 term、投票给自己、向所有节点发送 RequestVote;收集多数票后成为 Leader | 选举超时(150-300ms 随机)触发;获得多数票成为 Leader;发现更高 term 退回 Follower |
状态转换的核心驱动力是任期号(Term)——Raft 将时间划分为若干个连续的 term,每个 term 从一次选举开始。全局单调递增的 term 是所有节点判断信息新旧的基本依据。
三、Leader Election:随机化超时避免选票分裂
3.1 选举触发机制
每个 Follower 维护一个选举定时器,定时器时长在 [150ms, 300ms] 之间随机选取。当定时器超时且期间未收到任何有效 Leader 的心跳时,Follower 转换为 Candidate:
- 自增当前 term
- 将自己的选票投给自己
- 重置选举定时器(再次随机)
- 向集群中所有其他节点并行发送
RequestVoteRPC
3.2 三选一结果
选举可能产生三种结果:
- 赢得选举:Candidate 获得多数派(N/2 + 1)的选票,立即成为新 Leader,并开始发送心跳
- 选举失败:Candidate 在选举超时内未收集到多数票,或者收到了来自更高 term 的 RPC
- 其他节点当选:Candidate 收到新 Leader 的心跳(AppendEntries),立即退回为 Follower
3.3 随机化超时的数学直觉
在 N 个节点的集群中,如果所有 Follower 使用固定超时,选票必然分摊给多个 Candidate。随机化超时大幅降低了"同时超时"的概率——假设有 3 个节点同时进入 Candidate 状态,每个节点独立随机选取 [150, 300ms] 内的超时值,它们再次同时超时的概率极低。Raft 论文中实测表明,选举平均在 200ms 内完成。
四、Log Replication:两阶段提交的精简实现
4.1 日志结构
每个节点维护一个日志条目序列,每个条目包含三个关键信息:
- Term:该条目创建时的 Leader term
- Index:条目在日志中的位置(从 1 开始)
- Command:要提交到状态机的具体操作
4.2 复制流程
Leader 收到客户端写请求后的处理流程:
- 将命令封装为新的日志条目,追加到本地日志,标记为"未提交"(Uncommitted)
- 通过
AppendEntriesRPC 并行将条目发送到所有 Follower - Follower 将条目持久化到本地日志,返回 ACK
- Leader 收到多数派的 ACK 后,将条目标记为"已提交"(Committed)
- Leader 下一次心跳或 AppendEntries 中携带
commitIndex,通知所有 Follower 前 N 个条目已提交 - 各节点将已提交条目应用到本地状态机
4.3 日志不一致的修复
当 Follower 日志与 Leader 不一致时(如 Leader 崩溃后出现新条目),Leader 执行以下修复流程:
- Leader 为每个 Follower 维护
nextIndex(下一个要发送的条目索引)和matchIndex(已知复制的最高索引) - 当 AppendEntries 因日志不一致而失败时,Leader 递减
nextIndex并重试 - 最终找到 Leader 和 Follower 日志分叉点,Leader 以自身日志覆盖 Follower 的不一致后缀
五、Safety 保证与关键约束
5.1 Leader 完整性(Leader Completeness)
Raft 的核心安全保证:如果一个日志条目在某个 term 中被提交,那么该条目一定存在于所有更高 term 的 Leader 日志中。这意味着 Leader 永远不会"丢失"已提交的数据。
5.2 选举限制(Election Restriction)
Follower 在投票时会比较 Candidate 的日志新旧程度:如果 Candidate 的日志"至少跟自己一样新",才投赞成票。判断规则为:
- Candidate 最后一个条目的 term 更大,则视为更新
- Term 相同时,Candidate 的日志更长(index 更大),则视为更新
这确保了当选 Leader 的节点一定包含了所有已提交的日志。
5.3 提交前 Term 条目
Raft 的一个重要约束:Leader 不能通过"复制到多数派"来间接提交之前 term 的条目。这条规则看似反直觉,但在特定场景下(如 Leader 变更时日志被新 Leader 覆写)保证了安全性。Leader 只能通过提交当前 term 的条目来间接提交之前 term 的条目。
六、Cluster Membership Change
在生产环境中,集群的节点集合可能因扩缩容、故障替换等原因发生变化。Raft 使用联合共识(Joint Consensus)协议来实现安全的成员变更:
- Leader 将新配置以
C-old-new形式写入日志 - 在过渡期间,决策需要同时获得旧配置和新配置的多数派同意
- 配置稳定后,Leader 提交
C-new日志,此后仅需新配置多数派同意
这种方式避免了在配置切换过程中出现两个不相交的"多数派"导致脑裂(Split-Brain)的风险。
此外,单成员变更(每次只增减一个节点)是一种更简单的实现方式,但其类型覆盖场景有限,无法应对多点故障。实际系统如 etcd 使用单成员变更来简化实现,因为单成员变更下不可能出现多数派冲突。
七、Log Compaction:快照机制
随着时间推移,日志会持续增长,消耗大量内存和磁盘空间。Raft 使用快照(Snapshot)进行日志压缩:
- 当日志达到一定大小时,Leader 生成一个快照,包含状态机的当前最后应用状态
- 快照中记录
last_included_index和last_included_term - 快照之前的日志条目被丢弃
当新节点加入或 Follower 严重滞后时,Leader 使用 InstallSnapshot RPC 将最新快照发送给 Follower,Follower 用快照替换本地状态后再继续同步增量日志。
八、Raft vs Paxos:设计哲学对比
| 维度 | Raft | Paxos |
|---|---|---|
| 设计目标 | 可理解性(Understandability) | 理论完备性 |
| 核心范式 | 强领导(Strong Leadership) | 对等协商(Peer-to-Peer) |
| 问题分解 | Leader Election + Log Replication + Safety | Multi-Paxos 隐含分解,无显式分离 |
| 领导选举 | 显式、有独立协议 | 隐式、通常使用外部机制(如 Lease) |
| 日志连续性 | 要求日志条目物理连续 | 允许空洞(Gap) |
| 成员变更 | 联合共识协议清晰定义 | Multi-Paxos 中 M 变更较复杂 |
| 工程实现 | 大量开源实现(etcd、TiKV、Consul) | 实现较少,通常基于 Chubby 经验 |
| 性能 | 等价于 Multi-Paxos,高度优化后可达 100k+ ops/s | 同上,理论等价 |
九、性能优化与工程实践
9.1 批量处理(Batching)
将多个客户端请求打包为一个日志条目(或一次 AppendEntries RPC),减少磁盘 I/O 和网络 RTT 开销。etcd 在优化后可达 10,000-100,000 写操作/秒。
9.2 Pipeline(流水线)
Leader 不必等待前一个 AppendEntries 返回就发送下一个,利用网络全双工特性将吞吐量提升 3-5 倍。
9.3 Read Index / Lease Read
对于一致性读,Raft 提供了几种优化方案:
- Read Index:Leader 向多数派发送一轮心跳确认自己仍是 Leader,然后直接读取状态机
- Lease Read:Leader 维护一个在多数派节点上有效的租约(Lease),在租约有效期内可直接读状态机
- Follower Read:Follower 向 Leader 查询
commitIndex,等待状态机应用到该索引后直接读取
9.4 Pre-Vote 协议
一个刚启动或网络恢复的节点如果立即发起选举(可能因其日志较新但暂时网络未恢复),会导致当前 Leader 退位并触发不必要的选举。Pre-Vote 协议要求节点在不实际增加 term 的情况下先询问其他节点是否愿意投票,只有获得多数派预投票才会真正发起选举。
十、故障场景与恢复
| 故障类型 | 检测方式 | Raft 的应对 |
|---|---|---|
| Leader 崩溃 | Follower 选举超时(150-300ms) | Follower 发起选举,新 Leader 接管,后续请求被重定向 |
| Follower 崩溃 | Leader 收不到 Follower AppendEntries ACK | Leader 持续重试;Follower 恢复后日志不一致由 AppendEntries 一致性检查修复 |
| 网络分区 | 多数派可达的 Leader 继续服务 | 多数派分区维持可用;少数派分区停止写入,恢复后日志自动对齐 |
| 磁盘故障 | 写 WAL 持久化失败或无法读取 | 节点崩溃恢复重放日志;如日志损坏,从快照恢复 |
| 拜占庭故障 | Raft 不直接保证 | 需使用 PBFT 等拜占庭容错算法,Raft 仅应对 Crash Fault |
十一、生产部署建议
- 节点数量:3 或 5 个节点是最佳平衡点。3 节点容忍 1 个故障,5 节点容忍 2 个故障,超过 5 边际收益递减且增加提交延迟
- 心跳间隔:通常为 1/2 ~ 1/3 的选举超时下限,在典型场景中设为 50-150ms
- SSD 存储WAL 的持久化是性能关键,强烈建议使用 NVMe SSD 并独立于状态机数据磁盘
- 监控指标:关键 metrics 包括 term 增长速率、Leader 切换频率、日志复制延迟、快照生成耗时
- 网络:节点间建议使用专用网络,延迟应控制在 10ms 以内
结语
Raft 的成功不仅在于其理论完备性,更在于其工程友好性。它将复杂的共识问题分解为可独立理解的三个子模块,并通过强领导模型简化了协议分析。理解 Raft 不仅是掌握一个算法,更是构建可靠性系统的思维训练——日志即事实(Log as Source of Truth)、多数派即权威(Majority is Authority)、任期即时钟(Term as Logical Clock),这三条原则构成了 Raft 的核心哲学。

发表评论 取消回复