Paxos 算法是什么?介绍 RAFT 和 ZAB,以及它们之间的区别?会有脑裂问题吗?为什么?
1. Paxos 算法:共识的理论基石
- 目标: 在可能出现故障(进程宕机、网络延迟、分区、消息丢失/重复)的异步网络中,让一组节点对一个值达成一致。它是最早被证明正确的共识算法之一,奠定了理论基础。
- 核心角色:
- Proposer: 提出提案(Proposal,包含提案编号和提议的值)。
- Acceptor: 负责“投票”决定是否接受提案。
- Learner: 学习被选定的值(非必需角色,可合并)。
- 核心阶段 (两阶段):
- Prepare 阶段 (Phase 1a & 1b):
- Proposer 选择一个全局唯一的、递增的提案编号
N
,向大多数 (Majority) Acceptor 发送Prepare(N)
请求。 - Acceptor 收到
Prepare(N)
:- 如果
N
大于它之前响应过的任何 Prepare 请求的编号,则承诺不再接受任何编号小于N
的提案,并返回它已接受过的编号小于N
的提案中编号最大的那个提案(如果有的话,包含提案编号和值)。 - 否则忽略或回复拒绝。
- 如果
- Proposer 选择一个全局唯一的、递增的提案编号
- Accept 阶段 (Phase 2a & 2b):
- Proposer 如果收到大多数 Acceptor 对
Prepare(N)
的响应:- 如果响应中没有返回任何已接受的提案值,则 Proposer 可以自由选择自己的值
V
。 - 如果响应中有返回已接受的提案(即存在
(proposal_id, value)
),则 Proposer 必须选择其中编号最大的那个提案的值V
作为自己提案的值(这是保证安全性的关键)。
- 如果响应中没有返回任何已接受的提案值,则 Proposer 可以自由选择自己的值
- Proposer 向大多数 Acceptor 发送
Accept(N, V)
请求。 - Acceptor 收到
Accept(N, V)
:- 如果它没有承诺过不接受编号为
N
的提案(即它没有响应过编号大于N
的Prepare
请求),则接受该提案(N, V)
并回复。 - 否则忽略或回复拒绝。
- 如果它没有承诺过不接受编号为
- Proposer 如果收到大多数 Acceptor 对
Accept(N, V)
的接受回复,则值V
被选定。
- Proposer 如果收到大多数 Acceptor 对
- Prepare 阶段 (Phase 1a & 1b):
- 特点:
- 理论严谨: 安全性(Safety)有严格证明(唯一值、值一旦选定不可更改)。
- 复杂难懂: 原始论文晦涩,工程实现需要大量变种和优化(Multi-Paxos 用于日志复制)。
- 活性问题: 存在活锁风险(Proposer 不断提出更高编号但无法达成一致)。
- 效率: 一次基本 Paxos 实例只能确定一个值。Multi-Paxos 通过选举一个相对稳定的 Leader 来优化连续提案,减少 Prepare 阶段开销。
2. Raft 算法:为可理解性而设计
- 目标: 提供与 Paxos 相同的安全性和容错性(容忍
(n-1)/2
个节点故障),但通过分解问题(Leader 选举、日志复制、安全性) 和强化 Leader 角色,使其更容易理解和实现。 - 核心角色 (强Leader制):
- Leader: 唯一处理客户端请求的节点。接收请求,将操作作为日志条目(Log Entry)复制到 Follower,在日志条目被安全复制后通知 Follower 提交(Apply to State Machine)。
- Follower: 被动响应 Leader 和 Candidate 的请求。存储 Leader 复制过来的日志。
- Candidate: 在选举过程中暂时存在的状态(Follower 超时未收到 Leader 心跳时转换而来)。
- 核心机制:
- Leader 选举:
- 节点启动为 Follower。
- Follower 在选举超时(随机时间,通常在 150-300ms)内未收到 Leader 的心跳 (AppendEntries RPC),则转变为 Candidate。
- Candidate 增加自己的任期号 (单调递增),向所有其他节点发送
RequestVote
RPC。 - 节点收到
RequestVote
:- 仅当 Candidate 的任期号 >= 自己的当前任期 并且 自己的日志至少不比 Candidate 的日志旧(比较最后一条日志的 term 和 index)并且 自己在本任期还没投过票时,才投票给该 Candidate。
- Candidate 如果收到大多数节点的投票,则成为新 Leader。
- Leader 立即开始发送心跳(空的
AppendEntries
RPC)以确立权威并阻止新选举。 - 安全性关键: 投票规则确保新 Leader 一定包含所有已提交的日志条目。
- 日志复制:
- 客户端请求发送到 Leader。
- Leader 将请求作为新日志条目追加到自己的日志中。
- Leader 通过
AppendEntries
RPC 将新日志条目并行发送给所有 Follower。 - Follower 收到
AppendEntries
:- 检查一致性(前一条日志的 term 和 index 是否匹配)。
- 如果匹配,则追加新条目并回复成功。
- 如果不匹配,Leader 递减索引并重试,直到找到一致点并修复 Follower 的日志(强制覆盖)。
- Leader 收到大多数 Follower 对某条日志条目的成功回复后,认为该条目已提交(Committed),将其应用到自己的状态机。
- Leader 在后续的
AppendEntries
RPC(包含心跳)中通知 Follower 最新的已提交索引。 - Follower 将已知已提交的日志条目应用到自己的状态机。
- 安全性约束:
- 选举限制 (Election Restriction): 如上所述,确保 Leader 包含所有已提交日志。
- 提交规则 (Leader Completeness): Leader 只能提交当前任期的日志条目(直接或间接通过提交包含当前任期条目的旧条目)。
- Leader 选举:
- 特点:
- 易于理解: 角色清晰,流程直观。
- 强 Leader: 简化了日志复制流程,提高了效率。
- 成员变更: 通过 Joint Consensus 或 Single-Server Changes 安全地改变集群成员。
- 日志压缩: 通过快照(Snapshot)机制清理旧日志。
3. ZAB 协议:ZooKeeper 的核心
- 目标: 为 ZooKeeper 这个分布式协调服务设计的原子广播协议,保证所有事务(状态变更)以相同的顺序被所有服务器可靠地交付(Exactly-Once Delivery)。核心是保证全局有序(Total Order)。
- 核心角色:
- Leader: 唯一处理写请求的节点(读请求 Follower 可直接处理)。负责将事务提议(Proposal)按顺序广播。
- Follower: 参与选举,接收 Leader 的广播消息并 Ack。
- Observer: (可选)仅接收广播,不参与投票,用于扩展读性能。
- 核心阶段:
- 崩溃恢复 (Leader Election & Discovery):
- 选举 (Fast Leader Election): 节点进入 Looking 状态。交换投票信息(包含 epoch - zxid 的高位, zxid - 最大事务ID)。节点投票给拥有最大 zxid 的节点,如果 zxid 相同则投票给 server id 最大的节点。快速选出具有最新历史的 Leader(通常是拥有最大 zxid 的节点)。
- 发现 (Synchronization): 新 Leader 与 Follower 同步状态,确保所有 Follower 拥有 Leader 上所有已提交的事务。Follower 会丢弃 Leader 上没有的事务。目标是使 Leader 和 Follower 的事务日志在提交点之前完全一致。
- 消息广播 (Atomic Broadcast):
- Leader 为每个事务分配一个全局单调递增的 zxid(事务ID)。
- Leader 将事务作为 Proposal 按 zxid 顺序广播给所有 Follower。
- Follower 按顺序接收 Proposal 并写入本地事务日志,然后向 Leader 发送 Ack。
- Leader 收到大多数 Follower 的 Ack 后,认为该事务已提交。
- Leader 发送
COMMIT
消息给所有 Follower(或在下一个 Proposal 中捎带 Commit 信息)。 - Follower 收到
COMMIT
后,将事务应用到内存数据库 (ZooKeeper DataTree)。
- 崩溃恢复 (Leader Election & Discovery):
- 特点:
- 为 ZooKeeper 量身定制: 深度集成到 ZooKeeper 的设计中(如 zxid 结构)。
- 主备模式: 强 Leader,所有写必须通过 Leader。
- 保证顺序: 严格保证事务的全局顺序(FIFO 客户端顺序 + 因果顺序)。
- 高性能恢复: Fast Leader Election 和 Synchronization 阶段设计旨在快速恢复服务。
- 视图 (Epoch): 每个 Leader 任期对应一个 Epoch(zxid 高位),用于识别过期的 Leader。
Paxos, Raft, ZAB 关键区别总结
特性 | Paxos (Basic) | Raft | ZAB |
---|---|---|---|
主要目标 | 就单个值达成一致 | 管理复制日志 (状态机复制) | 原子广播有序事务 (状态机复制) |
角色 | Proposer/Acceptor/Learner (抽象) | Leader/Follower/Candidate (具体) | Leader/Follower/Observer (具体) |
领导者机制 | 无固有 Leader (Multi-Paxos 引入) | 强 Leader (读写必经 Leader) | 强 Leader (写必经 Leader) |
理解/实现难度 | 非常困难 | 相对容易 | 中等 (集成于 ZK,理解需结合 ZK) |
日志复制流程 | 复杂 (需处理乱序提案) | 清晰 (Leader 顺序追加并修复日志) | 清晰 (Leader 顺序广播 Proposal) |
核心阶段 | Prepare/Promise; Accept/Accepted | Leader Election; Log Replication | Fast Leader Epoch; Sync; Broadcast |
全局顺序保证 | 需 Multi-Paxos 额外机制实现 | 内置 (通过 Leader 和日志索引) | 内置 (通过 zxid 和 Leader) |
成员变更 | 非核心,需扩展 | 核心支持 (安全协议) | 核心支持 |
日志压缩 | 非核心,需扩展 | 核心支持 (快照) | 核心支持 (快照) |
典型应用 | 理论基石,Chubby (优化版) | etcd, Consul, TiKV, 众多新数据库 | ZooKeeper |
脑裂问题 (Split-Brain) 分析
-
什么是脑裂?
在分布式系统中,当网络发生分区(Network Partition)时,原本的一个集群可能被分割成两个或多个彼此无法通信的子集群。如果这些子集群都认为自己是“活着的”并独立地处理写请求(比如都选举出了自己的 Leader 并接受客户端写操作),就会导致数据不一致。这就是脑裂问题。 -
Paxos/Raft/ZAB 会有脑裂问题吗?为什么?
在严格遵守算法协议的前提下,Paxos、Raft、ZAB 都通过“多数派 (Quorum)”原则来避免脑裂导致的数据不一致。它们本身不会发生真正的脑裂(即多个分区同时成功提交冲突的写操作)。-
核心防御机制:多数派 (Quorum)
- 所有三个算法在关键操作上都要求获得大多数 (Majority) 节点的同意才能生效:
- Paxos: 选定值需要大多数 Acceptor 接受 (
Accept
阶段)。 - Raft:
- 选举 Leader 需要大多数节点的投票 (
RequestVote
)。 - 提交日志条目需要大多数节点成功复制 (
AppendEntries
Ack)。
- 选举 Leader 需要大多数节点的投票 (
- ZAB:
- 选举 Leader 需要大多数节点的投票 (Fast Leader Election)。
- 提交事务 Proposal 需要大多数 Follower 的 Ack。
- Paxos: 选定值需要大多数 Acceptor 接受 (
- 所有三个算法在关键操作上都要求获得大多数 (Majority) 节点的同意才能生效:
-
为什么多数派能防止脑裂?
- 假设一个
2n+1
个节点组成的集群。 - 发生网络分区后,任何包含大多数节点的分区最多只能有一个(因为
(n+1) + (n+1) > 2n+1
,不可能同时存在两个分区都达到n+1
个节点)。 - 因此:
- 选举: 只有包含大多数节点的那个分区才能成功选举出新的 Leader。另一个分区(节点数 <=
n
)无法获得大多数投票,选举会失败,其中的节点会保持 Follower/Candidate (Raft) 或 Looking (ZAB) 状态,不会成为 Leader。 - 写操作/提案提交: 只有包含大多数节点的分区(即拥有合法 Leader 的分区)才能成功提交写操作(日志条目/事务)。另一个分区的节点即使误认为有 Leader(实际上选举失败或 Leader 是旧的),也无法获得大多数 Ack,写操作无法提交。
- 选举: 只有包含大多数节点的那个分区才能成功选举出新的 Leader。另一个分区(节点数 <=
- 结果: 虽然网络分区发生了,但只有一个分区(包含大多数节点的分区)能正常提供服务并提交写操作。另一个分区无法提供服务(选举不出 Leader 或写操作无法提交)。 这保证了数据的一致性,避免了脑裂。
- 假设一个
-
-
注意事项 (为什么有时感觉有“脑裂”)
- 客户端视角的“双主”: 在网络分区期间,客户端可能连接到不同分区。连接到合法分区的客户端能正常读写。连接到少数派分区的客户端可能:
- 遇到超时或错误(如果该分区没有 Leader)。
- 读到旧数据: 如果该分区的 Follower 尚未与 Leader 断开连接感知,或者 ZooKeeper 允许 Follower 处理读请求(可能读到未更新的数据)。
- 但关键是,连接到少数派分区的客户端绝对无法成功完成写操作(因为写必须提交到 Leader,而少数派分区无法提交写)。
- 配置错误 (真正的危险): 如果人为错误配置(如设置了错误的集群节点列表、多数派计算错误),破坏了多数派原则,那么算法本身的安全性保证就被打破了,脑裂就可能发生。这是工程实践中脑裂最常见的原因,而非算法本身的缺陷。
- 旧 Leader 的“僵尸”写: 在网络分区初期,旧 Leader 可能不知道自己已失去多数派连接。它可能仍然尝试处理写请求,但由于无法获得大多数 Ack,这些写请求会失败或阻塞(取决于实现),不会被提交。当它重新加入集群时,其未提交的日志会被新 Leader 覆盖(Raft)或通过同步丢弃(ZAB)。
- ZooKeeper 的
sync
命令: 为了应对 Follower 可能读到过期数据的问题(特别是在网络恢复但同步完成前),ZooKeeper 提供了sync
命令,客户端可以在读操作前调用它来确保自己连接的服务器的状态是最新的(或至少不旧于调用sync
时的状态)。
- 客户端视角的“双主”: 在网络分区期间,客户端可能连接到不同分区。连接到合法分区的客户端能正常读写。连接到少数派分区的客户端可能: