Raft_选举与日志复制流程

导语: 在上一篇中, 我们拆解了 Raft 的核心组件和节点状态. 但静态的结构无法体现分布式系统的魅力, 今天我们要让这些齿轮转动起来. 在 Raft 的世界里, 系统大部分时间都处于"太平盛世"(正常承接请求), 此时的核心任务是日志复制; 而一旦"国王驾崩"(Leader 宕机), 系统就会进入危机时刻, 触发领导者选举. 本文将带你穿梭于这两种状态之间, 并在微观层面透视节点状态参数的每一次跳动.


一、 正常承接请求:日志复制的流转艺术

当集群稳立了一个 Leader 后, 所有的目光都聚焦在如何高效、安全地把客户端的指令同步给整个集群.

1. 日志复制的全流程与状态演变

我们以客户端发送一条 SET X 10 的指令为例, 来看看底层的状态参数是如何翻涌的.

sequenceDiagram participant C as 客户端 (Client) participant L as Leader节点 participant F as Follower节点 (多数派) C->>L: 1. 发起写请求 (SET X 10) Note over L: [持久化] 将日志追加到本地 log[]<br/>[易失性] 不变 L->>F: 2. 并发发送 AppendEntries RPC Note over F: [持久化] 校验通过, 追加到本地 log[]<br/>[易失性] 不变 F-->>L: 3. 返回成功 ACK Note over L: [Leader易失性] 更新对应节点的 matchIndex 和 nextIndex<br/>[易失性] 计算多数派, 推进 commitIndex Note over L: [易失性] 将日志应用到状态机 (lastApplied = commitIndex) L-->>C: 4. 响应客户端成功 L->>F: 5. 下一次心跳携带最新 commitIndex Note over F: [易失性] 推进 commitIndex, <br/>并将日志应用到状态机 (lastApplied更新)

深入节点状态的改变细节:

  1. 接收与追加: Leader 收到请求, 将其封装为一个 Entry.
    • Leader 状态改变: 将该 Entry 追加到自己的 log[](持久化状态改变, 必须落盘). 此时 commitIndexlastApplied 不变.
  2. 广播 RPC: Leader 向所有 Follower 发送 AppendEntries.
    • Leader 状态改变: 读取自身的 nextIndex[] 来决定发给 Follower 哪一条日志.
  3. Follower 接收与落盘: Follower 收到日志, 进行一致性校验(Log Matching, 后文详解).
    • Follower 状态改变: 校验通过后, 将日志追加到自己的 log[](持久化状态改变), 并向 Leader 返回 ACK.
  4. Leader 统计与提交: Leader 收到多数派 ACK.
    • Leader 状态改变:
      • 更新这几个 Follower 对应的 matchIndex[](已复制的最高索引)和 nextIndex[] (Leader独有易失性状态改变).
      • 发现多数派的 matchIndex 已经达到当前日志索引, 于是推进自己的 commitIndex (易失性状态改变).
  5. Leader 应用与响应:
    • Leader 状态改变: 发现 commitIndex > lastApplied, 于是把日志应用到业务层(状态机), 并将 lastApplied 推进到与 commitIndex 对齐 (易失性状态改变). 最后回复客户端.
  6. 异步通知 Follower: 在下一次心跳或日志复制中, Leader 会带上新的 commitIndex.
    • Follower 状态改变: Follower 发现 Leader 的提交进度超前了, 于是推进自己的 commitIndexlastApplied, 应用状态机 (易失性状态改变).

2. Log 文件组织结构

Raft 的日志并不是简单的文本流, 它在磁盘上通常被组织为一个结构化的数组. 每一条 Log Entry 必须包含三个核心元素:

  • Index(索引): 该日志在整个日志序列中的单调递增编号.
  • Term(任期): 创建该条日志时, 当时 Leader 的任期号. (这是解决后续所有冲突的终极比对维度)
  • Command(指令): 真正要应用到状态机的业务操作(如 SET X 10).

3. 提交(Commit)的严谨定义

