Raft 剖析(二): 基础概念、核心组件与状态流转

导语: 在上一篇《Raft 引言》中, 我们站在上帝视角俯瞰了复制式状态机(RSM)和集群交互的宏观轮廓. 今天, 我们将视线拉近, 正式拆解 Raft 算法的引擎盖. 如果你打算自己动手写一个分布式的 KV 存储, 或者去阅读 etcd/TiKV 的源码, 本篇涉及的所有概念、状态和循环机制, 将是你绕不开的基本功.


一、 基本概念: Raft 世界的时空法则

在深入节点状态之前, 我们必须先统一 Raft 世界中的几个核心度量衡:

  1. Term(任期): Raft 将时间划分为一个个连续的任期(Term), 它充当了 Raft 世界里的逻辑时钟. 每一个任期都由一次选举(Election)开始. 你可以把它理解为现实世界中的"总统任期". 如果发现别人的 Term 比自己大, 节点会立刻"认怂"并更新自己的 Term.
  2. Log Entry(日志条目): 每一次写入请求都会被封装成一个 Log Entry. 它不仅包含具体的业务指令(如 SET X 10), 还必须带有生成该指令时的 Leader 的 Term 和它在日志序列中的 Index.
  3. Index(日志索引): 一个单调递增的整数, 代表日志条目在连续日志流中的位置.

二、 节点状态:三态流转

在 Raft 集群中, 任何时刻一个节点只能处于以下三种状态之一:

  • Follower(跟随者): 所有节点的初始状态. 它是被动的, 只会响应 Leader 和 Candidate 的请求. 如果它长时间没收到 Leader 的心跳, 就会认为 Leader 挂了, 自己变成 Candidate.
  • Candidate(候选人): 过渡状态. Follower 揭竿而起准备竞选 Leader 时的状态. 它会向全网拉票, 如果获得多数派选票, 就晋升为 Leader.
  • Leader(领导者): 集群的霸主. 负责处理所有客户端请求(读写), 并向所有 Follower 复制日志、发送心跳以维持统治.

状态转换场景与触发条件:

  • Follower -> Candidate: 选举超时(Election Timeout)未收到 Leader 的心跳.
  • Candidate -> Candidate: 竞选出现平局(Split Vote), 等待随机超时后开启下一轮(Term + 1)竞选.
  • Candidate -> Leader: 成功获得了集群中多数派(N/2+1)的选票.
  • Candidate -> Follower: 在竞选中途, 收到了具有相同或更大 Term 的合法 Leader 传来的心跳.
  • Leader -> Follower: 遇到了"前朝老兵"或者网络分区恢复, 发现集群中出现了具有更大 Term 的新 Leader, 立刻退位让贤.

三、 实现一个 Raft 算法需要哪些核心组件?

从工程落地的角度来看, 手写一个 Raft 并不只是写几个 if-else, 你需要构建四个核心子系统:

  1. RPC 网络模块(Transport): 负责节点间的高效异步通信, 必须支持重试和超时机制.
  2. 定时器驱动(Ticker/Timer): Raft 是时间敏感的协议. 你需要两个核心定时器:心跳定时器(Heartbeat Timer)*和*选举超时定时器(Election Timer)(注意:选举超时时间必须加上随机抖动, 防止活锁).
  3. 日志与存储模块(Log & Storage): 也就是预写日志(WAL)的实现. 要求极高的顺序追加写入性能, 且必须保证 fsync 强制落盘.
  4. 状态机(State Machine): 将最终提交的日志应用到业务逻辑层(例如一个内存中的 HashMap).

四、 每个节点存储哪些状态参数, 如何存储?

Raft 对状态的分类极其严谨, 分为持久化(落盘)易失性(内存)两大类. 这是面试高频考点.

1. 所有节点的持久化状态(必须在回复 RPC 前落盘)

  • currentTerm:节点见过的最大任期号(初始为 0).
  • votedFor:当前任期内, 把票投给了哪个候选人(防止一任多投).
  • log[]:日志条目数组, 包含指令及其所属的 Term.

2. 所有节点的易失性状态(重启后重新计算)

  • commitIndex:已知被提交的最高日志索引.
  • lastApplied:已经被应用到本地状态机的最高日志索引.

3. Leader 独有的易失性状态(每次选举后重新初始化)

  • nextIndex[]:对于每一个 Follower, Leader 准备发送给它的下一条日志的索引(初始为 Leader 最后一条日志索引 + 1).
  • matchIndex[]:对于每一个 Follower, 已知已经复制到该 Follower 的最高日志索引(初始为 0).

五、 Raft 的两种核心 RPC 与代码映射

Raft 协议的极致简洁体现在它主要依靠两种 RPC 来打天下. 在工业级实现 etcd 中, Raft 核心被抽象为一个纯异步的状态机, 它没有传统的 RPC 接口定义, 而是通过 Step(m pb.Message) 来推进状态.

1. RequestVote RPC (请求投票)

  • 作用: Candidate 发起, 用于向其他节点拉票.
  • 参数: term, candidateId, lastLogIndex, lastLogTerm.

2. AppendEntries RPC (追加日志/心跳)

  • 作用: Leader 发起, 用于复制日志条目. 当日志条目为空时, 它就充当心跳包.
  • 参数: term, leaderId, prevLogIndex, prevLogTerm, entries[], leaderCommit.

etcd 源码窥探 (etcd/raft/raftpb/raft.pb.go): etcd 将这两种 RPC 统一收敛在了 Protobuf 定义的 Message 结构体中:

Go

