彻底搞懂 Raft 算法:为“被理解”而生的分布式共识

彻底搞懂 Raft 算法:为“被理解”而生的分布式共识

在分布式系统的世界里,共识(Consensus)是一个核心难题:如何让一堆机器对某个值(比如数据记录)达成一致?

很久以来,Paxos 算法是这个领域的“神”,但它太难懂了,难懂到连工程师都难以正确实现它。于是,Raft 诞生了。Raft 的设计初衷非常直白——“为了被理解”。它将复杂的共识问题拆解得清晰明了。

要理解 Raft,其实只需要搞懂三个核心问题:

  1. 老大是怎么选出来的?(Leader Election)
  2. 数据是怎么同步的?(Log Replication)
  3. 出了问题怎么办?(Safety & Network Partition)

本文将通过图解的方式,带你拆解 Raft 的核心原理。


一、 核心概念:三个角色与任期

在 Raft 集群中,任何时候,一个节点只能处于以下三种状态之一。我们可以把它想象成一个由群众、竞选者和独裁者组成的微型社会。

1. 三种角色 (Roles)

  • Leader(领导者/独裁者)
    • 特权:全权负责处理客户端的写请求,把日志复制给 Follower。
    • 义务:它必须不断给 Follower 发心跳(Heartbeat),证明自己还活着,否则会被“罢免”。
  • Follower(追随者/群众)
    • 本分:平时不说话,只响应 Leader 的指令(写数据)和 Candidate 的拉票请求。
    • 造反:如果在一段时间内没收到 Leader 的心跳,它就会认为 Leader 挂了,从而变身 Candidate。
  • Candidate(候选人/竞选者)
    • 目的:当 Follower 没听到 Leader 的声音,就会变成 Candidate,发起投票,试图成为新 Leader。

2. 任期 (Term)

Raft 将时间划分为一个个的 Term(任期),用递增的数字表示(Term 1, Term 2...)。

  • 每一届任期开始都是一次选举。
  • 规则:如果一个节点发现别人的 Term 比自己大,它会立马变回 Follower(认怂)。这就像前朝皇帝遇到了新朝皇帝。

角色状态转换图

stateDiagram-v2 [*] --> Follower Follower --> Candidate : 选举超时 (收不到心跳) Candidate --> Leader : 获得大多数选票 Candidate --> Follower : 发现更高任期 / 输掉选举 Candidate --> Candidate : 选举超时 (平票,重新选举) Leader --> Follower : 发现更高任期 (比如网络恢复后)

二、 阶段一:Leader 选举 (Leader Election)

Raft 使用心跳机制来触发选举。只要 Leader 还在发心跳,Follower 就乖乖听话;心跳一停,天下大乱。

1. 触发选举

Follower 节点有一个选举超时定时器(Election Timeout)(通常在 150ms-300ms 之间随机)。

  • 正常情况:收到 Leader 心跳 -> 重置定时器 -> 继续当 Follower。
  • 异常情况:定时器倒计时结束(超时) -> 变身 Candidate

2. 竞选流程

当一个节点变成 Candidate 后,它会执行以下动作:

  1. 任期 +1(Term ++)。
  2. 给自己投一票
  3. 群发 RequestVote RPC:向所有其他人喊话“请投我一票!”。

3. 谁能当选?

其他节点收到拉票请求,会根据以下原则投票:

  • 先来后到:一个任期内,票只有一张,先给谁就是谁的。
  • 比我新:你的日志不能比我的旧。

结果判定

  • 赢了:获得了集群大多数(N/2 + 1)的票数 -> 成为 Leader -> 立即发送心跳宣告主权。
  • 输了:收到了别人的心跳(说明别人赢了) -> 变回 Follower。
  • 僵局(Split Vote):票数对半开,谁都没过半。

4. 巧妙的“随机超时”

为了解决僵局,Raft 让每个节点的超时时间是一个随机值。这保证了大概率有一个节点会“先醒来”并发起投票,从而打破平衡。