在 Raft 中, "提交"是一个极其神圣的词. 定义: 当一条日志被 Leader 安全地复制到了集群的大多数(Majority, N/2+1)*节点上, 并且该日志是*当前任期由该 Leader 创建的, 这条日志就被定义为"已提交(Committed)". 意义: 一旦日志被提交, Raft 保证它永远不会被覆盖或丢失, 即使发生无数次极端宕机, 它最终也一定会出现在所有节点的状态机中.

4. Log Matching 特性(绝对的连续性保证)

Raft 是如何保证 Follower 不会瞎写日志的?这归功于 Log Matching(日志匹配特性):

  • 如果不同节点上的两条日志拥有相同的 Index 和 Term, 那么它们存储的 Command 一定相同.
  • 更狠的保证: 如果不同节点上的两条日志拥有相同的 Index 和 Term, 那么它们之前所有的日志都完全一模一样.

如何实现? Leader 每次发 AppendEntries 时, 都会带上前一条日志的 prevLogIndexprevLogTerm. Follower 收到后, 会先去自己的日志库里查:

  • "我本地有 prevLogIndex 这个位置的日志吗?"
  • "如果有, 它的 Term 和 Leader 传过来的 prevLogTerm 一样吗?" 只要有一项对不上, Follower 直接拒绝这次追加!这就迫使 Leader 必须往回倒退找寻分歧点, 强制用 Leader 的日志覆盖 Follower, 保证了日志的绝对对齐.

二、 选举:危机时刻的权力交接

当系统风平浪静时, 大家都是 Follower. 但只要 Leader 的心跳停止跳动, 系统就会进入选举倒计时.

1. 触发选举的条件与场景

  • 场景: Leader 机器掉电断网、Leader 所在的机房发生网络分区(Network Partition)、或者启动初始化时.
  • 触发机制: 每个 Follower 内部都有一个 Election Timer(选举超时定时器). 只要收到 Leader 的心跳, 定时器就会被重置. 一旦定时器走完(超时), 该节点就认为 Leader 已死, 拉开选举大幕.

2. 选举过程与微观状态改变

  1. 揭竿而起(Follower -> Candidate):
    • 状态改变:
      • 节点身份变为 Candidate.
      • 自身的 currentTerm 增加 1 (持久化状态改变).
      • 给自己投一票, 即 votedFor 设为自己的 ID (持久化状态改变).
      • 重置选举定时器.
  2. 四处拉票: Candidate 向其他所有节点广播 RequestVote RPC.
    • 日志发挥的决定性作用(Leader Completeness 保证): Candidate 在拉票时, 必须在 RPC 中带上自己最后一条日志的 lastLogIndexlastLogTerm.
    • 其他节点收到后, 会对比自己的日志. **规则是:只有当 Candidate 的日志比自己"新", 或者一样"新"时, 才会把票投给它. **(判断标准:Term 更大的更新; Term 相同则 Index 更大的更新). 这就保证了没同步完数据的落后节点, 绝对当不了 Leader.
  3. 投票处理:
    • 被拉票节点的状态改变: 如果发现 Candidate 日志够新, 且自己在当前 Term 还没投过票, 就会投赞成票, 并将 votedFor 设为该 Candidate 的 ID, 同时更新自己的 currentTerm (持久化状态改变).

3. 获胜的判断条件

选举有三种可能的结果:

  • 赢了: 获得了集群中多数派(N/2+1)的选票. 立刻晋升为 Leader, 开始发送心跳镇压其他 Candidate, 并初始化自己的 nextIndex[]matchIndex[].
  • 输了: 收到了别人发来的合法 Leader 心跳(且对方 Term >= 自己的 Term). 立刻退化为 Follower.
  • 平局(Split Vote): 没人获得多数票. 等待定时器再次超时, currentTerm 再加 1, 开启下一轮.

4. 避免无限循环的投票分裂:随机选举超时