// etcd raft 核心消息结构体简化版
type Message struct {
    Type             MessageType // 消息类型, 如 MsgApp(追加), MsgVote(投票)
    To               uint64      // 接收方 ID
    From             uint64      // 发送方 ID
    Term             uint64      // 发送方的 currentTerm
    LogTerm          uint64      // 对应 RequestVote 的 lastLogTerm
    Index            uint64      // 对应 RequestVote 的 lastLogIndex 或 AppendEntries 的 prevLogIndex
    Entries          []Entry     // 需要复制的日志列表
    Commit           uint64      // 对应 AppendEntries 的 leaderCommit
}

六、 Raft 共识层运行的核心循环

Raft 节点本质上是在执行无限的事件循环. 根据当前身份的不同, 处理逻辑截然不同.

1. Follower 处理循环

Follower 是最轻松的角色, 它的工作就是"等".

graph TD A[启动/重置 Election Timer] --> B{等待事件触发} B -->|收到 Leader 的 AppendEntries| C[验证 Term 与日志匹配性] C -->|成功| D[追加本地日志并返回 ACK] C -->|失败| E[拒绝追加并返回失败] D --> A E --> A B -->|收到 Candidate 的 RequestVote| F[验证是否已投票及日志新旧] F -->|允许| G[投赞成票] F -->|拒绝| H[投反对票] G --> A H --> A B -->|Election Timer 超时| I[转变为 Candidate 状态]

2. Candidate 处理循环

Candidate 处于"打仗"状态, 要么自己当选, 要么别人当选, 要么重来.

graph TD A[自增 currentTerm] --> B[给自己投一票] B --> C[重置 Election Timer] C --> D[向所有节点广播 RequestVote RPC] D --> E{等待选举结果} E -->|收到多数派赞成票| F[晋升为 Leader] E -->|收到新 Leader 的 AppendEntries| G[退化为 Follower] E -->|Election Timer 再次超时| A

3. Leader 处理循环

Leader 身上担子最重, 既要安抚臣民(发心跳), 又要开疆拓土(同步日志).

graph TD A[成为 Leader, 初始化 nextIndex/matchIndex] --> B{主循环事件} B -->|心跳定时器触发| C[向所有 Follower 发送空 AppendEntries] B -->|收到客户端写请求| D[将 Entry 写入本地日志] D --> E[异步向 Follower 广播 AppendEntries] E --> F{检查返回的 ACK} F -->|多数派成功| G[推进 commitIndex] G --> H[应用至状态机并响应客户端] F -->|Follower日志不一致| I[递减该节点的 nextIndex 并重试复制]

七、 Raft 五大核心特性:钢铁般的工程保证

Raft 能够在复杂的网络故障中保证数据不丢、不错, 全靠这五大安全特性作为基石.

1. Election Safety(选举安全)

  • 解决了什么问题: 防止集群中同时出现两个 Leader(脑裂, Split-Brain), 避免数据双写导致彻底混乱.
  • 如何实现: 1. 一个任期(Term)内, 一个节点最多只能投出一张票. 2. 必须获得集群多数派(Majority)的选票才能当选. 由于多数派的交集特性, 在同一个 Term 内绝不可能有两个节点同时获得多数票.

2. Leader Append-Only(领导者日志只追加)

  • 解决了什么问题: 确立 Leader 作为唯一的数据源(Source of Truth), 简化了并发冲突解决模型.
  • 如何实现: Leader 永远不会覆盖或删除自己本地的日志条目. 所有的冲突解决, 都是以 Leader 的日志为准, 去强制覆盖 Follower 上的不一致日志.

3. Log Matching(日志匹配)

  • 解决了什么问题: 保证如果在两个节点上的某条日志拥有相同的 Index 和 Term, 那么它们之前的所有日志必然完全一致.
  • 如何实现: 这是通过 AppendEntries 的一致性检查实现的. Leader 每次发送新日志时, 都会带上上一条日志的 prevLogIndexprevLogTerm. Follower 接收时如果发现对不上, 就会拒绝这次复制, 迫使 Leader 往前寻找一致的锚点.

4. Leader Completeness(领导者完整性)

  • 解决了什么问题: 确保已经被"提交(Committed)"的数据永远不会丢失.
  • 如何实现: 这是 Raft 最精妙的设计之一. 在拉票时(RequestVote), Candidate 必须带上自己的 lastLogIndexlastLogTerm. Follower 只有在发现 Candidate 的日志至少和自己一样新时, 才会把票投给它. 因为已提交的日志必然存在于多数派节点中, 这就保证了选出来的新 Leader 必然包含了所有已提交的日志.

5. State Machine Safety(状态机安全)

  • 解决了什么问题: 确保所有节点按照相同的顺序执行相同的指令, 最终达到完全一致的业务状态.
  • 如何实现: 结合了上述所有特性. 一旦某台服务器在某个 Index 处将日志应用到了状态机, 无论发生什么宕机或选举, 其他任何服务器在这个 Index 处应用的指令都必定是相同的. 此外, Raft 附加了一个严苛的限制:Leader 只能通过计算副本来提交当前任期的日志, 不能直接通过计算副本来提交之前任期的日志, 从而彻底封死了幽灵复现的漏洞.

结语: 到这里, 我们已经把 Raft 运行的核心骨架拆解完毕. 从状态的三级跳, 到支撑起共识大厦的两种 RPC 和五大特性, Raft 用工程上极度克制和简洁的设计, 解决了分布式领域最头疼的一致性问题.

但在真实的网络环境中, 事情往往没有这么顺利:如果有 Follower 断网几个月重新连上, 日志落后了十万条怎么办?在下一篇《Raft 进阶》中, 我们将深入探讨日志快照(Snapshotting)集群成员变更(Membership Change)这些更高级的实战话题.

posted @ 2026-04-22 10:08  虾野百鹤  阅读(6)  评论(0)    收藏  举报