news 2026/10/23 5:31:41

分布式一致性算法:2PC、Paxos、Raft、Zab与脑裂机制全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
分布式一致性算法:2PC、Paxos、Raft、Zab与脑裂机制全解析

1. 先对齐问题:这几个算法不是一类东西

每次和搞基础架构的朋友聊面试,我都能听到候选人把 Paxos、Raft、Zab、2PC 这几个名字一口气背出来,但问到“它们各自解决什么问题”“能不能互相替代”“脑裂是怎么发生的”,很多人就开始含糊了。这类算法确实容易被混在一起,因为大家都是从“分布式一致性”这个入口听说的,可它们解决的问题、适用的边界、能容忍的故障模型,其实差别很大。

我先把结论摆在这:Paxos、Raft、Zab 是共识算法,目标是让一群节点就某个值达成一致,它们通常用来做复制状态机(Replicated State Machine),也就是把一份操作日志复制到多个节点,然后每个节点按同样顺序执行,得到同样的状态。2PC 则是原子提交协议,目标是让多个参与者要么全部提交、要么全部回滚,它不负责节点之间就“谁的值对”达成一致,只负责“这一个事务要不要落盘”。

标题里的“2CP”我理解是 2PC(Two-Phase Commit)的笔误,后面统一写作 2PC。这两种场景经常在同一套系统里出现,但认知上必须先分开,否则后面的脑裂问题、Quorum 机制、Leader 选举都会越看越乱。

这篇内容适合三类人:刚入门分布式、想系统梳理共识算法边界的;在做技术选型、想知道该用哪个方向的;还有准备面试、想搞清楚“为什么 Raft 比 Paxos 容易讲”“Zab 和 Raft 到底差在哪”的人。我会按我自己的理解,把这些算法的核心机制、推导逻辑、工程陷阱全部拆开讲,最后专门聊脑裂问题。

1.1 一致性、共识、原子提交是三个层面的东西

很多人把“一致性”当成一个词,其实落到工程里有几种完全不同的目标。

  • 原子提交(Atomic Commitment):多个参与者各自持有数据,需要一起决定“提交/回滚”,典型代表是 2PC、3PC,常见于跨库事务、消息中间件的事务消息。
  • 共识(Consensus):一群节点对同一个提案达成一致,提案内容本身可以是任意的,典型代表是 Paxos、Raft、Zab 的选举阶段,常见于选主、分布式锁、配置下发。
  • 复制状态机(Replicated State Machine):用共识算法保证每个节点以相同顺序追加日志,然后按序执行日志,得到一致的最终状态。这是 Paxos 和 Raft 的经典用法。

共识算法和原子提交协议最本质的区别,在于“是否需要所有人都活着”。2PC 在本质上需要所有参与者都在线、都投票、都执行,任何一个参与者故障都可能阻塞整个事务;而共识算法只需要大多数节点(Quorum)在线就可以继续工作,少数派节点挂了并不影响系统推进。

这种容错模型的差异,直接决定了它们各自擅长什么场景。跨数据中心、跨团队的数据一致性保障,可以用 2PC 那一套;高可用副本复制、服务选主、配置同步,则几乎全是 Paxos/Raft/Zab 的天下。

1.2 复制日志:一致性的通用载体

理解 Paxos、Raft、Zab,千万别一开始就盯着“选举”“提案编号”这些细节,要抓住它们共同的抽象:把状态机的输入日志复制到所有副本上。

假设我们有一个 KV 存储,客户端执行Set(key, value),节点按“收日志请求 → 追加本地日志 → 日志达成一致 → 应用到状态机 → 返回客户端”来工作。只要每个节点追加的日志序列完全一致,那么即使它们各自应用日志的时间有先后,最终状态也是相同的。这就是复制状态机的核心思想。

所以 Paxos、Raft、Zab 真正做的事情,是“让每一个日志条目在所有副本上都以确定顺序被记录”。谁先谁后、由谁决定顺序、临时不一致怎么纠偏,这些才是算法的差异点。

在这个抽象下,脑裂问题的本质就很清楚了:如果网络分区后两个节点组都认为自己还有资格继续接受写请求,那么两边就会追加不相兼容的日志,等分区恢复时,日志已经产生了永久性分叉。一致性算法的任务,就是设计一套规则,让这种“两个组都觉得自己合法”的情况要么不可能发生,要么发生后能被自动纠正。

1.3 神经分区:一切问题的根源

分布式系统里没有“故障”这种单数概念。一个节点故障是一回事,网络故障让两个节点互相“看不见”是另一回事。后者的可怕之处在于:两边都在运行,都在处理请求,只是无法互相通信。

如果这时整个系统只有一个决策中心,比如单主架构,那么主节点在分区中的哪一边,决定了哪边能继续服务。要保证高可用,就必须允许少数派那边“尽快发现主节点不可达并重新选举”。问题来了:少数派那边选主成功了,原来的主节点其实还活着,只是网络把它隔离了。这就会出现两个主节点同时认为自己是合法的,也就是脑裂。

所有共识算法设计的核心,就是在回答一个问题:当网络分区发生时,如何保证只有一个主(或者只有一组合法决策者)能继续工作。理解了这一点,下面所有算法的细节才有了落点。

2. 2PC:最朴素的“全员点头”方案

2PC 是理解其他算法的最好起点,因为它最符合直觉:一件事情做不做,每个人都表态,所有人都同意才做。实现简单、理解简单,但它的问题也很典型。

2.1 Prepare 和 Commit 两轮怎么走

2PC 由一个协调者(Coordinator)和多个参与者(Participant)组成,流程只有两个阶段。

第一阶段,协调者向所有参与者发送 Prepare 请求,参与者收到后各自完成两件事:检查本地事务能否执行,能的话就写本地事务日志,然后锁定相关资源(比如数据库行锁),最后回复“可以提交”或者“不能提交”。这个“本地事务日志”很重要,它是后续故障恢复的依据,先落盘再回复,不能反过来。

如果所有参与者都回复“可以提交”,协调者进入第二阶段,向所有参与者发送 Commit 消息;只要有一个人回复“不能提交”,协调者就广播 Abort。参与者收到 Commit/Abort 后执行真正的提交或回滚,释放资源,回复 ACK,协调者记录最终结果。

用文字描述是这样的流程,实际协议的时序图大概是下面这种:

Coordinator Participant A Participant B |── Prepare 请求 ─────────>| | | |── 写事务日志并加锁 ───────>| 本地完成 |<───────── Vote Yes ─────| | |<───────────────────────── Vote Yes ─────────────────| | (全部 Yes) | |── Commit ───────────────>| | |── Commit ──────────────────────────────────────────>| |<───────── Ack ──────────| | |<───────────────────────── Ack ──────────────────────|

2.2 2PC 的软肋:阻塞、单点、脑裂

看着简单,2PC 的问题在真实环境里非常致命。

第一个问题叫阻塞。参与者一旦在第一阶段回复了 Yes,就必须一直持有资源锁,等待协调者的最终指令。如果协调者在这个节骨眼上崩了,参与者会一直卡在“事务未决”状态,既不能提交也不能回滚,资源锁一直占着。要是这个协调者永远不回来,那所有相关资源就永远被锁住,业务直接停摆。

第二个问题是协调者单点。协调者本身没有高可用设计,它挂了,整套事务的推进就悬空。有人会想“再起一个协调者”,但新协调者并不知道旧协调者之前有没有发过 Commit,贸然通知参与者提交可能造成错误,通知回滚又可能把已经提交的事务回滚掉,两边都不安全。

第三个问题其实就是脑裂的变体。假设协调者已经发出 Commit,其中一部分参与者收到了并提交,另一部分因为网络分区没收到,它们还在等指令。分区恢复后,一部分参与者状态是“已提交”,另一部分是“未决”,整个系统处于不一致状态。2PC 本身没有任何机制来处理这种情况,必须靠外部手段人工介入或者用补偿逻辑兜底。

所以严格说,2PC 不是一个容错协议,而是一个“在没有故障的理想条件下”保证原子性的协议。它假设协调者不会挂,网络不会分区,这在分布式环境中几乎不成立。

2.3 工程上的妥协:XA、消息表、TCC

虽然 2PC 有这么多问题,它仍然被广泛使用,原因很简单:对于短事务、小规模参与者、低故障概率的场景,2PC 的复杂度最低,收益最直接。数据库 XA 协议就是 2PC 的经典实现,很多老系统里的跨库事务都在用它,只是外面套了重试、超时、人工补偿等一大堆手段来弥补缺陷。

工程上常见的 2PC 妥协方案有这么几种。分布式事务消息会先把事务消息放在本地消息表,业务事务和消息写入放在同一个本地事务里,由一个后台任务不断把消息发出去;消息发送方的“事务”和接收方的“消费事务”各自独立,靠消息重试和幂等来兜底。TCC 则是把事务拆成 Try、Confirm、Cancel 三个阶段,用业务逻辑层面把 2PC 的锁和阻塞转换成可补偿的业务操作,实现最终一致。

我个人看法:2PC 应该被当作“理解分布式事务困局”的教材,而不是默认的技术方案。现代架构里能用幂等+消息+补偿解决的事务,尽量不要引入 2PC,它带来的可用性损失和运维复杂度往往比收益更大。

3. Paxos:从数学证明走到现实工程

Paxos 是 Leslie Lamport 提出的共识算法,也是分布式系统理论的一块基石。几乎所有人都说它难学,而且工程落地的坑很多。但它之所以重要,是因为 Paxos 直接把共识问题的核心抽象了出来:用多数派(Quorum)保证每次决策只有一个结果。

