先从一个最实际的问题说起你负责的系统在经历了网络抖动、节点宕机、机房断电之后数据还能不能保持一致我问的不是能不能恢复而是恢复之后大家手里的数据是不是同一个版本。如果这个问题让你心里发虚那分布式一致性算法就是你绕不过去的一课。Paxos、Raft、Zab、2PC这几个名词几乎出现在每一份分布式系统岗位的JD里但真正能把它们讲清楚、用明白的人其实不多。它们到底解决什么问题彼此之间是什么关系为什么有了Raft大家还要提Zab脑裂问题又是怎么回事这篇文章我用实际项目的视角把这块硬骨头嚼碎了讲。1. 从2PC讲起最直觉的方案最致命的前提1.1 两阶段提交到底在干嘛2PCTwo-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 领导选举与TermRaft把时间切成一段一段的任期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到底什么关系ZabZooKeeper 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发送ProposalFollower把事务写进本地日志后回复ACKLeader收到过半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。例如N3时F1可以容忍一个节点宕机N5时F2可以容忍两个节点宕机N7时F3可以容忍三个节点宕机所以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 2F1理论基石集成于ChubbyRaftLeader/Follower/Candidate多数派确认可容忍F个节点宕机etcd、Kubernetes、TiKVZabLeader/Follower多数派确认可容忍F个节点宕机ZooKeeper2PC的特点是全员通过才能行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节点分布在两个可用区21或者5节点分布在三个可用区221。为什么因为如果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它们只是工具箱里的四个亿亿真正的稳靠的是对这套机制深入肌理的理解以及一次次故障演练磨出来的手感。