如果多个节点同时超时, 同时变成 Candidate, 它们可能会把票平分, 导致谁也选不上, 下一轮又同时发起, 陷入死循环. **Raft 的解法极度优雅:随机化. ** Raft 规定每个节点的 Election Timeout 是一个随机区间(通常是 150ms ~ 300ms). 这就像赛跑时故意让大家的起跑线错开. 总有一个节点会最先超时并拉票, 等其他节点还没醒过神来, 它就已经拿到多数票当选了.


三、 系统异常场景:解决日志不一致的终极博弈

Leader 宕机重选虽然解决了可用性问题, 但会遗留一个巨大的历史包袱:日志不一致.

1. 系统异常导致日志不一致的场景

设想这样一种混乱局面:一个节点当了 Leader, 收了日志还没同步就挂了; 另一个节点当了 Leader, 同步了一半又挂了. 经过几轮折腾, 集群的日志会变成什么样?

你可以看看下面这张图(这也是 Raft 论文中最著名的 Figure 7 场景再现):

========================================================================
场景:新任 Leader (A) 上任时, 面对一群日志千奇百怪的 Follower
------------------------------------------------------------------------
Index:         1      2      3      4      5      6      7      8      9      10
------------------------------------------------------------------------
Leader A:    [T1] - [T1] - [T1] - [T4] - [T4] - [T5] - [T5] - [T6] - [T6] - [T6] 
(真理源头)

Follower B:  [T1] - [T1] - [T1] - [T4] - [T4] - [T5] - [T5] - [T6] - [T6]
(缺失日志:网速慢, 落后了一截)

Follower C:  [T1] - [T1] - [T1] - [T4] 
(严重缺失:很久以前就断网了)

Follower D:  [T1] - [T1] - [T1] - [T4] - [T4] - [T5] - [T5] - [T6] - [T6] - [T6] - [T6]
(存在未提交的废弃日志:曾经是 T6 的 Leader, 多写了一条还没提交就挂了)

Follower E:  [T1] - [T1] - [T1] - [T4] - [T4] - [T4] - [T4]
(存在严重冲突的脏日志:曾经是 T4 的 Leader, 断网期间自己傻傻收了请求, 其实都是废数据)
========================================================================
注: [Tx] 代表该日志条目是在任期 x 生成的. 

2. 独裁统治:如何解决日志不一致?

在 Raft 中, 解决冲突的原则只有一条:Leader 永远是对的(Leader Append-Only 特性). 任何与 Leader 日志冲突的 Follower 日志, 都会被无情地截断和覆盖.

具体抹平冲突的流程如下:

  1. 上任初始化: Leader A 刚当选时, 先把所有 Follower 的 nextIndex 都设为自己最新一条日志的后一个位置(即 Index=11).
  2. 发生拒绝: Leader 发送带心跳的 AppendEntries(prevLogIndex=10, prevLogTerm=T6) 给 Follower E.
  3. Log Matching 发挥威力: Follower E 发现自己 Index=10 的位置根本没日志, 或者 Term 对不上, 直接拒绝.
  4. 回退摸底: Leader 收到拒绝, 毫不气馁, 把发给 E 的 nextIndex 减 1(变成 10), 再发 prevLogIndex=9的探测包.
  5. 找到共识锚点: 就这样一路被拒绝一路回退, Leader 发送 prevLogIndex=4, prevLogTerm=T4 时, Follower E 发现:"哎?这个我本地有, 对上了!" 于是返回 ACK.
  6. 无情覆盖: Leader 找到了最后的一致点(Index=4). 在下一次 RPC 中, Leader 会把 Index 5 到 10 的所有正确日志一起打包发过去. Follower E 会把自己 Index 5 之后的那些脏日志全部删除(截断), 老老实实地写入 Leader 传来的新日志.

最终结果: 不管历史遗留问题有多复杂, 随着 Leader nextIndex 的不断回朔, 所有节点的日志最终都会被"强制刷机", 变得和当前 Leader 绝对一致. 这就是共识算法在混沌中重塑秩序的暴力美学!

posted @ 2026-04-22 19:13  虾野百鹤  阅读(3)  评论(0)    收藏  举报