3.1 角色和 Quorum

Paxos 里一共有三种角色:Proposer(提案者)、Acceptor(接受者)、Learner(学习者)。提案者提出提案编号和值;接受者负责投票和存储已接受的提案;学习者不参与决策,只学习最终被选定的值。

实际系统里,一个节点通常同时扮演三种角色。提案编号必须全局唯一且递增,这是整个协议能够收敛的前提。不同节点的编号生成规则通常是“节点唯一ID + 本地递增序号”,这样既唯一又能判断新旧。

Paxos 的一个关键思想:一个提案被选定,不要求所有 Acceptor 都接受,只需要超过半数的 Acceptor 接受。为什么是超过半数而不是全部?因为任意两组“超过半数”的 Acceptor 集合必然有交集,这个交集保证了信息不会丢失:后一组 Quorum 里至少有一个节点知道前一组的决策结果。

这个交集性质就是整个协议的数学根基。随便选两个阵营,比如 5 节点集群中的 {1,2,3} 和 {3,4,5},它们必然都包含 3 号节点。决策要想覆盖所有人,必须穿越每一个 Quorum,而所有 Quorum 之间的公共部分让你无论如何都能找到一个“见证者”。

3.2 Prepare 阶段到底在干什么

很多人卡在 Paxos 的 Prepare 阶段,觉得它名不副实:提案者明明还没有提案,为什么要先发 Prepare?

Prepare 阶段真正做的,是“探测并确认历史决策”。Proposer 生成提案编号 n,发给所有 Acceptor,请求他们承诺:“如果收到提案编号小于 n 的 Accept 请求,我会拒绝。”同时,如果这个 Acceptor 已经接受过某个编号更高的提案,它要把那个提案的编号和值一起返回给 Proposer。

这个承诺是协议安全的第一个关键机制。它保证了在同一轮 Paxos 里,后到的旧提案不会干扰已经达成共识的新提案。类比一下:你在会议上先举手说“我要发言”,然后大家记住“你已预约”,别人再想讲同一个话题就必须排在你后面。

如果 Proposer 收到的承诺里,某些 Acceptor 返回了它们曾经接受的提案值,Proposer 就必须放弃自己的自由选择权,改用其中编号最高的那个值。这是协议安全的第二个关键机制:未完成的旧决策不能被新提案悄悄覆盖。

3.3 Accept 阶段和值的选择规则

Prepare 拿到多数派承诺后,Proposer 进入 Accept 阶段,向这些 Acceptor 发送Accept(n, value)。Acceptor 只要没有承诺过更高的提案编号,就接受这个提案,并将值持久化,然后回复。

如果多数派 Acceptor 接受了,那么这个值就被选定,Learner 可以通过查询多数派节点学习到这个值。如果 Prepared 返回的旧值里有最高编号的旧提案,Proposer 必须携带那个旧值再发起 Accept,而不能用自己的新值。这就是 Paxos 著名的“值为那个值、提案编号是新的”规则。

这个规则为什么必须严格遵守?我举个例子。假设有 3 个节点,P1 提出提案 (1, A),被节点 1、2 接受,还没等它向 P1 确认,P1 就掉线了。P2 这时用提案编号 2 发起 Prepare,节点 2 会告诉它“我接受过编号 1 的值 A”。如果 P2 无视这个信息,强行提交自己的值 B,那么节点 1 已经接受了 A,节点 2、3 接受 B,系统里就会出现两个值。但只要 P2 遵守规则改用 A,最终所有节点统一到 A,旧决策被保留。

这个“必须采纳旧值”的细节,是 Paxos 正确性的核心。很多人自以为懂了 Paxos,其实根本没有深入这一步,导致很多实现是错的。

3.4 Basic Paxos 到 Multi-Paxos

Basic Paxos 每决策一个值,就要完整跑两轮 RPC,效率很低,而且多个 Proposer 并发时会形成“活锁”:你 Prepare 到一半,我用更高编号打断你,你再用更高编号打断我,大家谁都提交不了。

工程上基本不会直接用 Basic Paxos,而是用 Multi-Paxos。核心优化是:集群先通过一轮 Paxos 选出一个稳定的 Leader(区分提案者),此后只有 Leader 发起的提案才被处理。由于同一时间只有一个 Leader,下一条日志的决策可以跳过 Prepare 阶段,直接走 Accept,重大降低了 RPC 开销。

Multi-Paxos 还把决策对象从“单个值”扩展成“日志条目序列”。每条日志是一个编号连续的条目,系统对每个日志下标分别运行 Paxos。这样做的好处是能够按顺序构建复制状态机。

