一、分布式共识:为什么我们需要 Raft

1.1 CAP 定理与一致性的困境

在分布式系统中,数据可能分布在多个节点上,如何保证这些节点上的数据在面对网络分区、节点故障时仍然保持一致,是分布式系统最核心的挑战之一。CAP 定理告诉我们,网络分区(P)发生时,必须在一致性(C)和可用性(A)之间做出选择。CP 系统(如 etcd、ZooKeeper)选择了一致性,而共识算法就是实现 CP 选择的基石。

1.2 Paxos 的困境

在 Raft 之前,Lamport 的 Paxos 算法是分布式共识的事实标准。然而 Paxos 以其难以理解和实现而闻名 —— 即使在今天,能正确实现 Multi-Paxos 的团队屈指可数。Raft 的作者 Diego Ongaro 在论文中提出了一个明确目标:设计一个比 Paxos 更容易理解的共识算法,同时不牺牲正确性和性能。

Raft 通过"分离"将共识问题分解为三个相对独立的子问题:Leader 选举、日志复制、安全性,使得整个算法的推理路径更加清晰。

二、Raft 核心概念与角色模型

2.1 三种服务器状态

Raft 中每个节点在任意时刻都处于以下三种状态之一:

  • Leader:处理所有客户端请求,发起日志复制,定期向 Follower 发送心跳。一个 Term 内最多一个 Leader
  • Follower:被动接收 Leader 的请求,不主动发起任何操作。如果选举超时内未收到心跳,转为 Candidate
  • Candidate:在选举中发起投票请求,获多数票后成为 Leader

2.2 Term(任期)

时间被划分为一个个 Term(任期),每个 Term 是一个连续递增的 Term 编号。Term 是一次逻辑时钟,用于检测过期的信息。每个节点维护当前的 Term 编号,请求中的 Term 过期的节点会立即拒绝请求。每个 Term 最多选出一个 Leader,但也可能选不出(选票分裂导致无人获多数)。

2.3 RPC 通信原语

Raft 仅使用两种 RPC:

  • RequestVote RPC:Candidate 在选举期间发起,携带 Candidate 的 Term、最后日志索引和最后日志 Term
  • AppendEntries RPC:Leader 发起,用于日志复制和心跳(不带日志项时为心跳)

三、Leader 选举机制

3.1 触发与流程

当 Follower 的选举计时器(election timeout,通常 150-300ms 随机化)超时未收到 Leader 心跳时,该节点增加自己的 Term、转为 Candidate、投票给自己,并向其他所有节点并行发送 RequestVote RPC。Candidate 会处于三种结局之一:

  1. 赢得选举:获得集群多数票(N/2+1),立即成为 Leader 并向所有节点广播心跳
  2. 另一节点成为 Leader:收到更高 Term 的 AppendEntries,Candidate 退为 Follower
  3. 选举超时:选票分裂导致无人获多数,Term 增加后重新发起选举

3.2 任期随机化与选票承诺

为减少选票分裂的概率,Raft 要求每个节点的选举超时时间在一个范围内随机选择(通常 150-300ms)。这样不太可能两个节点同时超时。此外,每个节点一个 Term 内只能投票给第一个请求的 Candidate,保证每个 Term 最多只有一个 Candidate 能赢得选举。

3.3 日志完整性检查

Candidate 的日志必须至少和大多数节点一样"新"才能当选。RequestVote RPC 包含 Candidate 的最后日志索引和 Term,接收者会比较:如果请求者的日志不比自己新,则拒绝投票。比较规则:先比最后日志 Term,Term 相同则比日志长度。

四、日志复制:Raft 的核心工作

4.1 日志项结构

每个日志项包含三个要素:

  • Term:日志项创建时的 Leader Term
  • Index:日志项的位置(从1开始)
  • Command:客户端提交的命令(状态机指令)

4.2 提交流程

日志复制是 Raft 将客户端命令持久化到多数节点的过程:

  1. Leader 接收客户端命令,追加到本地日志
  2. Leader 并行向所有 Follower 发送 AppendEntries RPC(携带新日志项)
  3. Follower 检查一致性(前一条日志是否匹配),匹配则追加,冲突则删除冲突项及后续所有项
  4. Leader 收到多数 Follower 的成功响应后,将日志项标记为 Committed,应用到状态机
  5. Leader 在下次心跳或 AppendEntries 通知 Follower 最新的 CommitIndex

4.3 日志匹配性质

Raft 依赖两个关键的日志匹配性质保证安全性:

  • 不同日志中的两个项如果 Index 和 Term 相同,则存储相同的命令
  • 不同日志中的两个项如果 Index 和 Term 相同,则之前所有项都相同

五、安全性约束

5.1 选举限制

Raft 不允许一个日志项不是来自多数派的 Leader 被当选。这意味着 Leader 的日志必须包含所有已提交的日志项。

5.2 Leader 不提交之前 Term 的日志

这是 Raft 中最微妙的安全约束。直接提交当前 Term 的日志(因为多数复制即可),但对于之前 Term 的日志,不能仅凭多数复制就提交。这是因为:

考虑一个场景:旧 Leader 将日志复制到多数派后崩溃,新 Leader 和旧 Leader 的多数派有重叠。如果新 Leader 直接提交了旧 Term 的未提交日志,可能会覆盖已经被应用到状态机的正确日志。正确的做法:新 Leader 继续接收新 Term 的日志,当新 Term 的日志提交时,旧 Term 的日志也间接被提交。

