引言

在分布式系统中,多节点之间达成共识是核心难题。无论是分布式数据库的选主、配置管理的一致性,还是微服务协调服务,都依赖共识算法来保证多副本之间的状态一致性。Raft 算法由 Diego Ongaro 和 John Ousterhout 于 2014 年提出,以"可理解性"为设计目标,相比 Paxos 更易学习和工程实现,已成为 etcd、Consul、TiKV 等系统的核心共识引擎。本文从理论到工程实现,全方位剖析 Raft 算法。

一、分布式共识的问题定义

共识问题(Consensus Problem)的核心是:如何让一组独立的节点就某个值达成一致,即使部分节点可能发生故障。一个正确的共识协议必须满足以下三个属性:

  • 终止性(Termination):所有正常节点最终都会做出决定
  • 协定性(Agreement):所有正常节点决定的是同一个值
  • 有效性(Validity):决定的值必须是由某个节点提出的

Raft 通过"分解问题"的策略,将共识拆分为三个相对独立的子问题:选主(Leader Election)、日志复制(Log Replication)、安全性(Safety),逐一解决。

二、Raft 核心概念

2.1 节点状态机

Raft 中每个节点处于三种状态之一:

  • Leader(领导者):处理所有客户端请求,定期发送心跳维持领导权
  • Follower(跟随者):被动接收 Leader 的请求,超时后转为 Candidate
  • Candidate(候选人):发起选举,获得多数票后成为 Leader

状态转换规则简洁明了:Follower 在 ElectionTimeout 内未收到 Leader 心跳 → Candidate;Candidate 获得多数票 → Leader;发现更高 Term 的 Leader → Follower。

2.2 任期(Term)机制

Raft 将时间划分为不定长的 Term,每个 Term 从一次选举开始。Term 是一个全局递增的逻辑时钟,用于检测过期信息:

  • 每个节点持久化存储当前 Term 编号
  • 节点通信时携带 Term,遇到更高 Term 时更新自身
  • Candidate 或 Leader 发现自己的 Term 过时时,立即降级为 Follower
  • 拒绝来自过期 Term 的请求

2.3 日志条目(LogEntry)结构

每个日志条目包含三个关键字段:

  • Term:条目被创建时的任期号
  • Index:条目在日志中的位置(从 1 开始)
  • Command:状态机要执行的具体命令

日志是"仅追加"结构,一旦写入不可修改。这是 Raft 安全性的基石。

三、Leader 选举机制

3.1 选举触发与流程

Follower 维护一个ElectionTimeout定时器(通常150-300ms随机值)。当超时未收到 Leader 心跳或 Candidate 的 AppendEntries 时,该节点:

  1. Term 加 1
  2. 状态转为 Candidate
  3. 投票给自己
  4. 重置选举定时器
  5. 并行向所有节点发送 RequestVote RPC

3.2 RequestVote 投票规则

节点收到 RequestVote 请求时,遵循"日志至少一样新"原则决定是否投票:

  • 候选人的 Term 必须 ≥ 当前 Term
  • 如果 Term 更大,节点会更新自己的 Term 并转为 Follower
  • 每个 Term 每个节点最多投一票(投过票则拒绝)
  • 候选人的日志必须"至少一样新":比较最后日志条目的 Term 和 Index

3.3 选举结果

Candidate 在三种情况下结束选举:

  • 获胜:收到多数节点的选票(包括自己),成为 Leader,立即发送空的 AppendEntries 确立领导权
  • 失败:发现更高 Term 的 Leader 或 Candidate,降级为 Follower
  • 分裂:多节点同时 Candidate,无人获得多数票。Raft 通过随机 ElectionTimeout 大幅降低分裂概率

四、日志复制机制

4.1 正常流程

Leader 当选后开始服务客户端请求:

  1. 客户端向 Leader 提交命令
  2. Leader 将命令追加为新的日志条目
  3. Leader 并行向所有 Follower 发送 AppendEntries RPC
  4. 当多数 Follower 确认接收后,Leader 将该条目标记为 Committed
  5. Leader 通知 Follower 提交并应用到各自状态机
  6. Leader 返回结果给客户端