但 Multi-Paxos 也有一个经典难题:日志空洞。如果 Leader 广播了日志条目 5,但只让少数派收到就崩了,下一条日志条目 6 可以由新 Leader 继续决策,但条目 5 可能永远无法被选举,因为已经没有节点还记得它。处理这个问题的标准手段是在接受条目时把“已接受”状态也持久化,并确保新 Leader 在接管前补全所有空洞。这个问题的复杂度,是 Paxos 工程实现“文档写得少、坑踩得多”的根源。

4. Raft:把共识协议做成能落地的工程

Raft 是 Diego Ongaro 在斯坦福的博士项目中提出的共识算法,目标非常明确:不追求理论上的极简,而是追求“可理解、可实现、可教学”。Raft 拆解了共识的几个子问题——Leader 选举、日志复制、安全性、成员变更——每个部分都用直观的机制解决,并且给出了可验证的正确性论证。

4.1 任期与三态角色

Raft 把时间划分成连续递增的 Term(任期),每个任期最多产生一个 Leader。节点只有三种状态:Follower、Candidate、Leader。

正常情况下,集群里有一个 Leader,其余全是 Follower。Follower 被动接收 Leader 的心跳(空的 AppendEntries),如果在一段时间内没有收到心跳,它就认为当前任期结束,转成 Candidate,开始发起选举。

Term 是 Raft 的“逻辑时钟”。每个节点本地保存当前任期,所有 RPC 消息里都携带任期号。收到更大的任期号时,节点立刻把自己降级成 Follower 并更新任期;收到更小的任期号时,直接拒绝该请求。这套机制保证了旧 Leader 的垃圾信息不会被新 Leader 的日志复制流程接受,是防止脑裂后老 Leader“残留”的关键。

4.2 选举:随机超时是点睛之笔

Candidate 发起选举时,给自己投票,向其他节点发送 RequestVote RPC,并带上自己日志的最后一条任期号和下标。收到请求的节点如果还没有给同任期的其他 Candidate 投票,并且对方的日志比自己新,就投给这个 Candidate。

这里的两个细节值得展开。第一个是“日志更新”判定:比较最后一条日志的任期号,任期号大者更新;任期号相同,下标大者更新。这个规定保证了新 Leader 一定拥有多数派里最完整的日志。第二个是随机选举超时:每个 Follower 的选举超时是 150ms 到 300ms 之间随机生成,时间到了才开始发起选举。随机性让多个 Candidate 同时发起选举的概率很低,谁先发谁赢,赢家进来后立刻广播心跳压制其他人,快速收敛。

我在调试这类协议时的体会是:随机超时区间不能拍脑袋。太小了,网络抖动一下就容易触发选举,Leader 频繁换届,日志复制一直被打断;太大了,Leader 故障后服务不可用的时间太长。业界比较常见的做法是在 200ms 到 500ms 之间,还要保证心跳间隔明显小于超时下限。

4.3 日志复制与提交规则

Leader 收到客户端的写请求后,把操作包装成日志条目,追加到自己的日志,然后并行向所有 Follower 发送 AppendEntries。Follower 收到条目,检查前一条日志的任期和下标是否匹配,匹配就追加,不匹配就拒绝,Leader 会逐步回退,找到双方匹配的位置再继续。

提交规则是 Raft 的一个隐蔽但必须理解的细节。Leader 只提交那些“被多数派节点持久化”的条目,并且提交后,这个条目之前的所有条目也就间接被提交了。但是,对于旧任期提交的条目,Raft 不允许 Leader 仅仅因为它被复制到多数派就直接提交,而是必须等到“当前任期的一条日志也被复制到多数派”之后,才能一起标记提交。

这个“间接提交”不是杞人忧天。如果旧任期的 Leader 把一条旧任期条目复制到了多数派,但随后就崩溃了,新任 Leader 可能根本不知道这条日志的存在(因为它的日志更旧)。如果新 Leader 直接认为这条日志已提交,就可能出现“新 Leader 用自己的值覆盖了实际上已经被部分节点持久化的旧值,而旧值本应被保留”的不一致。等一条当前任期的新日志被提交,就能保证所有节点都对齐到相同的历史,旧日志是否被提交也就清晰了。

4.4 安全性直觉:多数派交集

Raft 的安全性证明,本质上还是回到“任意两个多数派必然有交集”这个核心性质。

  • 选举限制:因为多数派交集,新 Leader 必然了解旧 Leader 提交的所有日志。
  • 日志匹配:Leader 只有在日志完全匹配时才接受复制,纠偏后得到一致日志。
  • 提交安全:因为新 Leader 的日志里包含旧提交版本,提交不会回退。
  • 领导者完整性:日志只能从 Leader 流向 Follower,Follower 永远不会删除或覆盖一个在多数派上存在的已提交日志。

这个“交集保证覆盖”的思维方式,是判断一个共识协议是否可靠的最快方法。看一个协议,先找它的 Quorum 定义,再看两个并发决策是否会选到不同的值,这正是 Paxos、Raft、Zab 共同的推理路径。

