ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ZAB与Raft对比:分布式共识协议核心机制与工程实践

ZAB与Raft对比:分布式共识协议核心机制与工程实践 1. 分布式系统的定海神针ZAB和Raft到底在解决什么问题如果你近期面过分布式相关的岗位或者正在上手ZooKeeper、etcd、Kafka这些组件大概率绕不开两个名字ZAB和Raft。这俩协议一直被拿来对比网上讲原理的文章也不少但很多要么直接贴论文翻译要么把源码分析砸到你脸上对小白来说跟天书一样。这篇我打算换个思路站在到底为啥要有这俩东西的角度把人话版本讲透并且附上我自己画的时序图顺便聊聊怎么用Wavedrom这种工具快速出图。先说最基础的问题一台机器上的数据同步压根不需要协议。写进去就是写进去了读出来就是读出来的。可一旦你上了三台机器、五台机器数据得在它们之间保持一致麻烦就来了——谁先写谁后写写一半机器宕了怎么办网络抖动导致两台机器认为自己都该当老大怎么办这些问题不解决你集群里每台机器的数据就是各说各话客户端访问哪台机器完全是开盲盒。ZAB和Raft本质上就是两套内部协调规则。它们做的事情高度一致从一堆机器里选出一个领导者由它统一接收写入请求再把数据变更同步给其他机器最后在超过半数机器确认之后才告诉客户端写入成功。这个流程保证了无论客户端连到哪台机器最终读到的数据是一致且按顺序的。那为什么搞出两套协议学计算机的都懂同一个问题很少只有一种解法。ZAB是ZooKeeper为了自己的类文件系统模型量身定做的名字里的Atomic Broadcast就说明一切——它要的是严格有序的广播。Raft则是etcd、Consul这些新一代组件选择的共识算法设计目标非常明确把论文里那些晦涩概念翻译成工程师能直接看懂、能动手实现的模块。一个是为场景定制一个是为可理解性而生这就注定了它们在细节上有不少分叉。这篇文章适合谁看刚接触分布式没多久想搞清楚ZooKeeper和etcd底层差异的开发者准备面试需要快速梳理ZAB和Raft脉络的求职者还有那些被论文里epochquorum这些词劝退只想用大白话搞懂核心机制的兄弟们。文末我会附上从时序图角度理解两个协议的实用技巧以及我实际画图时的一些经验保证你看完能直接上手。2. ZAB协议拆解ZooKeeper靠什么保证数据有序2.1 两个核心阶段消息广播与崩溃恢复ZAB全称ZooKeeper Atomic Broadcast直译就是ZooKeeper原子广播协议。注意原子这两个字意思是所有写操作要么在所有机器上生效要么全都不生效不存在中间状态。它整个生命周期被清清楚楚地切成了两个阶段消息广播阶段和崩溃恢复阶段。你可以把ZAB理解成一个班里收作业的流程。平时正常上课的日子是消息广播阶段——课代表Leader收齐作业登记在册然后挨个告诉组员Follower今天作业是第3页到第5页。某天课代表转学走了或者生病没来班级不能一直乱着于是进入崩溃恢复阶段——大家重新选一个新的课代表出来顺便把之前没收齐的作业本重新清点一遍。这两件事会反复交替发生但只要集群在正常工作大部分时间都处于广播阶段。这里有一个在理解ZAB时非常关键的视角它首先是一套恢复协议其次才是广播协议。为什么这么说因为ZooKeeper运行过程中崩溃随时可能发生而一旦崩溃之前广播到一半的那些事务到底算不算数、按什么顺序补发才是最难处理的部分。如果恢复逻辑设计得不够严谨轻则丢数据重则整个集群脑裂。所以ZAB论文里其实把很多篇幅花在了恢复阶段广播阶段反而显得平淡。2.2 消息广播阶段事务ID如何保证全局有序先看正常情况下的写入流程。客户端往任意一台ZooKeeper服务器发写请求如果这台服务器不是Leader它会把请求转发给Leader。Leader拿到请求后给这个事务编一个全局自增的ID叫ZXIDZooKeeper Transaction ID。这个ZXID不是简简单单的1、2、3它分成了两部分高32位是epoch纪元编号低32位是计数器。每次选出新Leaderepoch就加一计数器清零。这相当于给每个事务盖了一个第几届领导班子、任期内第几号令的戳。事务ID定下来之后Leader就开始广播了。流程如下Leader把事务封装成一个Proposal提案发给所有Follower。Follower收到Proposal后写本地事务日志然后给Leader回一个ACK表示我记下来了。Leader收到超过半数n/21Follower的ACK就认为可以提交了于是广播一条COMMIT消息。Follower收到COMMIT后正式把这条事务应用到内存数据上然后给客户端返回成功。这一套流程熟悉MySQL半同步复制的兄弟应该眼熟跟经典两阶段提交很像但ZAB做了个有意思的优化Follower不用把ACK发给所有其他Follower只要发给Leader一个人就够了。Leader自己算一份票数凑够过半就拍板。这个设计大大减少了网络交互的次数因为ACK收敛到Leader一处Leader再做最终决策不需要Follower之间互相商量。这里还有一个容易被忽略的细节在ZAB里事务提交顺序和Proposal发送顺序是完全一致的每个Follower收到的Proposal序列都是一样的。因为Leader会给每个连接维护一个发送队列按照ZXID从小到大的顺序依次发送不会出现乱序。客户端从任意一台Follower读数据时看到的顺序都是一致的这就是原子广播的意义所在。2.3 崩溃恢复阶段ZXID成为选票的试金石Leader挂了或者Leader和超过半数的Follower之间断网了集群瞬间进入无主状态。这时候Follower之间要重新选一个Leader出来同时还要确认之前那些广播了一半的事务到底哪些是有效的ZAB的答案很巧妙——谁的数据最新谁就更有资格当Leader。怎么判断数据最新看ZXID。每一台存活的机器在投票时都会带上自己已经提交过的最大ZXID。ZXID越大的机器说明它知道的事务越多数据越新。新Leader选出来之后它会把所有Follower的日志跟自己比对找出那些自己已经提交但Follower还没提交的事务主动补发给Follower同时把那些自己还没提交的事务直接丢弃。这样就保证了所有机器恢复到同一个状态。这个恢复过程里最关键的处理技巧在于新Leader必须在自己上任的第一时间广播一条NEWLEADER消息把所有现存事务的状态彻底定死然后才允许继续接收新的请求。不然可能出现这种情况——新的写请求已经进来了旧事务还没同步完顺序直接乱套。ZAB用先恢复、再广播这种强顺序保证把混乱扼杀在恢复阶段。我把崩溃恢复的流程画成了一张时序图Follower1 Follower2 Follower3 (原Leader已宕机) | | | |---通知原Leader失联---| | | | |---选票(ZXID0x1002)--| |---选票(ZXID0x1002)-| | |---选票(ZXID0x1001)--| | | | | Follower2获得多数票成为新Leader | | | |---NEWLEADER(0x1002)---| |---ACK----------------| |---COMMIT(已提交事务)---| | |---同步日志--| | | | | ~~~~~ 恢复完成开始对外服务 ~~~~~这个图里可以清楚看到Follower2拿到了多数票但它之后要做的第一件事不是接收新请求而是先把所有机器拉到同一个事务水位线上。这一步走完ZAB才进入正常的广播阶段。2.4 ZAB时序图的核心要素拆解前面两张图大家应该已经感受到了ZAB的所有关键机制用一张时序图就能讲清楚。我平时在做技术方案评审或者写复盘文档时经常把ZAB的广播流程画成下面这个样子Client Leader FollowerA FollowerB FollowerC | | | | | |--写请求-| | | | | |--Proposal(ZXID0x1001)--| | |--Proposal(ZXID0x1001)-----------| | |--Proposal(ZXID0x1001)--------------------| | | | | | | |--ACK-----| | | | |--ACK-----------------| | | | (已收到2个ACK加上自己共3个过半成功) | | |--COMMIT--| | | | |--COMMIT-------------| | | |--COMMIT-------------------------| | | | | | |--成功--| | | |画这张图时有几个要素需要特别注意。第一是Proposal和ACK之间的因果关系ACK一定是对应某个具体ZXID的所以图中要标清楚事务号。第二是COMMIT在ACK之后这是两个阶段的核心语义不能被压缩掉否则读者看不出两阶段的意思。第三是过半这个关键判断点最好用注释标出来否则很多人看完图还是不知道Leader到底等了什么。我在实际画图时习惯把事务ID、节点角色、阶段名称这类关键词直接写到箭头标签上而不是只画线不写字。因为时序图天然适合展示谁在什么时间给谁发了什么把这些信息填满读者不需要看正文就能还原整个过程。3. Raft协议拆解把共识算法工程化的典范3.1 Raft的三大子问题选举、日志复制、安全性Raft是斯坦福的Diego Ongaro和John Ousterhout在2014年提出的共识算法设计目标用一句话概括就是让分布式共识算法能被普通人理解。论文标题就叫《In Search of an Understandable Consensus Algorithm》通篇都在强调可理解性。它不像Paxos那样需要你脑补一堆希腊字母映射而是直接把问题拆成了三个相对独立的子问题领导选举、日志复制、安全性。这三个问题是怎么拆的Raft把整个系统的运行状态用一个任期Term的概念贯穿起来。每个任期从一次选举开始如果选出了有效Leader这个Leader就掌管整个任期的事务如果选举失败这个任期直接结束进入下一个任期重新选。任期用连续的整数编号并且随着选举的推进单调递增。这个设计让Raft的逻辑变得非常清晰——任何时候看日志只要看哪个任期内的哪条日志就够了简单得像给每一页文件盖了个日期戳。为什么说Raft比ZAB更适合小白理解因为它把复杂度老老实实地摊开在桌面上而不是藏在一套精致的抽象里。ZAB的核心概念是原子广播你得先理解广播和恢复两个阶段的切换逻辑Raft的核心概念是日志复制你把Leader当成一个日志分发器把Follower当成日志存储桶模型极度直观。这也是为什么很多新项目选型时更青睐Raft——不是因为它比ZAB强而是因为它好懂、好实现、好排查。3.2 领导选举随机超时如何避免选票撕裂Raft的选举机制可能是整个协议里最聪明的一小段设计。所有节点启动时都是Follower状态它们各自设置一个随机的选举超时时间通常是150到300毫秒之间。在这个时间内只要持续收到Leader的心跳Heartbeat就继续保持Follower状态如果超时还没收到心跳就认为Leader可能挂了于是把自己变成Candidate发起新一轮选举。为什么非得用随机超时因为如果所有节点的超时时间都一样它们很可能在同一时刻超时同时发起选举票数会被撕成好几份谁也拿不到多数只能一遍遍地重选。随机超时本质上是在给每个节点一个起跑时间差让某一次只有一个节点先跳出来竞选其他人因为没有它跑得快只能给它投票选举立刻收敛。我画了一张选举过程的时序图方便大家直观理解NodeA(Follower) NodeB(Follower) NodeC(Follower) | (随机等待140ms) | | |--- 心跳超时 -------| | | 变成Candidate | | |---RequestVote(Term2)--| | |---RequestVote(Term2)-----------------| | |---ReplyVote(yes)---| |--ReplyVote(yes)---| | | 获得2票BC成为Leader | |---AppendEntries(心跳,Term2)--| |---AppendEntries(心跳,Term2)----------------| | | | | ~~~~~ NodeA开始正常发送心跳选举结束 ~~~~~图中的关键点在于NodeA率先超时并发起投票NodeB和NodeC还没超时它们顺势把票投给了NodeA于是NodeA一次就拿到了多数。如果NodeB和NodeC也同时超时三个人各自投自己票数就是1:1:1谁也出不来只能拖到下一个任期。随机超时的意义现在就体现出来了——它极大降低了这种票数撕裂的概率。3.3 日志复制从客户端写入到状态机应用的完整链路选举出了Leader之后剩下的核心工作就是日志复制。Raft把客户端的每一个写操作都包装成一条日志条目条目里至少包含三样东西操作指令本身、任期号、日志索引。Leader接收客户端请求后先把它追加到自己的日志里然后并行地给所有Follower发送AppendEntries消息让它们把这条日志也追加到本地。日志追加到本地还不算提交。Leader只有在确认这条日志已经被超过半数的节点复制成功后才会把它应用到自己的状态机里也就是真正执行这个操作然后给客户端返回成功。下一次心跳时Leader会告诉大家上一条日志已经提交了你们可以应用了。Follower收到提交信息后再把日志应用到自己的状态机上完成整个链路。把Raft日志复制画成时序图是这样的Client Leader( S1 ) Follower( S2 ) Follower( S3 ) | | | | |--SET keyvalue--| | | | |-- Append local log ----| | | |--AppendEntries(Term3, idx7)--| | | |--AppendEntries(Term3, idx7)----------| | | |--Reply success----| | |--Reply success---| | | | (已收到2个成功加上自己共3个提交) | | |-- Apply to state machine ----| | | |--AppendEntries(心跳, 提交idx7)--| | | |--AppendEntries(心跳, 提交idx7)----------| | | |-- Apply to state machine -| | | |-- Apply to state machine -| |--OK---| | |这个图非常值得反复看。注意本机追加日志和发送AppendEntries之间其实有一段本地操作这经常在普通博客里被一笔带过但实际上是Raft日志复制的起点——Leader不先把自己的日志写下来后续所有ACK的判断都会失去参照。另一个细节是提交信息是搭着下一次心跳发出去的不是单独发一条COMMIT消息。这个设计很精巧它把确认信息合并到了周期性心跳里省掉了一轮网络开销。3.4 Raft安全性规则为什么旧Leader必须自我了断Raft里有几条安全规则我挑一条最典型的讲任期编号比较规则。当一条AppendEntries消息到达一个节点时如果消息里的任期号小于节点当前的任期号节点会直接拒绝这条消息。为什么因为任期号代表的是时代信息——你已经活在了新时代旧时代的Leader发的指令就该一律不认。更精妙的是如果一个节点发现消息里的任期号比自己大它会立刻把自己降级为Follower并且更新自己的任期号。这意味着即使一个旧Leader因为网络分区被隔离在另一边当网络恢复后它收到新Leader的心跳消息看到任期号比自己大就会乖乖变成Follower然后把自己的日志跟新Leader对齐。这个过程不需要额外的人工干预纯粹靠任期号的大小关系就能实现认怂可以说非常符合工程直觉。日志匹配规则也是Raft安全性的基石。Leader在每次AppendEntries消息里除了带上最新日志条目还会带上上一条日志的索引和任期号。Follower收到后先检查自己的日志里有没有这条前一条日志如果没有就拒绝这次追加。通过这种从后往前逐个对齐的方式Raft能保证所有节点上已提交的日志是完全一致的绝不产生分叉。4. ZAB与Raft的全面对比到底哪里像、哪里不像4.1 先看相同点为什么总被拿来一起说说实话ZAB和Raft的相同点远多于不同点。第一它们都采用Leader/Follower模式所有写请求必须经过Leader这个中心化设计决定了协议的骨架。第二它们都依赖多数派Quorum机制过半即可提交这个阈值保证了任意两个多数派集合必有交集从而不会出现两个同时有效的决策。第三它们都通过日志复制来实现状态同步Leader负责把日志分发下去Follower负责确认和落盘。第四它们都有明确的任期或纪元机制用递增的数字来标记时代更替从而区分旧时代领导者和新时代领导者。这些相同点不是巧合。它们本质上是在解决同一个分布式共识问题只是解法和表述方式不同。理解这些共性比背不同点更有价值——因为面试官问你ZAB和Raft有什么异同时真正想听的是你对共识问题有没有底层认知而不是你记住了多少条差异。4.2 核心差异恢复方式的柔性与刚性之别虽然大方向一样但在具体机制上两者有不少值得玩味的差异。差异一术语体系完全不同。ZAB里叫消息广播崩溃恢复ZXIDRaft里叫日志复制领导选举任期Term日志索引。这导致很多面试者在描述时经常把概念混淆比如把ZXID说成是Raft里的东西或者把epoch和term直接对等。其实它们的功能类似但设计细节各不相同混着说会暴露你对协议的理解不够体系化。差异二恢复阶段的行为差异。ZAB的恢复阶段新Leader在同步完日志之前不会对外提供任何服务Raft的Leader上任后也是先发送一轮空的AppendEntries来确认自己还没退位并且要等一条日志提交成功后才能真正开始对外服务。两者本质都是先恢复再服务但Raft更明确地把空日志提交作为Leader上任的前置条件这个细节在理解Raft时经常被忽略但它恰恰是Raft防止双Leader同时写的保险栓。差异三日志条目的表述方式。ZAB的事务日志基于ZXID排序ZXID是一个全局的、包含epoch和计数器的复合IDRaft的日志则用任期号日志索引二元组来定位。ZXID天然能表示全局顺序因为计数器是递增的Raft的日志索引则只在单个任期内有序跨任期后需要重新通过日志匹配来找到正确位置。从数据模型上看ZAB更像追加序列Raft更像按索引寻址的日志仓库。4.3 对比总表看完这张表至少能应付80%的面试题对比维度ZABRaft全称ZooKeeper Atomic BroadcastReplicated And Fault Tolerant算法名取自raft出处ZooKeeper的专用一致性协议2014年论文提出的通用共识算法角色划分Leader、FollowerLeader、Follower、Candidate核心机制原子广播 崩溃恢复领导选举 日志复制时代标识ZXID高32位epoch 低32位计数器Term单调递增的任期号日志定位ZXID全序排列Term, 日志索引二元组提交判定Leader收到过半ACKLeader收到过半追加成功选举策略依赖ZXID比较数据最新者优先依赖任期号和日志新旧比较服务恢复新Leader同步完日志后服务新Leader提交一条空日志后服务代表性实现ZooKeeperetcd、Consul、TiKV、SOFAJRaft适合场景强一致协调服务分布式锁、配置管理高可用存储系统键值存储、元数据存储这张表建议直接收藏。面试前扫一眼脑子里把ZAB靠ZXID排序Raft靠任期加索引定位这两条主记忆线拉出来基本就能应对大部分概念题了。但注意表格只能帮你建立框架真正的理解还是得靠前两章那些时序图背后的机制细节。4.4 选型建议ZooKeeper还是etcd实际工作中选型其实很少会有人因为协议不同来选择——但协议的特性会影响组件的行为表现最终影响你的系统设计。如果你在做一个强一致性的协调服务比如分布式锁、元数据管理、配置中心ZooKeeperZAB是老牌选择生态成熟文档多踩坑经验随处可查。ZooKeeper的读写模型更贴近小数据量、高一致性要求的场景它的会话Session机制、临时节点、Watcher通知这些特性在协调类业务里非常顺手。如果你在做一个高可用的键值存储系统etcdRaft可能更合适。etcd的API更现代watch机制更稳定运维复杂度相对低一些而且Raft协议的可理解性让团队的排查和学习成本都低不少。Kubernetes就选择etcd作为存储后端这也让它成了云原生生态里的事实标准。有一点得提醒协议本身不会直接决定性能上限真正决定性能的是日志写入方式、快照策略、批量聚合等工程实现。ZAB和Raft在工程实现上的差距远小于实现质量上的差距。一个调优得当的ZooKeeper集群可能比一个默认配置的etcd集群快很多。所以选型时别只盯协议要多关注团队熟悉度和生态是否满足需求。5. 时序图绘制实操用Wavedrom画出ZAB与Raft的关键流程5.1 为什么推荐Wavedrom以及它到底是什么标题里提到了时序图那就必须聊聊画时序图这个实操环节。我自己用过的时序图工具不少PlantUML、Mermaid、draw.io、Excalidraw都试过但遇到协议讲解这种场景时我越来越喜欢用Wavedrom。这东西原本是硬件工程师画数字电路时序的但它的JSON语法足够简洁用来画分布式协议的消息交互、角色状态切换反而异常好用。Wavedrom的核心思路是用代码描述时序随时生成图片。你写一段结构化JSON它就渲染成一张时序图。这意味着它可以被纳入Git管理、可以在文档里自动构建不用像draw.io那样手动拖拽画完了还不好维护版本。它有两种主流渲染方式在线编辑器Wavedrom.com和VS Code插件后者可以实时预览边写边看效率很高。我推荐它还有一个重要原因它生成的图是SVG矢量图放大不模糊直接贴进博客或者PPT里都很清晰。如果你的技术文档需要持续更新用Wavedrom维护时序图绝对比反复手工改图要好得多。这个工具对不熟悉JSON的读者也很友好语法规则不超过10个半小时就能上手。5.2 ZAB消息广播流程的Wavedrom实现我现在就用Wavedrom把ZAB的消息广播流程写出来。先把节点定义好ZAB有Leader和若干Follower我们用S1、S2、S3来代表三台机器。Wavedrom里要用信号数组来定义时序行每条信号可以是一条波形。不过这里有个限制Wavedrom的波形图不太适合复杂的消息箭头展示它的强项是高低电平、数据变化。所以实际画协议时序图时我更建议用它的数据信号功能来表现状态机变化同时配合文本标签来模拟消息交互。比如这样{ signal: [ { name: Leader状态, wave: x.3.4..., data: [空闲, 发送Proposal, 收集ACK] }, { name: Follower1日志, wave: x0.1.1..., data: [空, 记录ZXID0x1001, 已提交] }, { name: Follower2日志, wave: x0.1.1..., data: [空, 记录ZXID0x1001, 已提交] }, { name: Follower3日志, wave: x0.1.1..., data: [空, 记录ZXID0x1001, 已提交] } ]}渲染出来你会看到四条水平的状态轨道能直观看出不同角色在同一时间窗内所处阶段的对应关系。这种图适合放在文档里展示阶段对照——一眼就能看出Leader还在等ACK的时候Follower们已经写好了本地日志。但如果你更想表现消息的来回传递建议用Graphviz或者PlantUML的sequence diagram那边更合适。我自己常用的一个折中方案是正文里的示意图用PlantUML画时序图消息箭头清晰而在展示协议状态流转时用Wavedrom轨道对照清晰。两个工具各有不可替代的价值搭配着用才能讲明白一个复杂协议。5.3 PlantUML画法一张图还原Raft选举关键路径既然讲到消息交互我把Raft选举的PlantUML代码也分享出来。这是我在写内部技术文档时常用的模板注释里会标注每个步骤的逻辑意图startuml participant NodeA (Follower) as A participant NodeB (Follower) as B participant NodeC (Follower) as C A - A : 随机超时140ms未收到心跳 activate A A - A : 任期Term2成为Candidate A - B : RequestVote(Term2) A - C : RequestVote(Term2) B -- A : Vote(Term2, grantedtrue) C -- A : Vote(Term2, grantedtrue) A - A : 收到2票成为Leader A - B : AppendEntries(heartbeat, Term2) A - C : AppendEntries(heartbeat, Term2) deactivate A enduml这段代码生成图之后重点信息全部靠消息名称本身承载Term2说明这是一次新任期RequestVote和Vote表达了请求和响应的逻辑关系grantedtrue则标记了投票结果。在真实项目里我会再顺手把随机超时范围写成注释方便别人理解为什么选举能快速收敛。5.4 画好协议时序图的三个实战经验经验一别把图画得太满。一张图如果塞进了选举、日志复制、提交三个过程看起来像天书根本起不到解释作用。我一般坚持一图一事原则一张图只讲一个流程比如ZAB广播流程Raft选举流程Raft日志复制流程各画各的。需要展示全流程时用编号给步骤加注而不是堆在一条线上。经验二消息名称要带上关键参数。光写RequestVote还不够加上Term2和候选人信息读者才知道这是哪一轮的投票。光写ACK也不行要写ACK(ZXID0x1001)才能让人知道是回应哪条事务的。好的时序图应该能做到——不看正文只看箭头上的字就能还原出一个完整的故事。经验三把节点内部的本地操作画成自环指向自身或者半透明的说明框。很多人画时序图只画跨节点消息忽略了本地的日志追加、状态变更等关键动作。实际上这些本地操作往往是理解日志复制、提交时机的钥匙缺了它们图会显得跳步。我现在画图时习惯把Leader写本地日志Follower落盘Leader应用状态机这些关键本地动作都明确画出来瞬间图的信息量就上来了。6. 常见问题与面试考点别再让这些细节坑你6.1 高频疑问与快速解答问ZAB和Raft哪位更强——没有谁比谁强只有谁比谁更适合场景。ZAB从ZooKeeper的特定需求里生长出来深度绑定会话、节点等概念Raft则是刻意追求可理解性的通用算法。真要论工程影响两者都是分布式领域的里程碑。问Raft的Candidate状态是不是多余的——不多余。Candidate是Follower变成Leader的中间态它让发起选举这个动作有了明确的身份标识避免了一个节点既是Follower又是竞选者的尴尬。ZAB没有这个中间态但ZAB的设计目标不同不需要拆分这么细。问半数以上节点宕机了怎么办——整个集群进入只读或不可用状态。ZAB和Raft都无法在多数派不可用时继续写入这是共识算法的天花板不是实现缺陷。实际高可用方案里3节点集群允许挂1台5节点集群允许挂2台想挂更多就得加节点或换架构。问分布式锁用ZooKeeper还是etcd——两者都行。ZooKeeper有临时顺序节点etcd有Lease和Revision都能实现分布式锁。选型更多看团队成员熟悉度、运维成本和周边生态。很多团队选etcd单纯因为K8s已经在用它省去再维护一套ZooKeeper的精力。6.2 面试官最爱追问的三个魔鬼细节一Raft选区时为什么要求Candidate的日志至少不落后原因在于日志的向前兼容原则如果让日志落后的节点当上了Leader它就可能覆盖掉其他节点上已经提交的日志造成数据回滚。Raft通过比较日志的最后一条索引和任期号保证了选出来的Leader拥有最新的日志从根本上杜绝了回滚。二ZAB的事务提交在过半ACK之后就立刻返回成功那Follower会不会延迟收到COMMIT会。ZAB的Leader在收到过半ACK后马上给客户端回包但此时部分Follower确实可能还没执行提交。后续Leader会通过心跳或后续Proposal携带提交信息让所有Follower最终一致。这里有一个trade-off为了减少延迟牺牲了所有节点同步完成再返回的强同步换来的是最终一致但顺序确定的承诺。三Raft共识别里空日志提交到底有什么作用它是在新Leader上任时通过提交一条不包含任何指令的日志来证明自己仍然拥有多数派支持。如果这条空日志都无法复制成功说明Leader可能已经失去多数派地位系统会自动触发新一轮选举从而避免一个没实权的Leader还在硬撑的局面。这个机制是Raft安全性里一个很重要但很不起眼的保险丝理解了它你对Raft的理解直接上一个台阶。6.3 实操心得我踩过的坑和总结的避坑经验第一个坑把ZXID和Term当成同一个东西。这俩确实都扮演时代编号的角色但ZXID是复合结构由epoch和计数器组成同时承担选举依据和事务排序两个功能Term则只负责标识任期识别新一代Leader的合法性。你在画图、写文档的时候把这两个概念混用读者会一头雾水。第二个坑以为Follower不参与决策。在ZAB和Raft里Follower绝不只是被动接收的角色。ZAB里Follower要发ACK来确认事务Raft里Follower要回复AppendEntries的成功与否还要发起投票。它们是决策链路里实实在在的一环只是没有最终拍板权而已。如果写分析文章时只画Leader的动作会导致读者误以为是单机写、单机读的逻辑。第三个坑在讲解时忽略消息的前提条件。比如Raft里AppendEntries要求前一条日志必须匹配ZAB里Follower要先写本地日志再回ACK这些前提条件才是保证一致性的关键。画时序图时我习惯把前提条件在代码注释或者图上用虚线标出不然图看起来就像发个消息、回个ACK这么简单完全丢失了协议设计的精髓。如果你现在正准备面试或写技术方案我建议你亲自动手把ZAB的广播和Raft的选举各自画一遍时序图。你会发现脑子里觉得已经懂了的逻辑落到图上能暴露出一堆模糊地带——比如ACK是发给谁的提交是独立消息还是心跳带的投票是发完就等吗这些都是理解深不深的试金石。画完之后再回来读论文你会发现原来那些黑话其实都在讲你已经理解的东西只是把画家的话翻译成了论文语言而已。最后分享一个我个人的小习惯无论是ZAB还是Raft我都会把这个协议最怕什么记在笔记本首页。ZAB最怕的是恢复阶段和广播阶段的切换顺序出错Raft最怕的是任期号比较时出现错乱。把这个最怕想透协议的其他部分都会乖乖归位。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进