4.2 日志一致性保证

Raft 保证两条核心性质:

  • 如果两个日志条目有相同的 Index 和 Term,它们存储相同的命令
  • 如果两个日志条目有相同的 Index 和 Term,它们之前的所有条目都相同

第二条性质由 AppendEntries 的一致性检查保证:Leader 在 AppendEntries RPC 中携带前一条日志的 Index 和 Term,Follower 检查匹配后才接受新条目。

4.3 日志冲突修复

当 Follower 日志与 Leader 不一致时(如 Leader 崩溃后新 Leader 上任),AppendEntries 一致性检查会失败。Leader 通过"后退重试"策略解决:

  1. Leader 维护每个 Follower 的 nextIndex(下一个要发送的日志位置)
  2. 当 AppendEntries 被拒绝时,Leader 将 nextIndex 减 1 重试
  3. 逐步后退直到找到双方一致的位置
  4. 从该位置开始覆盖 Follower 的后续日志

优化:Follower 在拒绝响应中携带冲突信息,加速回退。

五、安全性保证

5.1 Election Safety

一个 Term 内最多只能有一个 Leader。因为每个节点每个 Term 只能投一票,不可能有两个 Candidate 同时获得多数票。

5.2 Leader Append-Only

Leader 永远不会覆盖或删除自己日志中的条目,只追加新条目。这保证了如果某个日志条目在某个 Term 被提交,它一定会出现在所有更高 Term 的 Leader 日志中。

5.3 Log Matching Property

如果两个日志包含一个具有相同 Index 和 Term 的条目,则该位置之前的所有日志完全一致。由 AppendEntries 一致性检查递归保证。

5.4 Leader Completeness Property

如果一个日志条目在某个 Term 被提交,它一定存在于所有更高 Term 的 Leader 日志中。这是 Raft 最关键的安全性保证。

此前的 Raft 实现中,新 Leader 当选后需要先提交一个 no-op 空条目才能开始服务,否则可能出现"已提交但未应用"的漏洞。这一细节在工程实现中至关重要。

5.5 State Machine Safety

如果某个节点已将某个 Index 的日志应用到状态机,则其他节点不会在同一 Index 应用不同的日志。这直接由 Leader Completeness 性质推导而来。

六、工程实现关键问题

6.1 持久化状态

崩溃后重启的节点必须恢复到崩溃前的状态。Raft 要求持久化以下三个状态:

  • currentTerm:当前 Term 编号
  • votedFor:当前 Term 投票给谁的 CandidateId(null 表示未投)
  • log[]:所有日志条目

必须在响应 RPC 之前持久化,否则可能出现投两次票或丢失已提交日志的严重错误。

6.2 日志压缩与快照

随着系统运行,日志无限增长会消耗大量内存和恢复时间。Raft 通过快照(Snapshot)解决这个问题:

  • 每个节点独立创建快照,仅覆盖已提交的日志
  • 快照包含:最后包含的 Index 和 Term、状态机的当前状态
  • Leader 通过 InstallSnapshot RPC 向落后太多的 Follower 发送快照

6.3 集群成员变更

生产环境中不可避免需要增减节点。直接切换配置可能导致"脑裂"。Raft 的解决方案是联合共识(Joint Consensus):

  1. Leader 写入一个同时包含新旧配置的日志条目 C-old,new
  2. 此阶段决策需要新旧配置各自的多数同意(交集保证安全性)
  3. 联合共识提交后,写入仅包含新配置的日志 C-new
  4. C-new 提交后,旧配置相关的节点可以安全关闭

6.4 线性一致性读

Raft 的日志复制只保证了写操作的共识,但读操作也需保证线性一致性。两种方案:

  • ReadIndex:Leader 记录当前的 commitIndex,等待状态机至少应用到该 Index 后再返回读结果。心跳确认领导权。
  • LeaseRead:基于租约的优化,Leader 在租约有效期内可直接读,无需每次心跳确认。

