Raft 核心概念解析:领导者选举与日志复制
在分布式系统的世界里,一致性(Consistency) 是基石。多个节点协同工作,对外表现得像一个单一、可靠的系统,这需要它们就操作的顺序和结果达成共识。共识算法(Consensus Algorithm) 正是解决这一问题的关键技术。长久以来,Paxos 算法被视为共识算法的黄金标准,但其复杂性和难以理解性也广为人知,阻碍了其广泛的工程实践。
为了解决 Paxos 的理解和实现难题,斯坦福大学的 Diego Ongaro 和 John Ousterhout 设计了 Raft 算法。Raft 的核心目标是 可理解性(Understandability),它通过将复杂的共识问题分解为几个相对独立且易于理解的子问题来实现这一目标:领导者选举(Leader Election)、日志复制(Log Replication) 和 安全性(Safety)。此外,Raft 还考虑了 集群成员变更(Cluster Membership Changes) 和 日志压缩(Log Compaction) 等实际工程问题。
本文将深入探讨 Raft 算法最核心的两个机制:领导者选举和日志复制,详细解析它们的工作原理、设计考量以及如何共同保证系统的一致性和可用性。
一、 Raft 基础:角色与任期
在深入细节之前,我们首先需要理解 Raft 中的基本概念:
-
节点角色(Node Roles): 在任何时刻,Raft 集群中的每个节点都扮演以下三种角色之一:
- 领导者(Leader): 负责处理所有客户端请求,并将日志条目复制到其他节点。在正常操作期间,集群中同时只有一个 Leader。如果 Leader 宕机或失联,集群会选举新的 Leader。
- 跟随者(Follower): 完全被动。它们不发送任何请求,只响应来自 Leader 或 Candidate 的请求。所有客户端请求都会被重定向到 Leader。
- 候选人(Candidate): 当 Follower 在一段时间内(称为选举超时,Election Timeout)没有收到 Leader 的心跳时,它会转变为 Candidate并发起一次新的选举,试图成为新的 Leader。
-
任期(Term): Raft 将时间划分为任意长度的 任期(Term)。每个任期用一个连续递增的整数(Term Number)来标识。任期在 Raft 中扮演逻辑时钟的角色,用于检测过期的信息(例如,来自旧 Leader 的消息)。
- 每个任期的开始都伴随着一次 选举(Election)。一个或多个 Candidate 会尝试赢得选举成为 Leader。
- 如果一个 Candidate 赢得了选举,它将在该任期的剩余时间内担任 Leader。
- 如果在特定任期内选举未能选出 Leader(例如,发生选票瓜分 Split Vote),该任期将在没有 Leader 的情况下结束,新的任期(和新的选举)将很快开始。
- 任何节点在通信时都会带上当前的任期号。如果一个节点收到一个带有更高任期号的请求或响应,它会立即更新自己的任期号,并(如果当前是 Leader 或 Candidate)转变为 Follower 状态。如果收到的消息任期号较旧,则直接拒绝该消息。
二、 核心机制之一:领导者选举(Leader Election)
Raft 采用一种强领导者模型,所有操作都必须通过 Leader 进行。因此,快速、明确地选举出唯一的 Leader 至关重要。
1. 触发选举:
- 当一个 Follower 在其 选举超时(Election Timeout) 时间内没有收到来自当前 Leader 的有效 心跳(Heartbeat)(一种特殊的
AppendEntries
RPC)或任何其他AppendEntries
RPC 时,它会认为 Leader 可能已经宕机或失联。 - 此时,该 Follower 会 增加 自己的当前任期号(
currentTerm
),将自己的状态从 Follower 转换 为 Candidate。 - 为了避免多个 Follower 同时超时并导致选票瓜分,每个 Follower 的选举超时时间通常在一个固定的基础值上 随机 增加一小段时间(例如,150ms-300ms)。这种随机性大大降低了同时发起选举的可能性。
2. 选举过程:
成为 Candidate 后,节点会立即执行以下操作:
- 投票给自己: Candidate 首先为自己投上一票。
- 发送
RequestVote
RPC: Candidate 向集群中的 所有其他 节点并行发送RequestVote
RPC 请求,请求它们为自己投票。RequestVote
RPC 包含以下关键信息:term
: Candidate 的当前任期号。candidateId
: 请求投票的 Candidate 的 ID。lastLogIndex
: Candidate 本地日志中最后一条日志条目的索引。lastLogTerm
: Candidate 本地日志中最后一条日志条目的任期号。这两个日志信息用于确保只有拥有“最新”日志的 Candidate 才能当选 Leader(这是 Raft 安全性的关键保证之一)。
3. 投票规则与响应:
收到 RequestVote
RPC 的节点(可以是 Follower 或其他 Candidate)会根据以下规则决定是否投票:
- 任期检查:
- 如果接收者的
currentTerm
大于 请求者的term
,则拒绝投票(说明请求者是一个过期的 Candidate)。 - 如果接收者的
currentTerm
小于 请求者的term
,则接收者首先更新自己的currentTerm
为请求者的term
,并转变为 Follower 状态(如果当前是 Leader 或 Candidate)。然后继续进行后续检查。
- 如果接收者的
- 投票限制: 在同一个任期内,每个节点 最多只能投一票(遵循先到先得原则)。如果接收者在本任期内已经投过票给其他 Candidate(通过
votedFor
状态变量记录),则拒绝投票。 - 日志“最新”检查(关键安全保证): 只有当 Candidate 的日志至少和接收者本地日志一样“新”时,接收者才会投票给该 Candidate。Raft 定义“新”的比较规则如下:
- 首先比较最后一条日志条目的 任期号 (
lastLogTerm
)。任期号大的日志更新。 - 如果任期号相同,则比较日志的 索引 (
lastLogIndex
)。索引大的(即更长的日志)更新。 - 只有 当
(candidate.lastLogTerm > receiver.lastLogTerm)
或者(candidate.lastLogTerm == receiver.lastLogTerm && candidate.lastLogIndex >= receiver.lastLogIndex)
时,接收者才会认为 Candidate 的日志足够新,并可能投票给它(如果其他条件也满足)。这个规则确保了新当选的 Leader 一定包含了所有已提交(committed)的日志条目。
- 首先比较最后一条日志条目的 任期号 (
如果以上所有条件都满足,接收者就会向 Candidate 发送一个 同意投票 的响应。否则,发送 拒绝投票 的响应。
4. 选举结果:
Candidate 在发送 RequestVote
RPC 后会等待响应。可能会出现以下三种结果:
- 赢得选举 (Becomes Leader): 如果 Candidate 在当前任期内收到了来自 集群中超过半数(Majority) 节点的投票(包括自己的那一票),它就赢得了选举,成为新的 Leader。
- 成为 Leader 后,它会立即向所有其他节点发送 心跳消息(空的
AppendEntries
RPC),以宣告自己的领导地位,并阻止其他节点因超时而发起新的选举。
- 成为 Leader 后,它会立即向所有其他节点发送 心跳消息(空的
- 选举失败 – 发现新 Leader (Becomes Follower): 如果 Candidate 在等待投票期间,收到了一个来自声称是 Leader 的节点的
AppendEntries
RPC(心跳或日志复制请求),并且该 RPC 中的term
不小于 Candidate 当前的term
,那么 Candidate 承认该 Leader 的合法性。- 如果该 Leader 的
term
大于 Candidate 的term
,Candidate 会更新自己的term
。 - 无论哪种情况,Candidate 都会 转变为 Follower 状态,并遵循新 Leader。
- 如果该 Leader 的
- 选举超时 – 选票瓜分 (Starts New Election): 如果 Candidate 在一个选举超时周期内,既没有赢得选举(未收到多数选票),也没有收到来自合法新 Leader 的消息,那么这次选举就失败了(很可能发生了选票瓜分,即没有 Candidate 获得多数票)。
- Candidate 会 增加 自己的
currentTerm
,然后 重新开始 新一轮的选举过程(回到步骤 1)。由于选举超时的随机性,下一轮选举中发生选票瓜分的概率会降低。
- Candidate 会 增加 自己的
通过这种机制,Raft 保证了在任何一个任期内,最多只有一个 Leader 被选出(Election Safety),并且选出的 Leader 拥有所有已提交的日志条目(Leader Completeness 的一部分)。
三、 核心机制之二:日志复制(Log Replication)
一旦 Leader 被选举出来,它就开始负责处理客户端请求并将操作序列(表示为日志条目)复制到所有 Follower 节点,以保证系统状态的一致性。
1. 日志条目(Log Entry):
每个日志条目包含:
- 命令(Command): 需要被状态机执行的操作(例如,
SET x = 5
)。 - 任期号(Term Number): 创建该日志条目的 Leader 的任期号。
- 索引(Index): 日志条目在日志中的位置(从 1 开始单调递增)。
2. 复制过程:
- 接收请求: 客户端将请求发送给 Leader。
- 本地追加: Leader 接收到请求后,首先将该命令和当前的任期号打包成一个新的日志条目,并将其 追加到自己的本地日志 末尾。
-
并行复制: Leader 随后向集群中的 所有 Follower 节点 并行 发送
AppendEntries
RPC,要求它们复制这个(或一批)新的日志条目。AppendEntries
RPC 包含以下关键信息:term
: Leader 的当前任期号。leaderId
: Leader 自己的 ID。prevLogIndex
: 紧邻在新条目之前的那个日志条目的索引。prevLogTerm
:prevLogIndex
处日志条目的任期号。这两个字段用于进行 一致性检查。entries[]
: 需要被复制的日志条目列表(可以为空,此时作为心跳)。leaderCommit
: Leader 已知的、已经被提交的最高日志条目的索引(commitIndex
)。
-
Follower 处理
AppendEntries
RPC:- 任期检查: Follower 首先检查 RPC 中的
term
。如果term < currentTerm
,则拒绝该 RPC(来自过期的 Leader)。如果term > currentTerm
,Follower 更新currentTerm
并转为 Follower 状态(如果它之前是 Candidate)。如果term == currentTerm
,继续处理。 - 一致性检查(关键): Follower 检查其本地日志在
prevLogIndex
位置上的条目是否存在,并且任期号是否与 RPC 中的prevLogTerm
匹配。- 匹配: 如果匹配,说明 Follower 的日志在该点之前与 Leader 保持一致。Follower 会:
- 删除本地日志中从
prevLogIndex + 1
开始的所有(可能存在的)不一致的条目。 - 将 RPC 中
entries[]
里的新条目追加到本地日志末尾。 - 向 Leader 回复 成功。
- 删除本地日志中从
- 不匹配: 如果不匹配(例如,缺少
prevLogIndex
处的条目,或者该位置的任期号不同),说明 Follower 的日志与 Leader 在此点之后不一致。Follower 会向 Leader 回复 失败。
- 匹配: 如果匹配,说明 Follower 的日志在该点之前与 Leader 保持一致。Follower 会:
- 任期检查: Follower 首先检查 RPC 中的
-
Leader 处理响应:
- 成功响应: 如果 Leader 收到 Follower 的成功响应,它会更新该 Follower 的
nextIndex
(下一个要发送给该 Follower 的日志条目的索引)和matchIndex
(已知已与 Leader 匹配的最高日志条目的索引)。 - 失败响应: 如果 Leader 收到 Follower 的失败响应(由于一致性检查失败),Leader 会 递减 该 Follower 对应的
nextIndex
,并在下一次重试时发送包含更早日志条目的AppendEntries
RPC。这个过程会一直持续,直到找到 Leader 和 Follower 日志匹配的点(即prevLogIndex
和prevLogTerm
能够匹配上),然后才能开始追加新的日志条目。这个机制确保了 日志匹配特性(Log Matching Property):如果两个节点的日志在某个索引位置具有相同的任期号,那么它们在该索引之前的所有日志条目也都完全相同。
- 成功响应: 如果 Leader 收到 Follower 的成功响应,它会更新该 Follower 的
3. 日志提交(Committing Entries):
仅仅将日志条目复制到大多数节点还不够,还需要一个明确的 提交(Commit) 过程,表示该条目是安全的,可以被应用到状态机。
- Leader 提交: 当 Leader 发现某个日志条目(假设索引为 N)已经被 成功复制到集群中的大多数(Majority) 节点(即,对于大多数 Follower,
matchIndex >= N
),并且该条目自身的任期号log[N].term
等于 Leader 当前的任期号currentTerm
时,Leader 就认为该条目是 已提交(Committed) 的。- 注意: Raft 的规则是 Leader 只能提交自己当前任期内的日志条目。对于之前任期的日志条目,它们只有在当前任期的一个条目被成功提交后,才能被间接(或隐含地)提交。这是为了处理 Leader 变更时可能出现的复杂情况,保证安全性。
- Leader 会维护一个
commitIndex
变量,表示当前已提交的最高日志条目的索引。Leader 会将commitIndex
更新为满足上述条件的最高索引 N。
- 应用到状态机: 一旦日志条目被提交,Leader 就可以安全地将该条目中的命令 应用(Apply) 到自己的状态机,并计算出结果。
- 通知 Follower 提交: Leader 在后续的
AppendEntries
RPC(包括心跳)中,会携带当前的leaderCommit
值,告知所有 Follower 当前集群已经提交到的日志索引。 - Follower 提交与应用: Follower 收到
AppendEntries
RPC 后,会比较 RPC 中的leaderCommit
和自己本地的commitIndex
。它会将自己的commitIndex
更新为min(leaderCommit, index of last new entry)
(index of last new entry 指的是本次 RPC 成功追加的最后一条日志的索引)。然后,Follower 会将所有索引小于等于其commitIndex
且尚未应用的日志条目,按顺序应用到自己的状态机。
通过这个日志复制和提交流程,Raft 确保了:
- 日志匹配特性(Log Matching Property): 如前所述,保证了节点间日志的一致性基础。
- 领导者只追加原则(Leader Append-Only): Leader 永远不会覆盖或删除自己的日志条目,只会追加。
- 状态机安全(State Machine Safety): 如果一个节点已经将某个日志条目应用到其状态机,那么其他任何节点在相同索引位置上,都不会应用一个不同的日志条目。这是因为只有已提交的条目才能被应用,而提交需要多数节点的确认,并且日志匹配特性保证了已提交条目的一致性。
4. 心跳(Heartbeat):
为了维持领导地位并让 Follower 知道 Leader 仍然存活,Leader 会周期性地(通常比选举超时短得多)向所有 Follower 发送 空的 AppendEntries
RPC(即 entries[]
列表为空)。这些 RPC 就是 心跳。Follower 收到心跳后会重置自己的选举计时器,防止它们发起不必要的选举。心跳消息也顺带携带了最新的 leaderCommit
信息。
四、 领导者选举与日志复制的协同工作
领导者选举和日志复制并非孤立存在,它们紧密协同,共同构成了 Raft 共识机制的核心:
- 选举保证日志的连续性: 选举规则中“日志最新”的检查(比较
lastLogTerm
和lastLogIndex
)是至关重要的安全措施。它确保了只有拥有包含所有已提交日志条目的 Candidate 才能当选为新的 Leader。这被称为 领导者完整性特性(Leader Completeness Property)。当选的新 Leader 的日志可能比某些 Follower 的日志更长,或者包含了不同(但未提交)的条目,但它一定包含了所有之前任期被提交的条目。 - 新 Leader 强制日志一致性: 新 Leader 当选后,它并不依赖于 Follower 本地可能存在的、来自旧 Leader 的未提交日志。相反,它会通过
AppendEntries
RPC 的一致性检查机制,强制所有 Follower 的日志与自己的日志保持一致。如果 Follower 的日志在某个点与 Leader 不匹配,Leader 会逐步回溯nextIndex
,找到匹配点,然后用 Leader 的日志覆盖 Follower 在该点之后的所有条目。这确保了即使在 Leader 切换后,集群的日志也能快速恢复到一致状态。 - 任期号的作用: 任期号是区分不同领导周期、识别过期信息、保证选举安全和日志复制正确性的关键。任何节点遇到比自己当前任期更高的任期号时,都会无条件更新任期并转为 Follower,这有助于快速终结旧任期的活动,并使集群向新的 Leader 或选举靠拢。
五、 总结
Raft 通过将共识问题分解为领导者选举和日志复制两个核心子问题,并精心设计了它们之间的交互和安全规则,实现了比 Paxos 更易于理解和实现的共识算法。
- 领导者选举 通过随机超时、任期、投票规则(特别是日志最新检查)确保了在任何时刻最多只有一个 Leader,并且新 Leader 拥有所有已提交的日志。
- 日志复制 通过
AppendEntries
RPC、一致性检查、多数派确认提交以及commitIndex
的传递,确保了所有节点最终拥有相同的、按顺序排列的已提交日志序列,并能安全地将这些日志应用到状态机,从而达成状态的一致性。
这两个机制,辅以任期概念和明确的节点角色转换,共同构成了 Raft 算法稳定、可靠、易于理解的基础。正是这种清晰的设计哲学,使得 Raft 在工业界得到了广泛的应用,成为构建高可用、强一致性分布式系统的重要工具。理解了领导者选举和日志复制的细节,也就掌握了 Raft 算法的精髓。