☰
分布式一致性算法精讲:Paxos、Raft、Zab与脑裂实战
2026/10/23 1:35:25 网站建设 项目流程

先从一个最实际的问题说起:你负责的系统,在经历了网络抖动、节点宕机、机房断电之后,数据还能不能保持一致?我问的不是"能不能恢复",而是"恢复之后大家手里的数据是不是同一个版本"。如果这个问题让你心里发虚,那分布式一致性算法就是你绕不过去的一课。

Paxos、Raft、Zab、2PC,这几个名词几乎出现在每一份分布式系统岗位的JD里,但真正能把它们讲清楚、用明白的人其实不多。它们到底解决什么问题?彼此之间是什么关系?为什么有了Raft大家还要提Zab?脑裂问题又是怎么回事?这篇文章我用实际项目的视角,把这块硬骨头嚼碎了讲。


1. 从2PC讲起:最直觉的方案,最致命的前提

1.1 两阶段提交到底在干嘛

2PC(Two-Phase Commit,两阶段提交)可能是所有分布式一致性协议里最容易被理解的一个。它的场景很具体:一个事务跨多个数据库节点执行,要么全部成功,要么全部回滚。比如一个订单系统,订单库在A机房,库存库在B机房,扣库存和建订单必须同时生效,这时候就需要一个协调者(Coordinator)出来主持大局。

整个流程分两个阶段。

第一阶段叫准备阶段(Prepare Phase),协调者给所有参与者发Prepare请求,问"你们能不能把这个事务准备好?"每个参与者收到请求后,执行事务但先不提交,把事务写进undo日志和redo日志,然后给协调者回复"我准备好了"或者"我不行"。这里的关键是,参与者一旦回复"准备好了",事务就已经处于可提交状态,只等协调者最后的命令。

第二阶段叫提交/回滚阶段(Commit/Abort Phase),协调者收集所有参与者的响应,如果全员回复"准备好了",就广播Commit命令,大家一起提交;只要有一个人说不行,或者超时没收到回复,就广播Abort命令,大家全部回滚。

听起来逻辑很完整,对吧?但问题恰恰出在这个"看起来完整"上面。

1.2 2PC的三个硬伤

第一个硬伤是同步阻塞。参与者从回复"准备好了"那一刻起,就要一直持有事务相关的资源锁,直到收到Commit或Abort命令。可协调者可能挂了,网络可能断了,参与者就只能傻等。这个等待可以长达数小时甚至更久,业务完全卡死。我一个做过银行核心系统迁移的朋友跟我说,他们最怕的就是2PC协调者宕机,锁住的账户数据能让整个网点业务瘫痪。

第二个硬伤是协调者单点。协调者本身也是分布式系统里的一个节点,它也可能宕机。如果协调者在第二阶段开始前挂了,所有参与者都在等着一个永远不可能到来的命令,事务状态就悬空了。有人会说,给协调者做高可用不就行了?可协调者一旦发生主备切换,新的协调者怎么知道之前的事务状态?答案是它不知道。这其实就是后面的Paxos、Raft要解决的核心问题之一——状态同步。

第三个硬伤,也是最隐蔽的,是脑裂风险。假设协调者给A节点发了Commit命令,但在给B节点发的时候网络分区了,B节点没收到Commit,它还以为事务失败要回滚。于是A保留数据,B丢掉数据,整个系统的一致性就此撕裂。2PC标准协议里并没有处理这种"部分提交"的机制。

也正因为这些硬伤,2PC在工程上更多是作为一种"单机多库"的解决方案,真正要做到跨机器、跨机房的强一致,后面的共识算法才是主角。


2. Paxos:理论上的"黄金标准",实践里的"天坑"

2.1 Paxos解决什么问题

Paxos解决的问题比2PC更底层:2PC需要一个活着的协调者,Paxos问的是——如果协调者本身可能挂掉,而且没有谁来代替它告诉别人"谁是协调者",我们还能不能达成一致?

答案是能,而且不需要任何中心节点做大脑。Paxos让所有节点通过一轮投票机制,在某个提议(Proposal)上达成共识。这个提议可以是"节点3当选为leader",可以是"事务T要提交",也可以是"这条数据的值是v1"。

Paxos里角色分三种:提议者(Proposer)、接受者(Acceptor)、学习者(Learner)。提议者负责发起提议,接受者负责投票并存储已接受的值,学习者负责学习最终确定的值。实际工程中一个节点往往同时扮演多个角色。

2.2 Basic Paxos的两个阶段

Basic Paxos就是把决策过程拆成Prepare和Accept两个阶段。

Prepare阶段,提议者生成一个编号M(这个编号是全序的,比如用时间戳加节点ID组合),把它发给大部分接受者,问"你们当前接受过的最大编号是多少?值是什么?"接受者收到Prepare请求后,会记住这个编号M,承诺不再接受编号小于M的Prepare请求,然后回复自己已接受的编号最大的提议。

Accept阶段,提议者收集到多数派的Prepare响应后,如果响应里没有带任何已接受的值,自己就随便选一个值带上;如果有带上某个值,就必须尊重它,把那个值作为自己的提议值。然后提议者发送Accept(M, value)给接受者,接受者如果发现M比之前承诺的编号大,就接受这个提议,否则拒绝。

这个过程最关键的设计是那个"必须尊重已存在的值"。它保证了如果已经有提议被多数派接受,后来的提议者哪怕发起新的一轮投票,最终被确定的值还是之前那个值。这就是Paxos正确性的来源。

2.3 Paxos为什么难用

理论讲完了,我不卖关子,直接说结论:Basic Paxos在真实工程里几乎没人直接用。为什么?首先是活锁问题。如果多个提议者同时发起投票,编号低的提议会被顶掉,然后它们再提高编号重来,很可能陷入"你顶我、我顶你"的无限循环。Lamport自己在论文里都调侃过,要让系统推进还得靠选出一个leader来仲裁。

其次是理解成本太高。这个算法大名鼎鼎,但真正能把Basic Paxos推导过程讲清的人少之又少。我自己就是翻了三四遍论文、画了无数时序图才真正敢说"我懂了"。但懂了跟能落地是两码事——论文里压根没提日志复制怎么做、状态怎么快照、成员变更怎么处理,这些工程细节全靠后人自己补。

所以你会发现,工业界绝大多数系统最终拥抱的不是Paxos,而是Raft。不是因为Raft更高效,而是因为它把人话讲明白了。


3. Raft:把"可理解性"当成第一设计目标

3.1 Raft是怎么诞生的

Raft的论文开篇就说了,Paxos虽然正确,但它极难理解,工程实现更是充满了坑。Raft作者的目标很明确:设计一个和Paxos正确性等价,但更容易理解、更容易实现的共识算法。这种"工程可落地性优先"的思路,让Raft在极短时间内成为分布式系统领域的事实标准——从etcd到Kubernetes,从Consul到TiKV,你手上大量关键基础设施的背后都是Raft。

Raft的核心手段还是多数派投票,但它在Paxos复杂的提议模型之上做了三件关键的简化:一是建立了严格的Leader机制,所有写入都必须经过Leader;二是把共识问题拆成三个相对独立的子问题,领导选举、日志复制、安全性;三是用随机超时和Term编号把问题化简到"谁拥有一段合法任期"就够了的程度。

3.2 领导选举与Term

Raft把时间切成一段一段的任期(Term),每个Term最多只能有一个Leader。每个节点起一个随机超时时间,比如150-300毫秒,如果在这个时间内没收到Leader的心跳,它就认为当前Term的Leader失效了,自己变成Candidate,并向其他节点发起选举投票。因为超时是随机的,通常只有一个节点先醒过来,发出投票请求,其他节点在超时重发前就会响应,于是这个节点顺利当选。

这里有个细节我觉得特别值得展开:随机超时的范围应该怎么设置?太接近了会导致频繁的选举冲突,太远了会让故障恢复变得迟缓。实践中一般用150-300ms,但要配合网络RTT来调整——在跨机房部署时,如果网络往返要80ms,那超时下限至少得翻倍,否则就会出现"领导人明明活着,只是因为网络延迟没及时收到心跳就被拉下马"的情况。我曾经在一个项目里把超时设置成和心跳周期同数量级,结果每隔几分钟就触发一次选举,整个集群的性能曲线跟心电图一样。后来把心跳间隔设为100ms、选举超时设为300-500ms之后,瞬间稳定下来。

3.3 日志复制与提交规则