七、Raft 与 Paxos 对比

维度RaftPaxos
设计目标可理解性,工程友好理论完备性
问题分解选主/日志复制/三阶段Basic Paxos/Multi-Paxos
Leader必须存在 Leader可无 Leader(柔性 Paxos)
学习难度低,论文可读性高高,久负"难懂"之名
工程实现etcd/TiKV/ConsulChubby/Spanner/ZooKeeper
性能相近,实际差异取决于实现优化相近,实际差异取决于实现优化

八、etcd 中的 Raft 实现解析

etcd 是 CoreOS 开发的分布式 KV store,使用 Raft 实现数据一致性,是 Kubernetes 的后端存储。etcd 的 Raft 实现有这些特色:

  • 预投票(PreVote):Candidate 先进行一轮预投票(不递增 Term),避免因网络隔离的节点不断递增 Term 干扰集群
  • Leader 转移(Leadership Transfer):主动将 Leader 转移到指定节点,用于滚动升级和均衡
  • 批量化与管道化:AppendEntries RPC 批量发送,网络管道化提升吞吐量
  • WAL(Write-Ahead Log):基于预写日志的持久化保证崩溃恢复一致性

九、实战:从零实现一个 Mini Raft

以下是 Raft 核心状态机的伪代码结构:

struct RaftNode {
    persistent: (currentTerm, votedFor, log[]),
    volatile:   (commitIndex, lastApplied),
    leaderState: (nextIndex[], matchIndex[]),
}

impl RaftNode {
    // 选举定时器
    fn election_timeout(&mut self) {
        self.currentTerm += 1;
        self.state = Candidate;
        self.votedFor = self.id;
        send_request_vote_to_all();
    }

    // AppendEntries 处理
    fn handle_append_entries(&mut self, args: AE_Args) -> AE_Reply {
        if args.term < self.currentTerm { return reject(); }
        self.state = Follower;
        self.currentTerm = args.term;
        reset_election_timer();
        // 一致性检查
        if !self.log.matches(args.prevLogIndex, args.prevLogTerm) {
            return reject_with_conflict_info();
        }
        // 追加新条目,解决冲突
        self.log.truncate_and_append(args.entries);
        if args.leaderCommit > self.commitIndex {
            self.commitIndex = min(args.leaderCommit, self.log.last_index());
        }
        return success();
    }

    // 新 Leader 初始化
    fn become_leader(&mut self) {
        self.state = Leader;
        self.nextIndex = vec![self.log.last_index() + 1; peer_count];
        self.matchIndex = vec![0; peer_count];
        // 提交 no-op 条目确保 Leader Completeness
        self.append_entry(noop_command());
        // 立即发送心跳
        send_append_entries_to_all();
    }
}

十、性能优化与生产实践

  • 批处理(Batching):将多个客户端请求打包为一个日志条目,减少 RPC 次数
  • 管道化(Pipelining):不等前一次 AppendEntries 返回就发送下一条,充分利用带宽
  • Learner 节点:只接收日志但不参与投票的节点,用于读扩展和新节点追赶
  • CheckQuorum:Leader 定期确认与多数节点的连接,若失去多数则主动降级
  • 集群规模:Raft 集群建议 3、5、7 节点。偶数节点不提供更强的容错能力,徒增延迟

总结

Raft 通过"分解问题"和"日志仅追加"两大设计原则,将复杂的共识问题拆解为选主、日志复制、安全性三个可独立理解和实现的模块。相比 Paxos,Raft 在保持同等容错能力和性能的同时,大幅降低了理解和实现的门槛。理解 Raft 不仅有助于掌握分布式系统核心知识,也是正确使用 etcd、Consul、TiKV 等分布式系统的基础。建议读者结合 Raft 论文 和 etcd raft 模块 源码深入学习。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部