4.5 Raft 工程化常见的坑

Raft 相对 Paxos 好理解,但不代表实现容易。我在实际项目中踩过和见过的坑有这么几个,每个都值得多说一句。

第一个是磁盘同步。日志持久化必须fsync才能算“持久化”,否则内存里以为写成功了,一断电就丢。有些实现为了性能不做 fsync,结果故障恢复时日志不完整,直接破坏安全性质。这不是性能问题,是正确性问题。

第二个是选举超时配置。心跳网络偶尔抖动可能导致 Follower 超时发起选举,引起忙乱的选举风暴。给 Follower 增加“最近见过 Leader 心跳就不立即发起选举”的逻辑,或者把超时区间拉大,能显著降低无谓选举。

第三个是比较隐蔽的 ReadIndex 与线性一致读。如果 Leader 在某次分区中已经和多数派失联,但它还能收到客户端的读请求,它如果直接返回本地状态,就可能在旧日志上提供陈旧读取。安全做法是:Leader 每次执行读操作前,先从多数派确认自己仍然是 Leader,或者使用租约机制,否则读操作就不能保证线性一致。

第四个是成员变更。直接把集群从 3 个节点改为 5 个节点时,如果一次切换到位,旧配置和新配置的多数派可能没有交集,就会出现两个 Leader 同时存在的风险。Raft 的标准做法是单节点变更,逐个增删节点,保证任意时刻新旧配置的多数派有交集;或者用 Joint Consensus,先过渡到新旧配置同时生效的状态。

这些问题任意一个出纰漏,最终表现往往不是“算法不工作”,而是“故障恢复后状态错乱”,特别难排查。做伪复制的同学千万不要以为日志复制跑通就算完事了,容错路径才是关键。

5. Zab:ZooKeeper 的原子广播协议

Zab 的全称是 ZooKeeper Atomic Broadcast,是 ZooKeeper 里用来给所有事务排序和复制的协议。很多人会把 Zab 和 Raft 混在一起,其实它们虽然长得像,但设计的出发点有明显差异。

5.1 两个阶段:恢复与广播

Zab 把协议分成两个模式:恢复模式(Recovery)和广播模式(Broadcast)。

正常工作时,集群处于广播模式,有一个 Leader 负责接收客户端事务,给每个事务分配全局唯一的 ZXID(ZooKeeper Transaction ID),然后广播给 Follower。Follower 收到事务后按 ZXID 顺序写入本地日志,回复 ACK。Leader 收到多数派 ACK 后提交该事务,通知 Follower 可以提交应用。

ZXID 的构造是(epoch, counter)两个部分。epoch 对应 Leader 的任期,每次选出新 Leader 都会递增;counter 在同一任期内单调递增。这种结构让事务天然拥有全局有序编号,这正好满足 ZooKeeper 对会话和数据的全序要求。

当 Leader 故障或者网络分区导致 Follower 发起选举时,集群进入恢复模式。这个阶段要完成两件事:选出一个新 Leader,并让新 Leader 和其他 Follower 同步历史事务历史。Zab 的选举有一个特别规定:新 Leader 必须拥有“最高历史记录”,也就是它的日志必须包含所有之前可能被提交过的事务。

5.2 Zab 和 Raft 的差异到底在哪

两者从表面看高度相似:都有一个 Leader、都有任期概念、都要求多数派 ACK、都通过复制日志让副本状态一致。但几个关键差异决定了它们各自的性格。

第一个差异在提交规则。Raft 有那个“必须有一条当前任期日志被提交才能间接提交旧日志”的规定,而 Zab 在处理 Leader 切换时,新 Leader 会先向 Follower 同步完整的历史事务,确认没有遗漏后才进入广播模式。Zab 把旧事务的“追平”放在恢复模式里显式完成,而不是靠新日志的间接提交来隐式保证。

第二个差异在选举方式。Raft 使用随机超时+最新日志判定,Zab 则用一个更确定的“历史完整度”来选:拥有最高 (epoch, counter) 的节点优先成为 Leader。这减少了偶然性,但也要求选举阶段做更多的同步协调。

第三个差异在对外语义。Raft 的核心抽象是复制状态机,日志条目可以是任意命令;Zab 的核心抽象是原子广播,专门面向 ZooKeeper 需要的事务全序与 FIFO 会话语义。Zab 天然保证事务提交顺序和客户端看到的顺序一致,Raft 也可以做到,但需要额外约定。

5.3 为什么 ZooKeeper 没有直接用 Raft

这个问题的答案,一半是历史,一半是设计匹配度。Raft 是 2014 年前后发表的论文,而 Zab 在 ZooKeeper 的早期版本里就已经运作多年了。ZooKeeper 需要的是一个面向高吞吐、强有序广播的协议,Zab 的“epoch + counter + 恢复模式同步历史”结构正是为这种场景量身定做的。