Raft里每个节点都维护一条日志,日志项里包含用户指令和一个递增的日志索引。客户端写入请求到达Leader后,Leader把操作追加到自己的日志中,然后并行把日志条目发给所有Follower,当这个日志条目被复制到超过一半的节点上时,Leader就可以把它提交(Commit),并向客户端返回成功。

提交规则有一个关键点:不能直接提交之前任期遗留的未提交日志项。Raft的设计是,Leader必须在当前任期至少提交一条自己的日志,这样之前任期的日志才能跟着一起被间接提交。否则可能出现一种诡异情况:一个新Leader在提交日志之前挂了,旧日志又被另一个节点的旧日志覆盖掉。这是Raft正确性证明里最容易卡住的点,我建议你画出"同一索引、不同任期"的日志冲突图自己推一遍,体会会比看十篇文章都深。

3.4 Raft是怎么悄无声息解决脑裂的

Raft对脑裂的解决思路非常干脆:在一个任期内只允许一个Leader。就算因为网络分区出现了两个分区,每个分区各自选出了一个"Leader",也只有一个分区能满足"日志最新且获得多数派选票"这个条件。另一个孤岛分区的"Leader"发送日志时,永远凑不齐多数派确认,因此它提交不了任何日志。

这里有个很多新手会忽略的副产品:旧Leader不会自动知道自己已经不是Leader了。网络分区恢复前,旧Leader只要还没收到新任期的心跳,它就会继续尝试提交数据,但提交成功不了,因为这个提交动作需要过半节点确认。一旦分区恢复、新Leader的心跳传过来,旧Leader才如梦初醒,把自己降级成Follower。所以Raft解决脑裂不是靠"检测",而是靠"起不了作用的旧中心在系统里根本改不了状态"。


4. Zab:动物园的看守,比Raft更刁钻的一致性协议

4.1 Zab和Raft到底什么关系

Zab(ZooKeeper Atomic Broadcast)是ZooKeeper内部使用的原子广播协议。它的目标跟Raft非常像:在多个节点间复制一份全序的日志,保证所有节点按同样的顺序执行同样的操作。如果把Raft和Zab摆在一起看,你会发现它们有很多共通之处,比如都有Leader、都用Term/epoch编号、都需要多数派确认、都通过日志复制来同步状态。

但Zab比Raft早了几年设计成型,而且它的侧重点有所不同。Zab要解决的场景是"类状态机复制",也就是ZooKeeper里的Znode树结构操作。ZooKeeper要求全局严格有序,所有写请求都要排成一个全序队列,所以Zab特别强调广播消息的顺序唯一性——它用ZooKeeper特有的ZXID来编号,ZXID的高32位是纪元(epoch),低32位是事务序号。

4.2 Zab的广播与恢复

Zab的运行也分为两个阶段:广播阶段和恢复阶段。广播阶段里Leader收到客户端写请求后,给每个Follower发送Proposal,Follower把事务写进本地日志后回复ACK,Leader收到过半ACK就提交并告诉所有Follower提交。这一步和Raft日志复制几乎一模一样。

真正的差异在恢复阶段。Zab的选举一旦完成,新Leader必须确保自己拥有最新最全的历史日志,然后它要把这些数据同步给所有滞后的Follower,在同步完成之前不接受新事务。这个"先同步、再服务"的顺序保证,让Zab恢复后能严格延续旧Leader产生的所有事务顺序。Raft虽然也有类似逻辑,但Zab的顺序语义更强——它把"投递顺序"和"提交顺序"都锁死成一个单调递增的序列,每个节点执行写操作时都严格遵守这个序列。

我个人在实际使用ZooKeeper时感受最深的一点是:ZooKeeper对写性能的牺牲换来的是极致的一致性体验。它的顺序一致性让你几乎不用操心"先看到哪个后看到哪个"的问题,这在配置分发、分布式锁、元数据管理等场景尤其省心。代价是写吞吐量上限不高,扩容也只能扩大读能力,所有写还是要过Leader一条线。


5. 脑裂问题:每个分布式工程师都会遇上的深夜噩梦

5.1 什么是脑裂,什么情况会触发

脑裂(Split Brain)指的是分布式系统因网络分区,导致两个或多个节点组各自认为自己是"唯一的老大",并且各自为政,最终让系统状态产生分裂的场景。最经典的触发条件就两个:网络分区和节点假死。

