共识算法 ZAB
ZAB 的作者说 ZAB 不是 Paxos,但后面我们又把 ZAB 归纳为 Paxos。我认为这两个说法都对,只是他们描述的时间不一致。在 ZAB诞生时,它解决了 Paxos 不能保证顺序执行的问题,从某些角度来说 ZAB 是要比 Paxos 优秀的,说它不是 Paxos 也没问题。但是后来随来越来越多分布式算法诞生,例如 Raft,因为他们都类似 Paxos 执行逻辑,所以将这类算法归纳为 Paxos 的变种。
ZAB 协议为 ZooKeeper 中使用的共识算法。
为啥不使用 Paxos 实现 ZK
回到 ZAB 诞生的原因,不直接使用 Paxos 作为 ZooKeeper 的共识算法,是因为 Paxos 存在的局限:
- 提议者间竞争可能导致活锁
- 每次客户端的请求都要求两轮的 RPC 远程调用
- 日志复制完整性(日志对齐)问题
- 配置成员变更,节点崩溃恢复问题
除此之外,Paxos 只适合在集群中使某个值达成共识,而不关心达成共识的值是什么。而在 ZooKeeper 中,业务需求是要保证提案之间的因果关系 (顺序)。
🌰 A 服务器收到请求,先后创建 /foo, /foo/ofcoder 两个 Znode 节点。B 服务器收到请求先后创建 /method,/method/far 两个 Znode 节点。
第一个值的 Paxos 过程中:
- B 先发起 /method 提议,在 prepare 阶段获得大多数选票,并开始 accept 阶段。
- A 后发起 /foo 提议,在 prepare 阶段发现有结点接受了值 /method,则 A 会在 accept 阶段放弃原有的提议值改提议 /method。
之后开始第二个值的 Paxos 过程,但 A 要创建 /foo/ofcoder 时,会发现 /foo 没有创建而导致失败。
ZAB 相关术语
Paxos 是一种通用的分布式共识算法,而 ZAB 不同,它是一种专门为 ZooKeeper 设计的崩溃可恢复的原子广播协议 (Zookeeper Atomic Broadcast)。
相比于 Paxos,ZAB主要解决了事务操作的顺序性,在 ZAB 协议中,如果一个事务操作被处理了,那么所有其依赖的事务操作都应该被提前处理。
在学习 ZAB 之前,我们需要先整理几个术语、因为在 ZAB 的论文中,术语相对比较多,并且概念冗余。例如:
- 提案(proposal):进行协商的基本单元,在一些文档中,也有称之为操作(operation)、指令(command)。
- 事务(transaction):也是指提案,常出现在代码中,并非指具有 ACID 特性的一组操作。
- 已提出的 Proposal:指广播的第一阶段所提出的 Proposal,未提交到状态机的 Proposal。
- 已提交的 Proposal:指广播的第二阶段已提交 (Commmit) 到状态机的 Proposal。
ZAB 定义了三个角色、四种节点状态、四种 ZAB 运行状态、以及两种运行模式。
三个角色
领导者(Leader):整个 ZAB 协议的核心。
- 接收并处理所有事务请求,也即写请求。将每个事务请求,封装成提案(proposal)广播给每个跟随者(Follower),根据跟随者(Follower)返回请求,控制是否需要提交该提案。
跟随者(Follower)
- 接收 Leader 提出的提案(proposal),参与对提案(proposal)的投票;
- 接收并处理非事务请求,也就是读请求。如果 Follower 收到客户端的事务请求,则会将其转发给 Leader 进行处理;
- 参与 Leader 选举投票。
观察者(Observer)
- 跟 Paxos 中学习者(Learner)类似,增加 Observer,可以在不影响集群写性能的情况下,扩展读请求,或者跨区域之间读操作。
四种节点状态
虽然 ZAB 规定了三种角色,但它是通过定义四种状态来描述当前节点所处的角色的。包含以下状态:
- LOOKING:竞选状态,当前集群不存在 Leader。该状态下会发起领导者选举。
- LEADING:领导者状态,对应 Leader 角色。
- FOLLOWING:随从状态,同步 Leader 状态,参与投票。
- OBSERVING:观察状态,同步 Leader 状态,不参与投票。
这里与角色对应多出来一个状态,是因为 ZAB 是支持自动 Leader 选举的,LOOKING 是属于选举中的一个过渡状态。
四种 ZAB 运行状态
指 ZAB 集群的运行状态,因为 ZAB 除了正常向外部提供服务,还得有故障恢复功能。通过集群的状态可了解 ZAB 的运行过程。
- ELECTION:选举状态,表明节点正在进行 Leader 选举。
- DISCOVERY:成员发现状态,在选举出新 Leader 后集群所处的状态,用于节点协商沟通 Leader 的合法性。
- SYNCHRONIZATION:数据同步状态,在确认新 Leader 后,以 Leader 的数据为基础,修复各个节点的数据一致性。
- BROADCARST:广播状态,集群处于正常运行状态,可向外提供服务。
两种运行模式
从上述 ZAB 运行状态中,可以归纳为两种运行模式,即消息广播模式、崩溃恢复模式。
- 消息广播模式:
若集群中已有过半的 Follower 与 Leader 完成数据同步,那么整个集群就会进入消息广播模式。此时整个集群才会对外提供服务,即数据的查询、修改。 - 崩溃恢复模式:
在整个服务框架启动过程中、或者 Leader 服务器出现网络中断、崩溃退出等异常情况时,ZAB 协议就会进入崩溃恢复模式并选举新的 Leader 服务器。当新的 Leader 服务器在集群中有过半的 Follower 与其完成成数据同步后,ZAB 就会退出崩溃恢复模式。
❗️当一台新的 ZAB 节点加入集群时,该节点会先进入崩溃恢复模式,找到 Leader,并与其进行数据同步,然后一起参与到消息广播流程中。所以崩溃恢复模式还分为两个阶段:发现、同步。具体后文会详细讲解。
后文讲解思路也是从这两种模式入手,在崩溃恢复模式中,再细分为三个阶段,也就是四种运行状态的前三种(ELECTION、DISCOVERY、SYNCHRONIZATION)。
实物标识符 zxid
Leader 在收到事务请求,将其封装成 Proposal 时,会为每个 Proposal 生成对应的 zxid。
在消息广播模式中 zxid 标志着事务请求的先后顺序(保证 Proposal 的因果关系);在崩溃恢复模式中 zxid 是 Leader 的选举的判断依据;以及在 Leader 选举后,数据同步中 zxid 能方便的帮助 ZAB 抛弃上一个 Leader 没完成的 Proposal。
zxid 是一个 64 位数字,低 32 位是个简单的计数器,高32 位表示 Leader 周期的 epoch编号。后文中使用 <epoch, counter>
标示一个 zxid,如<1, 101>。
- epoch:标识当前集群所处的周期,或者说当前 Leader 的周期。在每一次 Leader 变更后,新 Leader 会在上一任 Leader 的 epoch 上加 1,作为自己的 epoch。
- 计数器:标识客户端的事务请求,Leader在每产生新的 Proposal 事务时,都会将计数器加 1。Leader 变更后,该计数器会重置为 0。
这样做的好处:
- 计数器可以定义 Proposal 的先后顺序,保证发送提交事务消息的广播顺序。
- epoch + 计数器,能有效的避免 zxid 的冲突,不会出现 Leader 使用了相同编号的 zxid 但提出了不同的 Proposal 的情况。
- 能随时获取到最新的 Leader 周期(epoch),当 Leader 收到在网络故障后,收到比他大的 epoch 的 Proposal,则证明集群中已有其他Leader,自己则变更为 Follower。
- 新 Leader 产生的 zxid 一定比老 Leader 产生 zxid 大。当老 Leader 宕机恢复后(以 Follower 角色)加入集群,如果有尚未提交的事务,则可以对比 zxid 进行抛弃(回退)那一些 Proposal,直到回退到一个确实已经被集群中过半机器 Commit 的最新 Proposal。
第3, 4点如果现在看不明白,在讲述崩溃恢复模式时,我会回过头来再讲讲的。
消息广播模式
消息广播模式是一个类似于二阶段提交(2PC)过程,针对客户端事务请求,Leader 将其生成对应的 Proposal,并发给所有的 Follower,收集各自的选票后,最后进行事务提交。与 2PC 不同的是,(1) 2PC 中协调者需要等待所有参与者的全部反馈,ZAB 中 Leader 只需等待大多数 Follower 的 ACK 响应;(2) ZAB 移除了第二阶段的中断逻辑 (事务回滚),不能终止事务。所有的 Follower 要么接收该 Proposal,要么抛弃 Leader 服务器。这意味着 Leader 收到过半的 Ack 响应后就可以提交该事务了,而不需要等待所有的 Follower 都返回 ACK。