更重要的是,Raft 的选举随机化虽然简单,但在 ZooKeeper 那种写密集、交互密集的场景里,频繁选举会带来额外的 ZXID 跳变。Zab 的确定性选主配合历史同步,更贴合 ZooKeeper 的“所有写操作都需要全局严格排序”的需求。不同系统选不同协议,不是谁比谁先进,而是看抽象是否匹配业务模型。

6. 脑裂问题:多数派不是银弹,但离开它更不行

现在聊标题里那个直击灵魂的问题:脑裂。前面所有算法都在为“防止脑裂”或者“脑裂后自动恢复”服务,这里我把脑裂的定义、场景、解法一次性说清楚。

6.1 一个典型的脑裂场景

想象一个 5 节点的集群,客户端通过负载均衡把请求打到任意一个节点上。某次网络交换机故障,集群被切成两组:节点 A、B 一组,节点 C、D、E 一组。旧 Leader 是 A,它在分组后仍然活着,还在接收客户端请求。

又因为网络隔离,C、D、E 收不到 A 的心跳,它们通过选举选出了新 Leader C。现在整个系统出现了两个 Leader:A 认为自己是合法 Leader,C 也认为自己是合法 Leader。如果两边都在接受写请求,就会产生两组不同的数据。等网络恢复,两个 Leader 的日志已经分叉,谁都不愿意放弃自己的数据,这就是脑裂的典型局面。

6.2 Quorum 是怎么挡住脑裂的

回到多数派原则:任何一个合法的决策者,必须能联系上超过半数的节点。

在上面这个例子中,5 节点集群的多数派是 3 个节点。A 所在的组只有 2 个节点,它无法联系到多数派,所以它没有资格继续当 Leader。C 所在的组有 3 个节点,达到了多数派,所以 C 合法当选。两组中必然只有一组能达到多数派,这就是 Quorum 防止并发选主的数学依据。

所以共识算法要求:一个 Leader 必须持续获得多数派的心跳反馈,一旦无法维持多数派,就必须放弃 Leader 身份,转入 Follower。这个“持续维持”非常重要,光靠“选举时获得多数派”还不够。旧 Leader A 在分区后的一小段时间里仍可能收到客户端请求,它必须通过心跳超时发现“我联系不上多数派了”,然后主动下野,这样才能避免继续写数据。

6.3 数据面脑裂:陈旧读、双写

很多系统的脑裂不止发生在 Leader 层面,还发生在数据访问路径上。

最常见的是“读任意节点”。如果客户端从任意副本读取数据,而读写之间没有强制走同一个 Quorum,那么客户端在分区后的少数派节点上,可能读到已经过期的数据。这个不叫“状态机不一致”,但用户感知到的是“我刚刚写进去了,怎么又读不到”,本质上也是脑裂的副作用。

更隐蔽的是“双写冲突”。两个客户端在分区两边同时修改同一个 key,如果系统没有版本号或者冲突检测,恢复后到底保留哪边,没有定论。这就是为什么很多系统引入版本向量、CAS、或者比较并交换来解决冲突。共识算法能保证日志不一致时收敛到同一个历史,但“业务数据的多版本冲突”需要业务层配合。

6.4 兜底手段:租约、fencing token、抢占锁

Quorum 是防止脑裂的第一道防线,但只靠它不够,因为“联系不上多数派”到“主动放弃领导权”之间存在时间窗口。这个窗口里,旧 Leader 还可能对共享资源做写入操作。工程上的兜底手段,我给你列几个实战中真正好用的。

第一种是租约(Lease)。Leader 每隔一段时间续租,续租成功才保持领导权。分区后,旧 Leader 无法续租,租约到期就必须认为自己失去领导权。租约时间要大于网络分区最坏恢复时间,否则会出现“新 Leader 上任了,旧 Leader 的租约还没到期”的重叠窗口。

第二种是 fencing token,也就是“带刺的令牌”。新 Leader 上任时,给自己发一个递增的令牌,后面每次写请求都携带这个令牌。存储服务端只接受令牌最新且递增的写入。旧 Leader 虽然可能还会发来写请求,但它持有的令牌已经过期,存储端直接拒绝。fencing token 比租约更硬性,因为它把拒绝动作给了数据面,而不是依赖 Leader 自己自觉下野。

第三种是抢占锁(Preemptive Lock)。把“谁是 Leader”映射到一把分布式锁上,获取锁者成为 Leader。但同时规定锁要定期续期,并且请求必须持有当前的锁版本。这本质上是 fencing token 的另一种实现方式。

第四种最简单粗暴:STONITH(Shoot The Other Node In The Head),也就是把可能出现脑裂时的那台旧主节点直接断电或隔离。数据库和存储系统里经常这么干,为了保证绝对安全,宁可停掉一个节点,也不让两个主节点同时运行。工程上这个方案仍然广泛存在,因为逻辑上最干净。