网络分区很好理解:交换机故障、机房光缆被挖断,导致集群被切成互不相通的几块。节点假死则更隐蔽:某个节点其实活着,但GC停顿了几十秒,心跳没跟上,被大家认定"死了",于是集群选了新Leader,结果旧Leader缓过来,发现自己还拿着leader身份,开始处理请求。

脑裂为什么可怕?我讲一个自己遇到的真实故障。某个内部基于自研Raft协议搭建的配置中心,某次发布后一台机器发生了长达40秒的Full GC停顿,心跳断了,集群立刻选出新Leader。GC结束后,旧Leader恢复了,但它脑子里还记着自己才是主节点,写请求照接不误。它的所有写入都因为无法获得多数派确认而失败——这是Raft机制救了我们一命,但客户端的体验却是"服务连续写失败"。如果当时用的是没有合法性验证的哨兵方案,后果就是A区写A数据、B区写B数据,恢复后两边数据对不上,直接丢数据。

5.2 破解脑裂的常见方案

解决脑裂的思路有几个层次,核心是"禁止不可控的决策权"。

第一是Quorum机制,也就是多数派决策。任何决策(选举、提交、更新)都必须超过一半节点同意才算数。这个机制保证分区两侧不可能同时成功:假设集群有3个节点,切分成1和2两份,只有包含2个节点的那个分区能拿到多数派,另一个分区无论干什么都成不了事。很多算法里的"过半提交"就是基于这个原理。

第二是租约机制(Lease)。给Leader一个有效期限,比如每秒续租一次,期限到了还没续上就自动退位。这样即使旧Leader没收到新Leader的消息,它手里的"权力"也会自然过期,不能在无限期内乱指挥。这相当于给"谁是领导"加了一个硬性的物理时钟约束。

第三是fencing token,中文常叫栅栏令牌。每次选举出一个新Leader就生成一个单调递增的token,客户端操作携带这个token,旧Leader的token值变小,在存储层被直接拒绝。这是HDFS、Kafka等多套系统防脑裂的标准手段,它不阻止旧Leader发请求,而是在存储层拒绝旧Leader的请求,让"旧老大说了算"这个事实变得毫无意义。

5.3 Quorum怎么选:奇偶数不是拍脑袋定的

Quorum大小的选择有明确的权衡公式。假设集群总节点数为N,能容忍的故障节点数为F,那么必须要满足N >= 2F + 1。例如:

  • N=3时,F=1,可以容忍一个节点宕机
  • N=5时,F=2,可以容忍两个节点宕机
  • N=7时,F=3,可以容忍三个节点宕机

所以3节点集群容忍1个节点故障,5节点集群容忍2个,这个"1个、2个"不是随便拍的,而是数学推导出来的。节点越多容错能力越强,但通信开销和延迟也越大。实际部署中2PC之外的多数都是3节点起步,重要业务用5节点,很少见到7节点的常规集群,因为7节点的写入效率损失已经很明显了。


6. 四个协议放一起对比,选型逻辑就清晰了

6.1 四者的核心差异对比

不少初学者会把2PC、Paxos、Raft、Zab混在一起谈,以为它们是同一类东西。其实它们解决的问题维度不一样,纯做对比的话,2PC解决的是"多节点事务原子性",Paxos/Raft/Zab解决的是"多节点状态机一致性"。为了让你一眼看明白,我把关键差异列成一张表:

协议核心角色提交条件容错能力典型应用
2PC协调者+参与者全体参与者同意不处理节点崩溃,协调者是单点分布式事务、单机多库
Basic PaxosProposer/Acceptor/Learner多数派接受可容忍F个节点宕机,N >= 2F+1理论基石,集成于Chubby
RaftLeader/Follower/Candidate多数派确认可容忍F个节点宕机etcd、Kubernetes、TiKV
ZabLeader/Follower多数派确认可容忍F个节点宕机ZooKeeper

2PC的特点是"全员通过才能行",Quorum类算法是"多数通过就能行"。全员通过比多数通过的可用性差——一个节点挂掉就卡死整个事务,而多数通过只需要过半数节点活着就能继续服务。这也是为什么现代分布式数据库几乎清一色选择共识算法而不是两阶段提交。

6.2 什么时候选Raft,什么时候选Zab

