Raft 分布式一致性协议详解
Raft 是一种为了可理解性而设计的分布式一致性协议,它通过管理复制日志(replicated log)来实现集群节点间状态的一致性,为分布式系统提供强一致性保证和高可用性。本文将深入探讨 Raft 的核心概念,包括节点角色、任期(Term)、领导者选举(Leader Election)、日志复制(Log Replication)以及 Learner 角色的应用。
1. Raft 核心概念
1.1 任期(Term)
Raft 将时间划分为任意长度的任期(Term),用连续的整数表示。每个任期从一次**选举(Election)**开始,一个或多个 Candidate 尝试成为 Leader。如果一个 Candidate 赢得选举,它将在该任期的剩余时间担任 Leader。在某些情况下,选举可能导致选票分裂(split vote),这种情况下该任期将没有 Leader,新的任期会很快开始。任期在 Raft 中扮演逻辑时钟的角色,用于检测过时的信息。
1.2 节点角色
在 Raft 协议中,任何时刻一个节点都处于以下三个主要角色之一,以及一个特殊角色:
Follower(跟随者)
- 所有节点的初始状态。
- 被动响应来自 Leader 和 Candidate 的 RPC 请求。
- 如果接收到 Leader 的心跳(空的
AppendEntries
RPC)或日志条目,则保持 Follower 状态。 - 如果在**选举超时(Election Timeout)**周期内没有收到 Leader 的通信,则转换状态为 Candidate,并发起新一轮选举(增加当前 term)。
Candidate(候选者)
- 由 Follower 超时后转换而来,用于发起选举以成为新的 Leader。
- 首先增加自己的当前任期号(
currentTerm
)。 - 投票给自己。
- 向集群中所有其他节点发送
RequestVote
RPC 请求投票。 - 状态转换:
- 如果获得超过半数节点的投票,则成为 Leader。
- 如果接收到具有更高或相等任期号的新 Leader 的
AppendEntries
RPC,则转变为 Follower。 - 如果选举超时(没有收到过半数投票,也没有发现新 Leader),则增加任期号,开始新一轮选举。
Leader(领导者)
- 负责处理所有客户端请求(如果客户端请求 Follower,Follower 会将其重定向到 Leader)。
- 管理日志复制:接收客户端请求,将其作为日志条目(log entry)添加到自己的日志中,并通过
AppendEntries
RPC 复制到其他 Follower 节点。 - 定期向所有 Follower 发送心跳(空的
AppendEntries
RPC)以维持领导地位并阻止新的选举。 - 如果接收到具有更高任期号的节点的 RPC 请求或响应,则转变为 Follower。
Learner(学习者) - 特殊角色
- 接收 Leader 的日志复制(
AppendEntries
RPC),但不参与 Leader 选举,也不计入日志条目提交所需的“大多数”节点。 - 主要用于在不影响集群选举和提交逻辑的情况下,让新节点追赶日志或扩展读能力。
- 当 Learner 的日志足够接近 Leader 时,可以通过集群配置变更将其提升为 Follower。
- 接收 Leader 的日志复制(
2. Raft 的 Leader 选举
Leader 选举是 Raft 保证单一 Leader 权威性的核心机制。
触发选举: 当一个 Follower 在一个随机化的
Election Timeout
时间内没有收到 Leader 的心跳时,它认为 Leader 可能已失效,于是增加自己的currentTerm
,转变为 Candidate,并发起选举。随机化超时时间是为了减少多个 Follower 同时发起选举导致选票分裂的可能性。请求投票 (
RequestVote
RPC): Candidate 向集群中的其他所有节点发送RequestVote
RPC,包含以下信息:term
: Candidate 的任期号。candidateId
: Candidate 自身的 ID。lastLogIndex
: Candidate 日志中最后一条目的索引。lastLogTerm
: Candidate 日志中最后一条目的任期号。
投票规则: 接收到
RequestVote
RPC 的节点(Follower 或 Candidate)会根据以下规则决定是否投票:- 任期检查: 如果请求的
term
小于接收者当前的currentTerm
,则拒绝投票。如果请求的term
大于接收者当前的currentTerm
,接收者会更新自己的currentTerm
为该term
,并转变为 Follower 状态(即使它之前是 Candidate 或 Leader)。 - 日志完整性检查(Safety): 只有当 Candidate 的日志至少和接收者自己的日志一样新(up-to-date)时,接收者才会投票给它。比较规则是:优先比较最后一条日志的任期号,任期号大的更新;如果任期号相同,则日志索引更大的(即日志更长的)更新。
- 先到先得: 在一个任期内,每个节点最多只能投票给一个 Candidate。
- 任期检查: 如果请求的
选举结果:
- 成为 Leader: 如果一个 Candidate 在同一任期内收到了来自集群中大多数(严格超过半数)节点的投票,它就赢得了选举,成为 Leader。随后,它会立即向所有其他节点发送心跳,宣告自己的领导地位。
- 选举失败(Split Vote): 如果多个 Candidate 同时发起选举,可能没有任何一个 Candidate 能获得大多数选票。这种情况下,它们会等待选举超时,然后增加任期号,开始新一轮选举。由于
Election Timeout
是随机的,再次发生选票分裂的可能性会降低。 - 发现新 Leader: 如果一个 Candidate 在等待投票期间,收到了来自声称是 Leader(且具有不小于 Candidate 当前任期的
term
)的AppendEntries
RPC,那么该 Candidate 会承认这个 Leader 的合法性,并转变为 Follower。
3. Raft 的日志复制(Log Replication)
一旦选出 Leader,系统便开始正常运行。Leader 负责接收客户端请求,将它们作为日志条目追加到自己的日志中,并将这些条目复制到所有 Follower,以确保所有节点最终拥有相同的日志序列。
3.1 日志结构
每个节点的日志由一系列**日志条目(Log Entry)**组成。每个条目包含:
- 指令(Command): 由客户端指定的状态机操作。
- 任期号(Term): 创建该条目时的 Leader 任期号。
- 索引(Index): 该条目在日志中的位置(从 1 开始递增)。
3.2 复制过程 (AppendEntries
RPC)
接收请求: Leader 接收到客户端的写请求。
追加日志: Leader 将该请求作为一个新的日志条目追加到自己的本地日志中。
发送 RPC: Leader 向每个 Follower 并行发送
AppendEntries
RPC,携带一个或多个新的日志条目。RPC 包含以下关键信息:term
: Leader 的当前任期号。leaderId
: Leader 自身的 ID。prevLogIndex
: 紧邻新日志条目之前的那个日志条目的索引。prevLogTerm
:prevLogIndex
对应条目的任期号。entries[]
: 要存储的日志条目(如果是心跳则为空)。leaderCommit
: Leader 已知的、已经被提交的最高日志条目的索引。
Follower 处理: Follower 收到
AppendEntries
RPC 后:- 任期检查: 如果 RPC 的
term
小于 Follower 的currentTerm
,则拒绝该 RPC。 - 一致性检查: Follower 检查其本地日志在
prevLogIndex
位置上的条目任期号是否与 RPC 中的prevLogTerm
匹配。- 匹配: 如果匹配,说明之前的日志是一致的。Follower 会将
entries[]
中的新条目追加到自己的日志中(如果存在冲突的未提交条目,会先删除冲突条目及其之后的所有条目)。然后向 Leader 回复成功。 - 不匹配: 如果不匹配,说明 Follower 的日志在
prevLogIndex
处与 Leader 不一致。Follower 会拒绝该 RPC,并告知 Leader。Leader 在收到拒绝后,会递减prevLogIndex
,并重新发送包含更早日志条目的AppendEntries
RPC,直到找到与 Follower 日志一致的点。
- 匹配: 如果匹配,说明之前的日志是一致的。Follower 会将
- 更新 Commit Index: Follower 会将自己的
commitIndex
更新为min(leaderCommit, index of last new entry)
。
- 任期检查: 如果 RPC 的
Leader 处理响应:
- 成功响应: Leader 收到 Follower 的成功响应后,会更新该 Follower 的
nextIndex
(下一个要发送给该 Follower 的日志条目索引)和matchIndex
(已知已复制到该 Follower 的最高日志条目索引)。 - 失败响应(因一致性检查): Leader 会递减对应 Follower 的
nextIndex
,并在下一次AppendEntries
RPC 中包含前一个日志条目,逐步向前回溯,直到找到一致点。 - 失败响应(因任期过低): 如果 Leader 收到响应表明自己的
term
过低,它会更新自己的term
并转变为 Follower。
- 成功响应: Leader 收到 Follower 的成功响应后,会更新该 Follower 的
3.3 日志提交(Commitment)
- 提交条件: 当 Leader 发现某个日志条目已经被成功复制到大多数(超过半数)节点上时(包括 Leader 自己),该日志条目就被认为是**已提交(Committed)**的。Leader 通过跟踪每个 Follower 的
matchIndex
来确定。 - 通知 Follower: Leader 在后续的
AppendEntries
RPC 中通过leaderCommit
字段告知所有 Follower 当前已提交的最高日志索引。 - 应用到状态机: 节点(Leader 和 Follower)一旦知道某个日志条目已被提交,就可以安全地将该条目中的指令**应用(Apply)**到其本地的状态机中,从而改变系统状态并最终响应客户端。应用操作是按日志顺序进行的。
3.4 安全性(Safety)
Raft 通过以下机制保证安全性:
- 选举限制: Candidate 必须拥有包含所有已提交条目的日志,才能赢得选举。这通过
RequestVote
RPC 中的日志完整性检查实现。 - Leader Append-Only: Leader 只能追加新条目,永远不会覆盖或删除其日志中的现有条目(尤其是已提交的条目)。
- Commit Rule: 只有被存储在大多数节点上的日志条目才能被提交。这确保了已提交的条目在后续的 Leader 选举中一定会被保留。
4. Learner 角色详解
4.1 为什么需要 Learner?
在某些场景下,直接将新节点以 Follower 身份加入集群可能存在问题:
- 影响可用性: 新加入的 Follower 需要从 Leader 同步大量日志数据。在此期间,如果 Leader 恰好需要这个新节点来构成“大多数”以提交新的日志条目,那么提交过程可能会被显著拖慢,甚至阻塞,直到新节点追赶上进度。
- 影响选举: 如果一个集群规模较小(例如 3 个节点),增加一个节点会改变“大多数”的阈值(从 2 变为 3)。如果新节点加入时日志差距很大,可能会影响选举的正常进行。
Learner 角色就是为了解决这些问题而引入的。
4.2 Learner 的工作方式
- 只同步,不参与: Learner 节点像 Follower 一样从 Leader 接收
AppendEntries
RPC 并追加日志条目,但它不参与 Leader 选举(既不能投票,也不能成为 Candidate),并且 Leader 在判断日志条目是否可以提交时,不计算 Learner 节点。 - 追赶日志: Learner 可以安全地加入集群并开始同步日志,而不会影响现有集群的提交决策和选举过程。
- 提升为 Follower: 一旦 Learner 的日志追赶到与 Leader 足够接近(通常由管理员或自动化机制判断),就可以通过一次集群成员变更(Configuration Change)将其角色提升为 Follower,使其正式成为集群中有投票权和计入提交决策的一员。
4.3 Learner 的应用场景
- 新节点加入(Node Addition): 最常见的场景。让新节点先作为 Learner 加入,同步完数据后再转为 Follower,避免影响集群性能和可用性。
- 扩展读能力(Read Scaling): 可以部署 Learner 节点专门用于处理只读请求,它们拥有最新的已提交数据副本,但不增加选举和写操作的复杂性。
- 数据中心迁移/容灾: 在另一个数据中心部署 Learner 节点作为冷备份或用于未来的切换。
- 临时成员: 在某些维护或测试场景下,临时加入一个 Learner 观察集群状态或接收数据。
5. 总结
Raft 协议通过将一致性问题分解为 Leader 选举、日志复制和安全性三个子问题,提供了一种更易于理解和实现的分布式一致性解决方案。其核心机制包括:
- 任期(Term) 作为逻辑时钟。
- 明确的节点角色(Follower, Candidate, Leader)及其转换规则。
- 基于随机超时和日志完整性的 Leader 选举。
- 通过
AppendEntries
RPC 实现的日志复制,确保日志在大多数节点上一致后才提交。 - Learner 角色用于平滑地增加节点或扩展读能力,而不影响核心一致性协议的运行。
Raft 的设计使其在实际系统(如 etcd, Consul, TiKV 等)中得到了广泛应用,为构建可靠的分布式系统奠定了坚实的基础。