6.5 多数派的具体实现:仲裁节点与权重

最后补充一个容易被忽略的工程细节:多数派不一定是“节点数量的一半以上”,而是“投票权重的一半以上”。在某些部署里,不同节点的权重不同,比如跨机架部署时,机架内一组节点权重合计达不到整体 50%,无法自行选主;跨数据中心部署时,把权重分配给包含主数据中心的组,这样即使发生数据中心级别的分区,也只有主数据中心那组能达到多数派。

还有一种常见设计是引入仲裁节点,它不存储业务数据,只参与投票。这样可以在不复制数据的情况下提升选举的多数派阈值,防止“2 节点集群 + 1 个仲裁节点”那种 2 节点互相不通信时的脑裂死局。

我自己的经验是:多数派设计要结合部署拓扑一起考虑,别只看节点数。同一个 5 节点集群,如果 3 个节点在同一机架、2 个在另一个机架,那么机架分裂时两边都能形成“几何上看起来多数”的组,这时候权重配置没做好,脑裂风险就冒出来了。

7. 算法对比与选型建议

前面把每个算法都拆开了,这里拉一张表,方便你按需求快速定位。

7.1 核心特性对照表

维度2PCPaxosRaftZab
问题类型原子提交共识(复制日志)共识(复制日志)原子广播(全序事务)
核心机制两阶段投票提案编号+多数派任期+随机选举+日志匹配epoch+zxid+恢复模式
容错能力协调者故障即阻塞少数派故障可用少数派故障可用少数派故障可用
脑裂防护无内置机制多数派+提案编号多数派+任期+选举限制多数派+历史完整度
工程复杂度低(协议层面)高(理论级)中(可教学化)中(专用性强)
典型适用场景跨库事务、消息事务理论基石、少数内部系统通用 KV/注册中心/选主ZooKeeper 类强有序广播
学习难度低高中中

这个表格里的“工程复杂度”,我特别想强调一下:Paxos 不高不代表它不好,而是它的“要求研究者自己处理实现中的很多细节”,比如空洞、活锁、成员变更;Raft 把这些坑显式地写成了章节,所以实现起来反而规范得多。

7.2 什么场景选什么

如果你在做分布式事务,且要求强原子性,除了 2PC/3PC/TCC 之外,还要考虑这个事务能不能用幂等消息和补偿逻辑做成最终一致。能用最终一致就别碰同步事务,这是非常现实的经验。

如果你在做副本复制、状态机同步,默认选 Raft。理由很简单:资料多、实现参考多、社区成熟度高、排查工具完整。Raft 在可教学性上的优势,直接转化成了工程上的低成本和低风险。

如果你在做一个和 ZooKeeper 类似的“事务全序 + 会话有序”的系统,可以研究 Zab 的设计。但说实话,如果不是必须从头开发类似 ZooKeeper 的协调服务,直接用现成的 ZooKeeper 方案比重写 Zab 更划算。

Paxos 的价值在“理解共识的本质”。我会强烈建议所有想深入分布式方向的人认真啃一遍 Paxos,哪怕最后不自己实现它。因为 Paxos 把共识问题抽象得最干净,理解了 Paxos 的编号、Quorum、旧值保留规则之后,再看 Raft 和 Zab 会非常轻松。

8. 实操心得与常见问题速查

最后这部分,我把和企业里被问过最多的脑裂与一致性场景相关的实操心得和排查方法整理出来。这些内容不是教科书上的,是我实际调试系统时踩过的坑的总结。

8.1 几个必须记住的实操经验

第一个经验:日志和快照的落盘顺序很重要。日志条目必须先在本地持久化,然后才能发送给其他节点或者回复 ACK。如果先回复 ACK 再 fsync,节点崩溃后日志可能丢失,而其他节点已经以为这条日志被持久化了,后续恢复几乎必然出问题。

第二个经验:选举超时、心跳间隔、租约时长这三个参数,永远要配套调整。心跳是 Leader 保命用的,选举超时是 Follower 决定要不要造反用的,租约是决定“旧 Leader 什么时候有权被彻底剥夺”。三者要满足:心跳间隔 < 选举超时下限 < 租约时长。违反这个不等关系,就会出现老节点以为自己还是 Leader、新节点已经当选的情况。

第三个经验:观察一个集群是否健康,别只看“是否选出了 Leader”。要定期检查日志落后程度(Lag)、选举次数、心跳超时日志。如果一个集群每隔几分钟就发生一次选举,说明心跳或者网络有问题,不要等事故爆发才去查。

第四个经验:关于脑裂的恢复,宁可启动时“丢写”也不要“双写”。一个本来可以实现自动恢复的系统,如果为了可用性允许两边继续写,就可能留下不可逆的数据分叉。很多存储系统设计的底线是:分区时牺牲一小部分可用性,也比数据回不来强。