选举流程图

sequenceDiagram participant A as Node A (Follower) participant B as Node B (Follower) participant C as Node C (Leader - 已挂) Note over C: Leader C 宕机 Note over A: 等待心跳... 超时! Note over A: 状态变为 Candidate<br/>Term = Term + 1<br/>给自己投一票 A->>B: RequestVote (请投我!) Note over B: 还没投过票<br/>日志也并不比A新 B-->>A: VoteGranted (同意!) Note over A: 拿到 2 票 (总共3节点)<br/>满足大多数! Note over A: 状态变为 Leader A->>B: Heartbeat (我是老大了)

三、 阶段二:日志复制 (Log Replication)

Leader 选出来了,现在开始干活。Raft 保证数据的强一致性

流程步骤

  1. Client 请求:客户端发送命令 set x=5 给 Leader。
  2. 预写入 (Append):Leader 把命令写入本地日志(状态:Uncommitted),并并行发送 AppendEntries RPC 给 Follower。
  3. Follower 写入:Follower 收到日志,检查一致性。如果没问题,写入本地日志,返回 Success
  4. 提交 (Commit)
    • Leader 收到大多数 Follower 的 Success。
    • Leader 将日志标记为 Committed,应用到状态机(执行 x=5)。
    • Leader 给客户端返回“成功”。
  5. 后续同步:Leader 在下一次心跳中告诉 Follower:“刚才那条已提交,你们也可以应用了。”

日志复制流程图

sequenceDiagram participant Client participant Leader participant Follower1 participant Follower2 Client->>Leader: set x=5 Note over Leader: 1. 写入本地日志 (Uncommitted) par 并行发送 Leader->>Follower1: AppendEntries (x=5) Leader->>Follower2: AppendEntries (x=5) end Note over Follower1: 2. 写入本地日志 Follower1-->>Leader: Success Note over Follower2: 2. 写入本地日志 Follower2-->>Leader: Success Note over Leader: 3. 收到大多数响应 (2/3) Note over Leader: 4. Commit 并应用状态机 Leader-->>Client: 返回成功 OK par 通知提交 Leader->>Follower1: 下次心跳: "x=5 已提交" Leader->>Follower2: 下次心跳: "x=5 已提交" end Note over Follower1: Commit 并应用 Note over Follower2: Commit 并应用

四、 阶段三:安全性与脑裂 (Brain Split)

分布式系统最怕网络分区(Network Partition)。假设 5 个节点(A, B, C, D, E)裂变成了两半:[A, B][C, D, E]

4.1.场景演练

  1. 旧 Leader (A) 的困境
    • A 在 [A, B] 分区。Client 给它发数据,它试图同步给 B。
    • 但它永远凑不齐 3 票(总共5个节点,需要3票)。
    • 结果:A 收下的数据永远是 Uncommitted无法响应客户端成功
  2. 新 Leader (C) 的崛起
    • [C, D, E] 分区,C 发现联系不上 A,发起选举。
    • C 拿到 C, D, E 的 3 票,成为新 Leader(Term 增加)。
    • Client 连接到 C,写入数据。C 能同步给 D 和 E,成功提交
  3. 网络恢复(真相大白)
    • 分区消失。A (Term 1) 遇到了 C (Term 2)。
    • 规则:Term 小的自动退位。
    • 结果:A 发现自己任期旧,立马变为 Follower。它会清空自己那些未提交的“脏数据”,同步 C 的最新数据。

4.2.如果分裂了两边都是都是三个刚好一半怎么办?

这种情况(比如总共 6 个节点,裂变成了 3 对 3),正是分布式系统中为什么要强烈建议部署奇数个节点(3, 5, 7...)的原因。

简单直接的答案是:整个系统会“停摆”(不可用)。两边都无法工作,谁也当不了老大。

