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 协议中,任何时刻一个节点都处于以下三个主要角色之一,以及一个特殊角色:

  1. Follower(跟随者)

    • 所有节点的初始状态。
    • 被动响应来自 Leader 和 Candidate 的 RPC 请求。
    • 如果接收到 Leader 的心跳(空的 AppendEntries RPC)或日志条目,则保持 Follower 状态。
    • 如果在**选举超时(Election Timeout)**周期内没有收到 Leader 的通信,则转换状态为 Candidate,并发起新一轮选举(增加当前 term)。
  2. Candidate(候选者)

    • 由 Follower 超时后转换而来,用于发起选举以成为新的 Leader。
    • 首先增加自己的当前任期号(currentTerm)。
    • 投票给自己。
    • 向集群中所有其他节点发送 RequestVote RPC 请求投票。
    • 状态转换:
      • 如果获得超过半数节点的投票,则成为 Leader。
      • 如果接收到具有更高或相等任期号的新 Leader 的 AppendEntries RPC,则转变为 Follower。
      • 如果选举超时(没有收到过半数投票,也没有发现新 Leader),则增加任期号,开始新一轮选举。
  3. Leader(领导者)

    • 负责处理所有客户端请求(如果客户端请求 Follower,Follower 会将其重定向到 Leader)。
    • 管理日志复制:接收客户端请求,将其作为日志条目(log entry)添加到自己的日志中,并通过 AppendEntries RPC 复制到其他 Follower 节点。
    • 定期向所有 Follower 发送心跳(空的 AppendEntries RPC)以维持领导地位并阻止新的选举。
    • 如果接收到具有更高任期号的节点的 RPC 请求或响应,则转变为 Follower。
  4. Learner(学习者) - 特殊角色

    • 接收 Leader 的日志复制(AppendEntries RPC),但不参与 Leader 选举,也不计入日志条目提交所需的“大多数”节点。
    • 主要用于在不影响集群选举和提交逻辑的情况下,让新节点追赶日志或扩展读能力。
    • 当 Learner 的日志足够接近 Leader 时,可以通过集群配置变更将其提升为 Follower。

2. Raft 的 Leader 选举

Leader 选举是 Raft 保证单一 Leader 权威性的核心机制。

  1. 触发选举: 当一个 Follower 在一个随机化的 Election Timeout 时间内没有收到 Leader 的心跳时,它认为 Leader 可能已失效,于是增加自己的 currentTerm,转变为 Candidate,并发起选举。随机化超时时间是为了减少多个 Follower 同时发起选举导致选票分裂的可能性。

  2. 请求投票 (RequestVote RPC): Candidate 向集群中的其他所有节点发送 RequestVote RPC,包含以下信息:

    • term: Candidate 的任期号。
    • candidateId: Candidate 自身的 ID。
    • lastLogIndex: Candidate 日志中最后一条目的索引。
    • lastLogTerm: Candidate 日志中最后一条目的任期号。
  3. 投票规则: 接收到 RequestVote RPC 的节点(Follower 或 Candidate)会根据以下规则决定是否投票:

    • 任期检查: 如果请求的 term 小于接收者当前的 currentTerm,则拒绝投票。如果请求的 term 大于接收者当前的 currentTerm,接收者会更新自己的 currentTerm 为该 term,并转变为 Follower 状态(即使它之前是 Candidate 或 Leader)。
    • 日志完整性检查(Safety): 只有当 Candidate 的日志至少和接收者自己的日志一样新(up-to-date)时,接收者才会投票给它。比较规则是:优先比较最后一条日志的任期号,任期号大的更新;如果任期号相同,则日志索引更大的(即日志更长的)更新。
    • 先到先得: 在一个任期内,每个节点最多只能投票给一个 Candidate。
  4. 选举结果:

    • 成为 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)

  1. 接收请求: Leader 接收到客户端的写请求。

  2. 追加日志: Leader 将该请求作为一个新的日志条目追加到自己的本地日志中。

  3. 发送 RPC: Leader 向每个 Follower 并行发送 AppendEntries RPC,携带一个或多个新的日志条目。RPC 包含以下关键信息:

    • term: Leader 的当前任期号。
    • leaderId: Leader 自身的 ID。
    • prevLogIndex: 紧邻新日志条目之前的那个日志条目的索引。
    • prevLogTerm: prevLogIndex 对应条目的任期号。
    • entries[]: 要存储的日志条目(如果是心跳则为空)。
    • leaderCommit: Leader 已知的、已经被提交的最高日志条目的索引。
  4. 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 日志一致的点。
    • 更新 Commit Index: Follower 会将自己的 commitIndex 更新为 min(leaderCommit, index of last new entry)
  5. Leader 处理响应:

    • 成功响应: Leader 收到 Follower 的成功响应后,会更新该 Follower 的 nextIndex(下一个要发送给该 Follower 的日志条目索引)和 matchIndex(已知已复制到该 Follower 的最高日志条目索引)。
    • 失败响应(因一致性检查): Leader 会递减对应 Follower 的 nextIndex,并在下一次 AppendEntries RPC 中包含前一个日志条目,逐步向前回溯,直到找到一致点。
    • 失败响应(因任期过低): 如果 Leader 收到响应表明自己的 term 过低,它会更新自己的 term 并转变为 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 等)中得到了广泛应用,为构建可靠的分布式系统奠定了坚实的基础。


Raft 分布式一致性协议详解
https://mfzzf.github.io/2025/03/13/Raft协议/
作者
Mzzf
发布于
2025年3月13日
许可协议