引言

在分布式系统中,多个节点之间达成一致是核心问题之一。当你在多个服务器上运行数据库、配置管理或服务发现系统时,必须确保即使部分节点发生故障,整个系统仍然能正确工作。共识算法(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 的第一步。选举流程如下:

  1. 所有节点初始为 Follower 状态
  2. 如果 Follower 在"选举超时"(通常 150-300ms,随机化)内没有收到 Leader 的心跳,它就假设 Leader 已经崩溃
  3. Follower 自增 term,变为 Candidate,向所有其他节点发送 RequestVote RPC
  4. 每个节点在同一 term 内只能投一票(先到先得原则)
  5. 如果 Candidate 获得超过半数节点的投票,成为 Leader
  6. 如果选举超时仍未产生 Leader(选票分裂),进入下一轮 term 重新选举

随机化选举超时是 Raft 的关键设计——它大幅降低了选票分裂的概率。假设两个节点同时发起选举,由于超时时间不同(如 150ms 和 280ms),下一次选举中它们不太可能再次同时超时。

2.4 日志复制

Leader 选举完成后,系统进入正常工作模式。日志复制的流程:

  1. 客户端向 Leader 提交命令(如 SET x=5)
  2. Leader 将命令追加到本地日志(尚未提交)
  3. Leader 通过 AppendEntries RPC 将日志条目并行发送给所有 Follower
  4. Follower 将日志写入本地后回复 ACK
  5. 当 Leader 收到超过半数的 ACK 后,提交该条目(apply 到状态机)
  6. 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 作者提出的解决方案是联合共识:

  1. Leader 同时使用新旧配置的联合配置(Cold ∪ Cnew)
  2. 所有决策(选举和提交)需要同时获得 Cold 和 Cnew 中的多数同意
  3. 联合配置提交后,切换到新配置

这种方式保证了在任何时刻,旧配置和新配置的多数派至少有一个节点重叠,从而避免脑裂。

3.2 单节点变更

实践中更常用的是单节点变更(一次只添加或移除一个节点),由 Diego Ongaro 在博士论文中提出。这种方法更简单且同样安全——只要证明每次单节点变更不会导致两个不相交的多数派即可。

四、日志压缩与快照

随着系统运行,日志会无限增长。Raft 通过快照(Snapshot)机制解决这个问题:

  1. 当日志达到一定大小时,Leader 创建一个快照,包含状态机的当前状态
  2. 快照包含最后一个被应用的日志条目的 index 和 term(即 last included index/term)
  3. 快照创建后,此前的日志可以被安全删除
  4. 如果新加入的节点或严重落后的 Follower 需要的日志已被删除,Leader 通过 InstallRPC 发送整个快照

快照机制对 Leader 的性能至关重要——它避免了日志无限增长导致的内存压力和恢复延迟。

五、线性一致性与 Read Index

Raft 的日志复制保证了写入的线性一致性,但读取也需要特殊处理。

5.1 为什么直接读取 Leader 不够?

如果网络分区发生,旧 Leader 被隔离但仍认为自己是 Leader,此时可能返回过时的数据。Raft 提供了两种安全的线性读取方案:

5.2 Read Index

  1. Leader 记录当前的 commit index 作为 read index
  2. Leader 向 Follower 发送一次心跳,确认自己仍是 Leader
  3. Leader 等待状态机至少应用到 read index
  4. 返回读取结果

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

维度PaxosRaft
可理解性极低,论文难以理解高,设计目标即为可理解性
实现复杂度Multi-Paxos 实现极为复杂模块化设计,容易正确实现
性能理论上相同优化后与传统 Multi-Paxos 相当
工业应用Chubby、Spanneretcd、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                        
                    
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部