Raft 为了保证数据绝不出错(Safety),宁可让系统停止服务(牺牲 Availability),也不会允许出现两个无法达成大多数共识的分区。

下面详细拆解这个“僵局”:

4.2.1、 数学规则:大多数 (Majority) 是多少?

Raft 的核心铁律是:必须要拿到“大多数”票数才能干活。
公式是:大多数 = floor(总节点数 / 2) + 1

  • 如果是 5 个节点:大多数 = 5/2 + 1 = 3 票。
  • 如果是 6 个节点:大多数 = 6/2 + 1 = 4 票。

4.2.2、 场景演练:3 对 3 的绝望死锁

假设集群有 6 个节点(A, B, C, D, E, F),Leader 是 A。
网络发生了分割,切成了两半:[A, B, C][D, E, F]

1. 左边分区 [A, B, C](包含老 Leader A)
  • 现状:Leader A 依然活着,试图处理请求。
  • 写数据:A 收到客户端写请求,发送给 B 和 C。
  • 结果:A 最多能收到 A, B, C 三个节点的确认。
  • 判定3 票 < 4 票(大多数)
  • 结局:Leader A 无法提交任何日志。客户端会一直等待或超时。左边瘫痪。
2. 右边分区 [D, E, F](没有 Leader)
  • 现状:D, E, F 收不到 A 的心跳,超时发起选举。
  • 选举:假设 D 发起竞选,它向 E 和 F 拉票。
  • 结果:D 最多能拿到 D, E, F 三张票。
  • 判定3 票 < 4 票(大多数)
  • 结局:D 永远选不上 Leader。哪怕无限重试,只要网络不好,它永远凑不齐 4 票。右边也瘫痪。

4.2.3、 为什么这反而是“好事”?

虽然系统不可用了(Downtime),但 Raft 成功避免了脑裂(Brain Split)导致的数据不一致。

  • 如果是 Paxos 或 Raft,这种情况下没有产生双主(左边是不能提交的废主,右边选不出新主)。
  • 数据没有冲突,一旦网络恢复,大家重新凑在一起,就能立刻恢复正常。

4.2.4、 最佳实践:为什么总是个数(Odd Number)?

这就解释了为什么在生产环境中,Etcd、Zookeeper、Consul 等组件的节点数永远建议是 3 个、5 个或 7 个,而极少见到 4 个或 6 个。

偶数部署的坏处(比如 4 个节点):

  1. 容错率没变
    • 3 个节点能容忍 1 个挂掉(剩 2 个,满足大多数 2)。
    • 4 个节点也只能容忍 1 个挂掉(剩 3 个,满足大多数 3)。如果有 2 个挂了,剩 2 个 < 3,系统就挂了。
    • 加了一台机器,容错能力竟然没提升!
  2. 脑裂风险增加
    • 偶数节点容易出现 50:50 的网络分区,导致像上面那样的全系统瘫痪。

小结

如果分裂成了两边刚好各一半:

  1. 写操作全部失败
  2. 选不出新 Leader
  3. 系统进入不可用状态,直到网络恢复。

这也是为什么架构师在设计 Raft 集群时,一定会告诉你:“请凑个单数!”


五、 总结 Raft 的精髓

Raft 之所以能取代 Paxos 成为工业界的主流(Etcd, Consul, TiKV 都在用),就在于它清晰的规则:

  1. 强主模型 (Strong Leader):所有写操作必须经过 Leader,简化了逻辑。
  2. 大多数原则 (Quorum):只要大多数节点活着,系统就能跑,数据就不会丢。
  3. 随机超时 (Randomized Timeout):用最简单的技巧解决了复杂的选举僵局。
  4. 任期逻辑 (Terms):用“朝代”的概念解决了时序和冲突问题。

一句话总结 Raft:

选出一个管事的,所有事情听他的,大多数人记下来了就算成功。

posted on 2025-11-27 02:12  滚动的蛋  阅读(0)  评论(0)    收藏  举报

导航