- 客户端发起事务请求,由 Leader 进行处理
- Leader 将该请求转换为事务 Proposal,同时为 Proposal 分配一个全局的 ID,即 zxid
- Leader为每个 Follower 维护一个 FIFO 队列,将上一步生成的 Proposal 放入队列中,根据 FIFO 策略进行广播
- Follower 收到 Proposal 后,会首先将其以事务日志的方式写入本地磁盘中,写入成功后向 Leader 反馈一个 ACK 响应
- Leader 收到过半的 ACK 响应后,自己完成对该 Proposal 的 Commit,并向每个 Follower 的队列中写入 Commit 消息进行广播
- Follower 接收到 Commit 消息后,会将上一条事务提交
Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往队列中发消息即可。如果使用同步的方式会引起阻塞,性能要下降很多。
如何保证事务执行的顺序
ZAB通过 zxid 中的计数器来保证提交顺序,具体如下:
在 Leader 收到客户端set X、set Y
两个请求后,会将其封装成两个Proposal(<1, 101>: X, <1, 102>: Y)进行广播所有的 Follower。
当 Leader 收到过半的 ACK 响应后,则会进行 Commit 消息的广播。这里需要注意,Leader 提交提案是有顺序性的,按照 zxid 的大小,按顺序提交提案,如果前一个提案未提交,此时是不会提交后一个提案的。因此X一定在Y之前提交。
最后,Leader返回执行成功响应给客户端。完成本次消息广播。
崩溃恢复模式
ZAB是一个强领导者模型的协议。消息广播模式只能在 ZAB 正常运行中向外部提供服务。这也要求 ZAB 设计者不得不考虑,当 Leader 宕机或者失去过半的 Follower 节点后,如何恢复整个集群。
为了更好理解崩溃恢复模式原理,通常会把他分为两个阶段或者三个阶段,即 Leader 选举、Leader 发现、数据同步。
基本约定
在选举新的 Leader 后,向外部提供服务之前,ZAB 还需要保证数据正确性,即上一个 Leader 崩溃之时,正在处理的事务请求,可能会出现两个数据不一致的隐患。针对这样情况,ZAB 保证一下特性:
1️⃣ ZAB 需要确保那些已经在 Leader 上提交的事务最终被所有服务器都提交。
出现场景:当 Leader 收到合法数量 follower 的 ACKs 后,就向各个 Follower 广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端返回「成功」。但是如果在各个 Follower 在收到 COMMIT 命令前 Leader 就挂了,导致剩下的服务器并没有执行都这条消息。
- 选择拥有 proposal 最大值(即 zxid 最大) 的节点作为新的 Leader。由于所有提案被 COMMIT 之前必须有大多数量的 Follower ACK,即大多数服务器已经将该 proposal 写入日志文件。因此,新选出的Leader 如果满足是大多数节点中 proposal 最多的,它就必然存有所有被 COMMIT 消息的 proposal。
- 新 Leader 与 Follower 建立先进先出的队列, 先将自身有而 Follower 缺失的 proposal 发送给它,再将这些 proposal 的 COMMIT 命令发送给 Follower,这便保证了所有的 Follower 都保存了所有的 proposal、所有的 Follower 都处理了所有的消息。
2️⃣ ZAB 需要确保丢弃那些仅仅只在 Leader 上被提出的事务。
出现场景:当 Leader 接收到消息请求生成 proposal 后就挂了,其他 Follower 并没有收到此 proposal,因此经过恢复模式重新选了 Leader 后,这条消息是被跳过的。 此时,之前挂了的 Leader 重新启动并注册成了 Follower,他保留了被跳过消息的 proposal 状态,与整个系统的状态是不一致的,需要将其删除。
Zab 通过巧妙的设计 zxid 来实现这一目的。一个 zxid 是64位,高 32 是纪元(epoch)编号,每经过一次 Leader选举产生一个新的 Leader,其epoch 号 +1。低 32 位是消息计数器,每接收到一条消息这个值 +1,新Leader 选举后这个值重置为 0。
这样设计的目的是即使旧的 Leader 挂了后重启,它也不会被选举为 Leader,因为此时它的 zxid 肯定小于当前的新 Leader。另外,当旧的Leader 作为 Follower 提供服务,新的 Leader 也会让它将所有多余未被 COMMIT 的 proposal 清除。
领导者选举 (ELECTION)
选举阶段在 ZAB 协议论文中没有具体描述,可用任何算法实现。此处介绍的是 ZooKeeper 中 ZAB 关于领导者选举的具体实现。
选主时机:
- 节点启动时:每个节点启动的时候状态都是 LOOKING。
- Leader 节点异常:Leader 节点运行后会周期性地向 Follower 发送心跳信息(称之为 ping),如果一个 Follower 未收到 Leader 节点的心跳信息,Follower 节点的状态会从 FOLLOWING 转变为 LOOKING。
- 多数 Follower 节点异常:Leader 节点也会检测 Follower 节点的状态,如果多数 Follower 节点不再响应 Leader 节点(可能是Leader节点与Follower节点之间产生了网络分区),那么 Leader 节点可能此时也不再是合法的 Leader ,也必须要进行一次新的选主。
选主流程:
- 各节点第一轮都会推荐自己为 Leader,想所有节点广播自己的提议。
选票内容为 <下一个 epoch, LOOKING, 投票者 myid, 投票者 zxid, 选择的Leader myid, 选择的 Leader zxid> - 各节点收到其他节点提议的 Leader 后,与自己所提议的 Leader 进行比较,根据比较的规则重新选择提议的 Leader 并广播,直到有过半的节点都提议某一节点,即结束 Leader 选举。Leader 节点状态转为 LEADING,其他结点状态转为 FOLLOWING。
Leader 选举的比较规则:
- 任期编号 (epoch):epoch 大的当选 Leader。
- 事务标识符 (zxid):可理解为事务 id,由 Leader 为每个 Proposal 生成,全局唯一。epoch 相同时,zxid 大的当选 Leader。
- myid:服务器 id。在安装 ZooKeeper 时配置。zxid 相同时,myid 大的当选 Leader。
ZAB 算法中通过 myid 来规避多个节点可能有相同 zxid 问题,🆚 Raft 算法中通过随机的 timeout 来规避多个节点可能同时成为 Leader 的问题。
成员发现 (DISCOVERY)
该阶段用于确立 Leader 的领导关系,继上一阶段,也就是 ELECTION 完成后,每个节点都有自己所保存的选票池,当选池中有过半的选票都提议同一节点为 Leader 时,则进入成员发现(DISCOVERY)状态。
- Follower 会主动联系准 Leader,并将自己最后接受的事务 Proposal 的 epoch 值发送给准 Leader,这里记作 FOLLOWERINFO。
- 准 Leader 收到来自过半(包含节点自己)的 FOLLOWERINFO 消息后,会从这个 FOLLOWERINFO 中选取最大的 epoch 值,对其加 1 后作为新的 epoch 值,并封装成 LEADERINFO 消息发给这些过半的 Follower。
- Follower 收到 LEADERINFO 消息后,会先校验 LEADERINFO 消息正确性:校验自己的 epoch 是否小于 LEADERINFO 消息中的epoch(因为该 LEADERINFO 可能是迟到的消息),如果小于,就将 LEADERINFO 消息中的 epoch 赋值给自己的 epoch,并将自己的运行状态变更为 SYNCHRONIZATION,最后向准 Leader 返回 ACK 响应(ACKEPOCH,会携带已有的提案列表信息)。
- 最后准 Leader 收到过半的 ACKEPOCH 消息后,也将自己的运行状态修改为 SYNCHRONIZATION。至此完成发现阶段的工作,集群确立 Leader 的领导关系。
从此之后,旧 Leader 发起的提案不会再收到多数 Follower 的响应,因为旧 Leader 的 epoch 已经小于多数 Follower 的 epoch。
数据同步 (SYNCHRONIZATION)
在 ZAB 中,Leader 为了更高效的将 Proposal 复制给 Follower,会在自己的内存队列中缓存一定数量(默认500)的已提交的 Proposal。在内存中的 Proposal 就有 zxid 的最大值和最小值,即:maxCommittedZxid 和 minCommittedZxid。Leader 到 Follower 有三种同步方式:
- DIFF:minCommittedZxid < Follower 最大的 zxid < maxCommittedZxid 时,则同步差异的 Proposal 给 Follower。
- TRUNC:Follower 最大的 zxid > maxCommittedZxid 时,要求 Follower 丢弃超出的那部分 Proposal。
- SNAP:Follower 最大的 zxid < minCommittedZxid 时,直接同步快照给 Follower。
数据同步流程:
- Leader 根据 Follower 的最大 zxid 来选择同步方式和需要发送的数据给 Follower,该过程传输的主要是日志数据流或者 Leader 给 Follower 的各种命令。
- Leader发送 NEWLEADER 命令给 Follower,告诉 Follower 日志同步已经完成。
- Follower 在收到 NEWLEADER 消息后,进行修复不一致数据,并返回给 Leader 响应 ACK 消息。
- Leader 在收到过半 ACK 消息后,则完成数据同步阶段,将自己运行状态修改为 BROADCARST(广播状态),并发送 UPTODATE 消息给过半的 Follower,通知他们完成数据同步,修改运行状态修改为 BROADCARST。
小结
总结
- 为啥不使用 Paxos 实现 ZK
- ZAB 中的角色:Leader, Follower, Observer
- 两种运行模式
- 崩溃恢复
- 选举阶段:在系统启动或 Leader 异常时选举出 Leader
- 成员发现:更新集群的 epoch,防止响应后续可能收到的旧 Leader epoch 相关的的提议
- 数据同步:完成数据对齐
- 消息传播:处理事务请求
- 崩溃恢复
题外话:
ZooKeeper 保证写的线性一致性,但读是最终一致性,若要保证读到的数据是最新的,读之前要使用
sync
方法。
参考链接
本文作者:Joey-Wang
本文链接:https://www.cnblogs.com/joey-wang/p/17638880.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步