如果让我给你一个直观的选型判断,我会这么说:你的业务系统需要内嵌一个分布式的、强一致的状态机,那直接考虑Raft。它有完整的开源实现,文档丰富,etcd本身就提供了经过大规模生产验证的Raft库,拿来改造成自己的存储引擎相当顺手。

如果你的服务依赖ZooKeeper集群做协调者的角色(比如分布式锁、元数据管理、服务发现),那你不用自己实现Zab,直接用ZooKeeper即可。Zab已经在上千家公司的生产环境里跑了十几年,它的稳定性和一致性细节远比绝大多数自研系统可靠。

至于Basic Paxos,我建议你把它当"必修课"去学,但不建议在生产里自己实现。它太灵活,坑太多。除非你是写论文或者做超低层基础设施,否则用Raft是更明智的选择。当年Google的Chubby用Paxos时Google自己都要吐槽实现难度,我们普通人没必要在泥潭里再走一遍。


7. 实操心得:从零搭建一个Raft集群时踩过的坑

7.1 节点参数配置参考

纸上谈兵这么久,最后给你一份可以直接抄作业的Raft集群配置参数,以etcd为例(其它Raft实现大同小异):

参数建议值说明
heartbeat-interval100ms心跳间隔,小于选举超时的1/3比较稳
election-timeout1000ms选举超时,一般定为心跳间隔的5-10倍
max-txn-ops128单次事务操作数上限,防止批量写入卡住日志复制
snapshot-count100000日志条目数达到此值时触发快照,避免日志无限增长
compaction-interval5000ms压缩间隔,配合快照清理过期日志
max-request-bytes1572864单请求上限,过大会拖慢复制并发

我给的这个配置不是最优解,万事不离业务场景。核心原则是:选举超时至少是心跳间隔的10倍,并且要能扛得住网络的毛刺。我曾经试过把心跳间隔弄到50ms、选举超时200ms,结果两台高负载的机器因为调度延迟触发了一连串无意义的选举,日志里全是"leader changed"告警,一看还是自己调的参数,血泪教训。

7.2 部署形态和故障演练建议

Raft集群部署最忌讳把节点都塞在同一个机房里。生产环境我建议至少是3节点分布在两个可用区(2+1),或者5节点分布在三个可用区(2+2+1)。为什么?因为如果3个节点全在一个机房,网络分区一发生,集群就彻底不可用了,等于没做容灾。

部署完之后的故障演练也特别重要。我建议每个季度做一次"宕机演练",具体操作包括:

  • 直接kill掉Leader节点,观察重新选举时间和写入恢复时间,理想情况是10秒内恢复
  • 用模拟工具制造单向网络分区(A能连B但B不能回A),观察是否有脑裂风险,旧Leader是否被正确降级
  • 给节点做重度CPU抢占,模拟GC停顿,验证选举超时是否够"抗噪"
  • 在以上每种故障期间同时发起写请求,确认是否存在"短暂拒绝写入"而非"错误返回成功"

如果你做过一轮这种演练,你就知道,真正决定一个分布式系统稳不稳的,往往不是算法本身,而是这些工程细节处理得好不好。

7.3 一个最容易被忽略的坑:时钟问题

Raft本身不依赖物理时钟做正确性判断,term和日志索引都是逻辑时钟。但lease机制里用到了物理时钟,还有Zab的epoch编号虽然不是物理时间但在工程实现中也依赖时钟来做续约判断。所以跨机房部署时,节点间的时间偏移(Clock Skew)如果超过一定阈值,租约续约就会出问题。

我在一个跨国部署项目里就遇到过:两个机房的时间差了5秒,结果A机房的Leader发给B机房Follower的心跳,B机房按本地时钟判断为"过期",导致无休止的选举。后来在每个节点上配置了NTP同步,并把时间偏移监控纳入了告警,才彻底压下去。这个坑不涉及算法本身,但绝对是工程实操中的必修课。


聊到这儿,四种一致性协议和脑裂问题的主体脉络已经讲得差不多了。我个人在实际项目中最深的一点体会是:不要迷信任何协议,当系统出现异常时,先检查你的网络、时钟和运维配置,再回头看算法实现。90%以上的"一致性故障",最后排查出来的根因都落在工程层,而不是算法层。Paxos、Raft、Zab、2PC,它们只是工具箱里的四个亿亿,真正的稳,靠的是对这套机制深入肌理的理解,以及一次次故障演练磨出来的手感。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询