8.2 常见问题速查表

现象可能原因排查方向
集群频繁选举心跳间隔太短、网络抖动、磁盘 fsync 慢调整超时参数;检查网卡丢包率;看是否磁盘 IO 饱和
故障恢复后日志不一致旧节点没按协议持久化、没有正确拒绝旧任期请求检查 fsync 调用位置;检查任期比较逻辑;检查日志覆盖时是否误删已提交条目
客户端读到陈旧数据从非 Leader 或非 Quorum 节点读启用 ReadIndex 或只从 Leader 读;确认读路径经过共识
新 Leader 提交了旧 Leader 未提交的日志缺少当前任期日志间接提交规则补足 Raft 的提交限制;或者改用 Zab 的恢复模式显式同步
两个节点都认为自己是 Leader分区后旧节点未及时下野检查租约和 fencing token;缩短租约时间;确认多数派判断逻辑
日志空洞无法决策Multi-Paxos 没有补洞机制持久化已接受状态;新 Leader 接管前先扫描并补齐所有空洞

8.3 老实讲,我的一点个人体会

做了这么多年分布式相关的东西,我越来越觉得,共识算法的核心不是“懂不懂证明”,而是“在故障面前能不能保持冷静”。Paxos、Raft、Zab 的文档都写得很清楚,但真正让运维和开发头疼的,永远是故障叠加时那些协议边界之外的情况:磁盘慢、时钟漂移、网络瞬断、进程半卡半活。

我最想分享的一个小技巧是:在自己实现或者阅读这类算法时,永远准备一张纸,把“两个多数派会不会有交集”“旧 Leader 在分区后还能不能拿下三分之二同意”“一个日志条目在什么条件下算提交”这几个问题写在纸上,每个新特性加进来之前都拿这三个问题问一遍。这是我在排查一个非常隐蔽的脑裂问题时悟出来的方法,后来几乎成了我评估所有分布式协议的试金石。

这些问题想明白了,Raft、Paxos、Zab 对你来说就不再是几个需要背的名词,而是一套可以随时拿出来用的思考工具。分布式系统的故障永远躲不开,但至少这些算法给了我们一套在混乱中仍然能保持一致的规则,剩下的,就看你怎么在工程里用好它了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 5:30:21

【算法导论】Agglomerative Hierarchical Clustering(AHC)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/23 5:29:12

从一张照片推断拍摄地点:AI图像检索与地理定位实战

把一张随手拍的照片丢给AI&#xff0c;让它告诉你这是在哪拍的&#xff0c;这件事听起来像是电影里的桥段&#xff0c;但现在用开源工具加一点检索思路就能实现。我最早接触这个需求&#xff0c;是在帮朋友整理旅行相册的时候&#xff0c;几千张照片散落在各个设备里&#xff0…

作者头像 李华
网站建设 2026/10/23 5:28:43

从LLM到AI Agent:四大核心能力拆解与工程落地指南

1. 当“会聊天”变成“会干活”&#xff1a;LLM与Agent的分界线先聊一个很现实的问题&#xff1a;你身边一定有人把大模型用得飞起&#xff0c;写文案、改代码、查资料、翻译文档&#xff0c;感觉无所不能&#xff1b;但真要让它“自主完成一件带依赖关系的任务”——比如“帮我…

作者头像 李华
网站建设 2026/10/23 5:28:41

AIGC多样性增强:系统解决生成同质化的实战指南

做 AIGC 落地这件事也有不短时间了&#xff0c;我见过很多团队最开始都特别兴奋&#xff0c;觉得生成模型是“创意永动机”。但真把项目跑起来&#xff0c;画风很快就变成&#xff1a;让模型给新产品取 10 个名字&#xff0c;出来 8 个都带“智”、“享”、“云”&#xff1b;让…

作者头像 李华
网站建设 2026/10/23 5:28:22

多态深度解析:从底层原理到业务落地,彻底搞懂面向对象核心特性

多态&#xff0c;几乎是每个开发者都会挂在嘴边的词。我写了快十年业务代码&#xff0c;面试过不少候选人&#xff0c;也带过刚入行的新人&#xff0c;大家都能背出面向对象三大特性是“封装、继承、多态”&#xff0c;但真被问到“你手头这段代码哪里用到了多态&#xff0c;它…

作者头像 李华
网站建设 2026/10/23 5:27:25

基于JSP+Servlet+MySQL的网上订餐系统毕业设计完整方案

简介&#xff1a;这份资源是北方民族大学计算机科学与工程学院2014届学士学位论文《基于Web的网上订餐系统的设计与实现》&#xff0c;面向计算机相关专业的毕业设计者与网上订餐系统开发学习者。论文完整论述了网上订餐系统的分析、设计与实现过程&#xff0c;涵盖系统可行性分…

作者头像 李华