图解 Raft 算法:轻松掌握核心流程
在分布式系统的世界里,如何让多个节点达成一致,像一个统一的整体一样工作,是一个核心且复杂的问题。这就是所谓的“共识问题”。想象一下,一个数据库集群、一个分布式锁服务或者一个高可用的配置中心,它们都需要确保所有节点上的数据状态最终是一致的,即使在面临网络分区、节点宕机等异常情况下也能正常工作。Raft 算法就是为了解决这个问题而设计的一种共识算法,它以其出色的可理解性(Understandability)而闻名,相较于其前辈 Paxos,Raft 将复杂的问题分解为几个相对独立的子问题,使得学习和实现都更为容易。
本文将尝试通过“图解”的方式(虽然无法直接展示图片,但会用详尽的文字描述和场景模拟,引导读者在脑海中构建画面),带你一步步深入 Raft 的核心流程,让你轻松掌握这个强大的分布式共识算法。
一、为什么需要 Raft?共识问题的挑战
在单机系统中,决策是简单的,因为只有一个权威。但在分布式系统中,情况变得复杂:
- 节点故障:任何节点都可能随时宕机或失联。
- 网络分区:网络可能分割成多个独立的子网,节点之间无法通信。
- 消息延迟与丢失:网络通信本身就不可靠,消息可能延迟、丢失或重复。
在这样的环境下,要让所有存活的节点对某个值(例如,日志序列中的下一条命令)达成一致,并保证这个决策是最终的、不可推翻的,就需要一个健壮的共识算法。Raft 的目标就是提供这样一个算法,它不仅要正确,还要易于理解和实现。
二、Raft 的核心概念:状态与任期
为了理解 Raft,首先要掌握几个基本概念:
-
服务器状态(Server States): 在 Raft 中,任何时刻,每个服务器节点都处于以下三种状态之一:
- 领导者(Leader): 负责处理所有客户端请求,管理日志复制,并向其他节点发送心跳维持权威。在一个任期内,最多只有一个 Leader。
- 跟随者(Follower): 完全被动。它们响应来自 Leader 和 Candidate 的请求。如果它们在一段时间内(选举超时)没有收到 Leader 的心跳,就会转变为 Candidate并发起选举。
-
候选人(Candidate): 用于选举新的 Leader。当 Follower 启动选举时,会转变为 Candidate 状态。它会向其他节点发送投票请求,如果获得集群中大多数节点的投票,就成为新的 Leader。
-
图解想象:想象一群人开会,Leader 是主持人,负责推进议程(处理请求、复制日志);Followers 是听众,听从主持人安排;当主持人失联(宕机),某个听众觉得等太久了,就站起来说“我来当主持人吧”(成为 Candidate),并向其他人征求同意(拉票)。
-
任期(Term): Raft 将时间划分为一个个连续的“任期”(Term)。每个任期由一个单调递增的整数标识。任期在 Raft 中扮演逻辑时钟的角色,用于检测过期的信息和解决冲突。
- 每个任期的开始都是一次选举(Election)。如果选举成功,一个 Leader 会在该任期的剩余时间里管理集群。
- 如果选举失败(例如,选票被瓜分,没有节点获得多数票),该任期将结束,新的任期将随着新一轮选举的开始而启动。
-
节点在通信时会带上自己的当前任期号。如果一个节点收到来自更高任期的消息,它会立即更新自己的任期并转为 Follower 状态。如果收到来自较低任期的消息,则直接拒绝。
-
图解想象:将每个任期想象成一个朝代。每个朝代开始时可能需要选一个新的皇帝(Leader Election)。皇帝在位期间(一个任期内)发号施令。如果有人拿着旧朝代的圣旨(低任期消息)来,肯定不认。如果有人带来新皇帝登基的消息(高任期消息),大家就得认新皇帝,进入新朝代。
三、Raft 的核心流程:领导者选举(Leader Election)
这是 Raft 算法的起点和基础。当系统启动或现有的 Leader 失效时,就需要选举出新的 Leader。
-
触发选举:
- Follower 维护一个“选举计时器”(Election Timeout),通常是一个随机值(例如 150-300ms),以减少多个 Follower 同时发起选举导致选票被瓜分的概率。
- 如果在计时器超时前没有收到 Leader 的心跳(AppendEntries RPC,即使没有日志也要发)或 Candidate 的投票请求,Follower 就认为 Leader 可能已经失效。
-
成为 Candidate:
- 该 Follower 增加自己的当前任期号(
currentTerm
)。 - 转换为 Candidate 状态。
- 为自己投一票。
- 重置选举计时器。
- 向集群中的所有其他节点发送“请求投票”(RequestVote)RPC。
- 该 Follower 增加自己的当前任期号(
-
投票过程:
- 其他节点(Follower 或 Candidate)收到 RequestVote RPC 后,会根据以下规则决定是否投票:
- 任期检查: 请求者的任期号(
term
)必须不小于接收者的当前任期号(currentTerm
)。如果接收者任期更高,则拒绝投票。如果请求者任期更高,接收者更新自己的任期为请求者的任期,并转为 Follower 状态(即使它之前是 Candidate 或 Leader)。 - 投票限制: 在同一个任期内,每个节点只能投一票(
votedFor
字段记录了投票给谁),遵循先来先服务原则。 - 日志完整性检查 (Raft Safety): 这是关键!为了保证选出的 Leader 拥有所有已提交的日志条目,Candidate 的日志必须至少和接收者“一样新”(up-to-date)。比较规则是:
- 首先比较最后一条日志条目的任期号,任期号大的日志更新。
- 如果任期号相同,则比较日志索引号(长度),索引号大的(日志更长的)日志更新。
- 只有当 Candidate 的日志至少和接收者一样新时,接收者才会投票给它。
- 任期检查: 请求者的任期号(
- 其他节点(Follower 或 Candidate)收到 RequestVote RPC 后,会根据以下规则决定是否投票:
-
选举结果:
- 赢得选举: 如果一个 Candidate 在其当前任期内收到了来自集群中超过半数节点(Majority)的投票,它就赢得了选举,成为新的 Leader。
- 成为 Leader 后,它会立即向所有其他节点发送心跳(空的 AppendEntries RPC),以宣告自己的权威并阻止新的选举发生。
- 选举失败 (Split Vote): 如果多个 Candidate 同时发起选举,可能会导致选票被瓜分,没有任何 Candidate 获得多数票。这种情况下,当前的任期将没有 Leader。
- Candidates 会等待自己的选举计时器超时,然后增加任期号,发起新一轮选举。由于选举超时的随机性,下一次选举中同时发起选举的概率会降低,最终某个 Candidate 能够获得多数票。
- 发现更高任期的 Leader: 如果一个 Candidate 在等待投票期间,收到了来自另一个声称是 Leader(且任期号不低于自己)的 AppendEntries RPC,那么:
- 如果该 Leader 的任期号严格大于自己的当前任期号,Candidate 会承认失败,更新自己的任期,并转为 Follower 状态。
- 如果该 Leader 的任期号等于自己的当前任期号,说明在本轮任期中已经有 Leader 产生了(可能是自己刚赢得选举但消息还没广播出去,也可能是别人赢了),它也会转为 Follower 状态。
- 赢得选举: 如果一个 Candidate 在其当前任期内收到了来自集群中超过半数节点(Majority)的投票,它就赢得了选举,成为新的 Leader。
-
图解想象:
- 节点 A 的计时器先超时,它变成 Candidate (Term 2),向 B, C, D, E 发送投票请求 (携带 Term 2 和自己的日志信息)。
- 节点 B 收到请求,检查 Term (假设 B 当前 Term 1),更新为 Term 2,检查日志 (假设 A 的日志至少和 B 一样新),投票给 A,重置自己的计时器。
- 节点 C 同理,投票给 A。
- 节点 D 可能也几乎同时超时,变成 Candidate (Term 2),向 A, B, C, E 发送投票请求。
- 节点 E 先收到了 A 的请求,投票给 A。现在 A 获得了包括自己在内的 3 票 (A, B, C, E 中的 A, B, E,假设 C 投给了 D 或者还没投),超过了 5 个节点中的半数 (3票)。
- A 成为 Leader (Term 2)。它立刻向 B, C, D, E 发送心跳 (AppendEntries RPC, Term 2)。
- 节点 D 收到 A 的心跳 (Term 2),发现已经有 Leader 了,并且任期相同,于是从 Candidate (Term 2) 转回 Follower (Term 2)。
- 如果发生 Split Vote (比如 A, B 获得 2 票,C, D 获得 2 票,E 宕机),则 A, B, C, D 的选举计时器会超时,它们会增加任期到 Term 3,发起新一轮选举。
四、Raft 的核心流程:日志复制(Log Replication)
一旦选出了 Leader,系统就可以开始处理客户端请求了。这主要通过日志复制机制来完成。
-
客户端请求: 所有客户端请求都发送给 Leader。如果客户端请求发送给了 Follower,Follower 会拒绝请求,并告知客户端当前的 Leader 是谁(如果它知道的话)。
-
Leader 处理请求:
- Leader 收到客户端请求(例如,一个写操作 “SET x = 5″)。
- Leader 将这个命令作为一个新的日志条目(Log Entry)追加到自己的日志序列(Log)末尾。每个日志条目包含:命令本身、创建该条目时的 Leader 任期号、在日志中的索引位置。
-
复制到 Followers:
- Leader 通过并行的 AppendEntries RPC 将新的日志条目发送给所有的 Follower。
- AppendEntries RPC 包含:
term
: Leader 的当前任期号。leaderId
: Leader 自己的 ID。prevLogIndex
: 紧邻新日志条目之前的那个日志条目的索引。prevLogTerm
: 紧邻新日志条目之前的那个日志条目的任期号。entries[]
: 要发送的日志条目(可以是多个,也可以是空的,用于心跳)。leaderCommit
: Leader 已知的、已经被提交的最高日志条目的索引(commitIndex
)。
-
Follower 处理 AppendEntries RPC:
- 任期检查: 同样,如果 RPC 中的
term
小于 Follower 的currentTerm
,拒绝请求。如果term
大于currentTerm
,Follower 更新currentTerm
并转为 Follower(如果它之前是 Candidate)。如果term
等于currentTerm
,继续处理。这时,Follower 知道 Leader 仍然存活,会重置自己的选举计时器。 - 一致性检查 (Crucial!): Follower 会检查收到的 RPC 中的
prevLogIndex
和prevLogTerm
是否与自己本地日志中对应位置的条目匹配。- 如果匹配:说明到
prevLogIndex
为止,Follower 的日志与 Leader 是一致的。Follower 可以安全地追加entries[]
中的新条目。如果新条目与本地已有的条目冲突(相同索引但任期不同),则删除本地该索引及其之后的所有条目,然后追加新条目。 - 如果不匹配:说明 Follower 的日志在
prevLogIndex
处与 Leader 不一致(可能是因为它丢失了条目,或者包含了旧任期的未提交条目)。Follower 会拒绝这个 AppendEntries 请求,并返回失败。
- 如果匹配:说明到
- 任期检查: 同样,如果 RPC 中的
-
处理不一致 (Leader 的应对):
- 当 Leader 收到 Follower 返回的失败响应(因为一致性检查失败)时,它知道该 Follower 的日志落后或有分叉。
- Leader 会递减要发送给该 Follower 的
nextIndex
(Leader 为每个 Follower 维护一个nextIndex
,表示下一个要发送给该 Follower 的日志条目的索引),然后重试 AppendEntries RPC,这次会包含更早的日志条目(即,prevLogIndex
会更小)。 - 这个过程会一直持续,直到找到一个 Leader 和 Follower 日志匹配的点(
prevLogIndex
和prevLogTerm
都匹配)。一旦找到匹配点,Leader 就可以将该点之后的所有日志条目一次性发送给 Follower,使其日志恢复一致。
-
提交日志条目 (Commitment):
- 一个日志条目只有在被 Leader 成功复制到大多数(Majority)节点上之后,才能被认为是“已提交”(Committed)。
- Leader 维护一个
commitIndex
,表示当前已提交的最高日志条目的索引。初始为 0。 - 当 Leader 发现某个索引 N 上的日志条目(必须是其当前任期内创建的条目)已经被复制到了大多数节点(通过 Follower 返回的成功响应来跟踪,Leader 为每个 Follower 维护一个
matchIndex
,表示该 Follower 已成功复制的最高日志索引),并且索引 N >commitIndex
时,Leader 就将commitIndex
更新为 N。 - 注意: Raft 不会直接提交之前任期的日志条目。它通过提交当前任期的条目来间接提交所有之前的条目(因为 Log Matching Property 保证了之前条目的一致性)。
- Leader 在后续的 AppendEntries RPC 中(包括心跳)会将最新的
commitIndex
告知所有 Follower。 - Follower 收到
commitIndex
后,就知道哪些日志条目可以安全地应用到自己的状态机了。Follower 会将所有commitIndex
之前的、尚未应用的日志条目按顺序应用到状态机。
-
应用到状态机 (Apply):
- 一旦一个日志条目被提交,所有节点(包括 Leader 和 Follower)最终都会将该条目对应的命令应用到它们各自的状态机上(例如,执行 “SET x = 5″)。这是状态最终保持一致的关键。
- 每个节点维护一个
lastApplied
索引,表示已应用到状态机的最高日志条目索引。节点会不断检查commitIndex
是否大于lastApplied
,如果是,就将lastApplied + 1
到commitIndex
之间的日志条目按顺序应用。
-
图解想象:
- Client 向 Leader (Term 2) 发送 “SET x=5″。
- Leader 在本地日志索引 3 处追加
[Term 2, "SET x=5"]
。 - Leader 向 Follower B, C, D, E 发送 AppendEntries (prevLogIndex=2, prevLogTerm=1, entry=[Term 2, “SET x=5”], leaderCommit=…)。假设之前索引 2 的条目是 Term 1 时提交的。
- Follower B, C, E 收到 RPC,它们的日志在索引 2 处的 Term 也是 1,一致性检查通过。它们追加条目 3,并返回成功给 Leader。Leader 更新 B, C, E 的
matchIndex
为 3。 - Follower D 可能网络延迟或宕机,没响应或响应失败。
- Leader 发现索引 3 的条目已被复制到 4 个节点 (Leader 自己 + B, C, E),超过了半数 (3个)。并且这个条目是当前 Term 2 的。Leader 将
commitIndex
更新为 3。 - Leader 将命令 “SET x=5” 应用到自己的状态机 (x 变为 5)。
- 在下一次心跳或 AppendEntries RPC 中,Leader 会将
leaderCommit=3
发送给所有 Follower。 - Follower B, C, E 收到
leaderCommit=3
,它们检查自己的lastApplied
(假设是 2),发现commitIndex > lastApplied
,于是将日志索引 3 的命令 “SET x=5” 应用到它们的状态机。现在 B, C, E 的状态也和 Leader 一致了。 - Leader 会继续尝试向 D 发送日志条目 3。如果 D 恢复了,最终也会赶上进度,提交并应用条目 3。
五、Raft 的安全性保证 (Safety)
Raft 的设计包含了几条关键的安全性属性,确保在任何情况下(节点故障、网络分区等,只要不是拜占庭故障)算法都能正确运行:
- 选举安全 (Election Safety): 在一个给定的任期内,最多只能选出一个 Leader。这由投票规则保证(每个节点在同一任期只能投一票,且需要获得多数票才能当选)。
- 领导者只追加原则 (Leader Append-Only): Leader 永远不会覆盖或删除自己日志中的条目,只会追加新条目。
- 日志匹配特性 (Log Matching Property): 如果两个不同节点的日志在某个索引位置具有相同的任期号,那么它们在该索引之前的所有日志条目都是完全相同的。这是通过 AppendEntries 的一致性检查强制实现的。Follower 只有在
prevLogIndex
和prevLogTerm
匹配时才会接受新条目。 - 领导者完整性 (Leader Completeness): 如果一个日志条目在某个任期被提交了,那么它将出现在所有更高任期的 Leader 的日志中。这是通过选举时的日志完整性检查(Candidate 的日志必须至少和投票者一样新)来保证的。这确保了新 Leader 不会丢失任何已提交的日志。
- 状态机安全 (State Machine Safety): 如果一个服务器已经将某个索引位置的日志条目应用到了它的状态机,那么其他服务器在相同索引位置不可能应用一个不同的条目。这是 Leader Completeness 和 Log Matching Property 结合的结果。一旦一个条目被提交(意味着它存在于多数节点的日志中),任何未来的 Leader 都会包含这个条目,并且由于日志匹配,不会在该索引处插入不同的条目。
六、总结:Raft 的魅力
Raft 算法通过将共识问题分解为领导者选举、日志复制和安全性保障这几个相对独立的模块,极大地降低了理解和实现的难度。其核心流程清晰:
- 系统平时由一个 Leader 负责,Followers 被动接收日志和心跳。
- Leader 失效时,Followers 通过超时机制触发选举,选出日志最新的节点成为新 Leader。
- Leader 接收客户端请求,将命令追加为日志条目,并复制到 Followers。
- 只有被复制到大多数节点的日志条目才会被提交,并最终应用到所有节点的状态机,保证一致性。
Raft 的设计哲学——优先保证可理解性——使其在工业界得到了广泛应用,例如 etcd、Consul、CockroachDB 等许多知名的分布式系统都采用了 Raft 或其变种作为核心的共识机制。
通过本文的“图解”描述,希望你对 Raft 的状态转换、选举过程、日志复制的细节、冲突处理方式以及关键的安全性保证有了更深入的理解。掌握了这些核心流程,你就掌握了 Raft 算法的精髓。当然,Raft 还有一些更深入的主题,如集群成员变更、日志压缩(快照)等,但理解了上述核心机制,就为进一步探索这些高级特性打下了坚实的基础。