5.3 安全性证明

Raft 通过反证法证明 Leader Completeness Property:如果某日志项在某 Term 被提交,则该日志项一定存在于后续所有 Term 的 Leader 日志中。

六、集群成员变更

6.1 联合共识(Joint Consensus)

Raft 使用两阶段成员变更保证配置变更期间不出现双 Leader:

  1. 联合共识阶段:新配置 Cnew 和 Cold 联合组成过渡配置。所有决策需要 Cold 和 Cnew 中各自多数同意
  2. 提交 Cnew:联合共识提交后,Leader 广播 Cnew,此后按 Cnew 运行

这种方法避免了整个变更过程中任何节点子集成为多数导致的分脑。

6.2 单节点变更

实际实现(如 etcd raft)中,两阶段可能被简化为每次只增减一个节点。单节点变更保证了新旧配置的多数派一定有重叠。

七、日志压缩与快照

7.1 日志无限增长问题

长时间运行后,日志条目数量会持续增长,占用大量内存,影响恢复速度。Raft 允许 Leader 创建快照:将当前状态机的完整状态序列化为快照,连同最后的日志 Index 和 Term 写入快照。

7.2 InstallSnapshot RPC

当 Leader 需要向一个严重落后的 Follower 同步时,远比日志复制高效的方式是发送快照。Leader 通过 InstallSnapshot RPC 发送快照数据,Follower 收到后重置自身日志并从快照起点恢复。

八、Raft 实现生态与实战应用

8.1 HashiCorp Raft (Go)

HashiCorp 的 Raft 实现是 Go 生态中最完整的参考实现,被 Consul、Vault 等广泛使用。它提供了可插拔的 Transport(TCP/gRPC)、Storage(BoltDB/MDBM)和 Snapshot 机制:

config := raft.DefaultConfig() config.LocalID = raft.ServerID("node1") store := raft.NewInmemStore() snapshotStore := raft.NewInmemSnapshotStore() transport := raft.NewNetworkTransport(raft.NewTCPStreamLayer(listener), 3, 10*time.Second, os.Stderr) r, err := raft.NewRaft(config, (*fsm)(nil), store, store, snapshotStore, transport) // Bootstrap with cluster configuration future := r.BootstrapCluster(raft.Configuration{ Servers: []raft.Server{{ID: config.LocalID, Address: transport.LocalAddr()}}, })

8.2 etcd/raft (Go)

etcd 使用的 Raft 实现是经过高度优化的。它分离了层的职责:

  • raft 核心:仅处理消息传递,不直接进行网络 IO
  • rafthttp:负责 HTTP 长连接传输 Raft 消息
  • wal + snap:日志和快照的持久化层

etcd raft 的特点是使用 Node 接口包装 Raft 状态机,用户只需实现 Ready 接口处理 Ready 事件。

8.3 TiKV/rust-rocksdb (Rust)

TiKV 使用的 Raft 实现是 raft-rs(Rust 语言实现),配合 RocksDB 作为底层存储,成就了 TiDB 的底层分布式 KV 引擎。raft-rs 的特点是内存安全、零拷贝消息编解码,以及深度优化的批量处理。

let storage = MemStorage::new(); let config =.Config { id: 1, ..Default::default() }; let mut node = RawNode::new(&config, storage, vec![]).unwrap(); // 处理 Raft 消息 node.step(message); // 处理 Ready 状态 let mut ready = node.ready(); if !ready.entries().is_empty() { storage.append(ready.entries()).unwrap(); } node.advance(ready);

九、性能优化与生产实践

9.1 批处理与流水线

Raft 的一个重要优化是批处理日志项:Leader 不逐条发送 AppendEntries,而是积累一批后一次性发送。配合流水线(pipeline)机制:发送第一条 AppendEntries 后不等响应就发送第二条,网络利用率大幅提升。

9.2 ReadIndex 与 LeaseRead

读请求如果走 Raft 日志会严重影响性能。Raft 提供两种优化方案:

  • ReadIndex:Leader 发起一轮心跳确认自己仍是多数派 Leader,确认后可直接读状态机
  • LeaseRead:基于 Leader 租约(lease),在租约期内(通常选举超时一半)直接读状态机

9.3 Pre-Vote 优化

网络分区中的节点恢复后,如果立即发起选举,会打断当前 Leader 工作。Pre-Vote 优化要求 Candidate 先在预投票阶段确认能否获多数支持,只有确有可能才真正增加 Term 发起选举。

十、总结

Raft 之所以成为工业界共识算法的事实标准,关键在于它在正确性、性能和可理解性之间取得了良好平衡。理解 Raft 的关键在于掌握三个子问题的解法,尤其是日志复制与安全性约束之间的交互。

在生产环境中部署 Raft 集群时,需要注意选举超时设置、日志批量处理、快照策略、网络抖动处理等细节。目前 etcd、TiKV、Consul、CockroachDB 等系统都运行着 Raft,它是构建可靠分布式系统的强大基石。

进一步学习建议:阅读 Raft 论文的 Figure 2(精确定义)和 Raft 博士论文的 Chapter 3(实现细节),尝试自己动手实现一个 mini-Raft,体验各模块的工程设计取舍。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部