1. Zab
Zab协议 的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播)。基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间数据一致性
系统架构可以参考下面这张图:
在ZooKeeper集群中,所有客户端的请求都是写入到Leader进程中的,然后,由Leader同步到其他节点,称为Follower。在集群数据同步的过程中,如果出现 Follower 节点崩溃或者 Leader 进程奔溃时,都会通过 Zab 协议来保证数据一致性
Zab 协议的具体实现可以分为以下两部分:
-
消息广播阶段 Leader节点接受事务提交,并且将新的Proposal请求广播给Follower节点,收集各个节点的反馈,决定是否进行Commit,在这个过程中,也会使用Quorum 选举机制。
-
崩溃恢复阶段 如果在同步过程中出现 Leader 节点宕机,会进入崩溃恢复阶段,重新进行 Leader 选举,崩溃恢复阶段还包含数据同步操作,同步集群中最新的数据,保持集群的数据一致性。
整个ZooKeeper集群的一致性保证就是在上面两个状态之前切换,当Leader服务正常时,就是正常的消息广播模式;当Leader不可用时,则进入崩溃恢复模式,崩溃恢复阶段会进行数据同步,完成以后,重新进入消息广播阶段。
2. Zxid
Zxid 在 ZooKeeper 的一致性流程中非常重要,在详细分析 Zab 协议之前,先来看下 Zxid 的概念。
Zxid是Zab协议的一个事务编号,Zxid是一个64位的数字,其中低32位是一个简单的单调递增计数器,针对客户端每一个事务请求,计数器加1;而高 32 位则代表 Leader 周期年代的编号。
这里Leader周期的英文是epoch,可以理解为当前集群所处的年代或者周期,对比另外一个一致性算法Raft中的Term概念。在Raft中,每一个任期的开始都是一次选举,Raft 算法保证在给定的一个任期最多只有一个领导人。
Zab协议的实现也类似,每当有一个新的Leader选举出现时,就会从这个Leader服务器上取出其本地日志中最大事务的Zxid,并从中读取epoch值,然后加1,以此作为新的周期 ID。总结一下,高 32 位代表了每代 Leader 的唯一性,低 32 位则代表了每代 Leader 中事务的唯一性(低32位是)epoch内的自增id,由0开始)。
epoch 编号可以理解为当前集群所处的年代,或者周期。每次Leader变更之后都会在 epoch 的基础上加1,这样旧的 Leader 崩溃恢复之后,其他Follower 也不会听它的了,因为 Follower 只服从epoch最高的 Leader 命令。
每当选举产生一个新的 Leader ,就会从这个 Leader 服务器上取出本地事务日志充最大编号 Proposal 的 zxid,并从 zxid 中解析得到对应的 epoch 编号,然后再对其加1,之后该编号就作为新的 epoch 值,并将低32位数字归零,由0开始重新生成zxid。
Zab 协议通过 epoch 编号来区分 Leader 变化周期,能够有效避免不同的 Leader 错误的使用了相同的 zxid 编号提出了不一样的 Proposal 的异常情况。
基于这样的策略:当 Follower 链接上 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 和 Follower 上的 ZXID 进行比对,比对结果要么回滚,要么和 Leader 同步。
3. Zab 流程分析
Zab 的具体流程可以拆分为消息广播、崩溃恢复和数据同步三个过程,下面我们分别进行分析。
3.1. 消息广播
在ZooKeeper中所有的事务请求都由Leader节点来处理,其他服务器为Follower,Leader将客户端的事务请求转换为事务Proposal,并且将 Proposal 分发给集群中其他所有的 Follower。
完成广播之后,Leader等待Follwer反馈,当有过半数的Follower反馈信息后,Leader将再次向集群内Follower广播Commit信息,Commit信息就是确认将之前的 Proposal 提交。
这里的Commit可以对比SQL中的COMMIT操作来理解,MySQL默认操作模式是autocommit自动提交模式,如果你显式地开始一个事务,在每次变更之后都要通过COMMIT语句来确认,将更改提交到数据库中。
Leader节点的写入也是一个两步操作,第一步是广播事务操作,第二步是广播提交操作,其中过半数指的是反馈的节点数 >=N/2+1,N 是全部的 Follower 节点数量。
在消息的广播过程中,Leader会为每一个Follower准备一个事务队列,该队列符合FIFO原则。假设Follower接收到了客户端的写请求,写请求会被转发至Leader处理。
消息广播的过程描述可以参考下图
- 客户端的写请求进来之后,Leader 会将写请求包装成 Proposal 事务,并添加一个递增事务 ID,也就是 Zxid,Zxid 是单调递增的,以保证每个消息的先后顺序;
- 广播这个Proposal事务,Leader节点和Follower节点是解耦的,通信都会经过一个先进先出的消息队列,Leader会为每一个Follower服务器分配一个单独的FIFO队列,然后把 Proposal 放到队列中;
- Follower 节点收到对应的 Proposal 之后会把它持久到磁盘上(本地事务日志文件),当完全写入之后,发一个 ACK 给 Leader;
- 当 Leader 收到超过半数 Follower 机器的 ack 之后,会提交本地机器上的事务,同时开始广播 commit
- Follower 收到 commit 之后,完成各自的事务提交。
zookeeper集群中为保证任何所有进程能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是 Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进行处理。
Leader为每个Follower使用队列做了异步解耦,大大降低同步阻塞,提高了系统的吞吐量。
3.2. 崩溃恢复
消息广播通过 Quroum 机制,解决了 Follower 节点宕机的情况,但是如果在广播过程中 Leader 节点崩溃呢?
这就需要 Zab 协议支持的崩溃恢复,崩溃恢复可以保证在 Leader 进程崩溃的时候可以重新选出 Leader,并且保证数据的完整性。
崩溃恢复和集群启动时的选举过程是一致的,也就是说,下面的几种情况都会进入崩溃恢复阶段:
- 初始化集群,刚刚启动的时候
- Leader 崩溃,因为故障宕机
- Leader 失去了半数的机器支持,与集群中超过一半的节点断连
崩溃恢复模式将会开启新的一轮选举,选举产生的 Leader 会与过半的 Follower 进行同步,使数据一致,当与过半的机器同步完成后,就退出恢复模式, 然后进入消息广播模式。
节点状态
Zab 中的节点有三种状态,伴随着的 Zab 不同阶段的转换,节点状态也在变化:
选举Leader很简单,只要保证新选出来的Leader服务器拥有最大的ZXID就可以,那么这个新Leader一定具有所有已提交的事务,还可以省去检查Proposal的提交和丢弃工作。
我们通过一个模拟的例子,来了解崩溃恢复阶段,也就是选举的流程。
假设正在运行的集群有五台Follower服务器,编号分别是Server1、Server2、Server3、Server4、Server5,当前Leader是Server2,若某一时刻 Leader 挂了,此时便开始 Leader 选举。
选举过程如下:
1.各个节点变更状态,变更为 Looking
ZooKeeper中除了Leader和Follower,还有Observer节点,Observer不参与选举,Leader挂后,余下的Follower节点都会将自己的状态变更为为 Looking,然后开始进入 Leader 选举过程。
2.各个 Server 节点都会发出一个投票,参与选举
在第一次投票中,所有的 Server 都会投自己,然后各自将投票发送给集群中所有机器,在运行期间,每个服务器上的 Zxid 大概率不同。
3.集群接收来自各个服务器的投票,开始处理投票和选举
处理投票的过程就是对比Zxid的过程,假定Server3的Zxid最大,Server1判断Server3可以成为Leader,那么Server1就投票给Server3,判断的依据如下:
- 首先选举 epoch 最大的
- 如果 epoch 相等,则选 zxid 最大的
- 若 epoch 和 zxid 都相等,则选择 server id 最大的,就是配置 zoo.cfg 中的 myid
在选举过程中,如果有节点获得超过半数的投票数,则会成为 Leader 节点,反之则重新投票选举。
4.选举成功,改变服务器的状态
3.3. 数据同步
崩溃恢复完成选举以后,接下来的工作就是数据同步,在选举过程中,通过投票已经确认Leader服务器是最大Zxid的节点,同步阶段就是利用Leader前一阶段获得的最新Proposal历史,同步集群中所有的副本。
Follower只会接收ZXID比自己的最后一次事务的ZXID大的提议。
- 所有的Follower向准Leader发送自己的最后接收事务的epoch
- 准Leader选出最大的epoch,并在此基础上进行加1,然后将新的epoch发送给所有的Follower
- Follower收到新的epoch之后,与自己的进行比较,小于就将自己的epoch更新成新的epoch,并向准Leader反馈ACK信息(epoch、历史事务集合)
- 准Leader收到ACK消息后,会在所有历史事务集合中选出其中一个历史事务集合作为初始化历史事务集合,该事务集合必须满足最大ZXID
- 准Leader将epoch和初始化历史事务集合发送给过半的Follower,每个Follower分配一个事务队列然后逐条将事务发送给Follower
- Follower接收到事务请求后,如果已执行过则跳过,未执行则执行事务并反馈响应给准Leader
- 准Leader收到响应后则发起事务commit请求,提交事务
- 数据完成同步后,准Leader就是Leader,ZAB协议由崩溃恢复模式进入消息广播模式
4. Zab 协议如何保证数据一致性
假设两种异常情况: 1、一个事务在 Leader 上提交了,并且过半的 Folower 都响应 Ack 了,但是 Leader 在 Commit 消息发出之前挂了。 2、假设一个事务在 Leader 提出之后,Leader 挂了。
要确保如果发生上述两种情况,数据还能保持一致性,那ZAB协议需要保证以下两条原则:
- 确保那些已经在Leader服务器上提交的事务最终被所有的服务器进行提交(在Leader上提交,说明绝大多数Follower已经接收到了事务的Proposal请求并返回了ACK响应,只是还未收到commit请求)
- 确保丢弃那些只在Leader服务器上被提出的事务(只在Leader上被提出说明没有其他Follower并没有收到Proposal请求)
针对这个要求,如果让 Leader 选举算法能够保证新选举出来的 Leader 服务器拥有集群总所有机器编号(即 ZXID 最大)的事务,那么就能够保证这个新选举出来的 Leader 一定具有所有已经提交的提案。
而且这么做有一个好处是:可以省去 Leader 服务器检查事务的提交和丢弃工作的这一步操作。
5. Zab 与 Paxos 算法的联系与区别
上面分析了 Zab 协议的具体流程,接下来我们对比一下 Zab 协议和 Paxos 算法。
Paxos 的思想在很多分布式组件中都可以看到,Zab 协议可以认为是基于 Paxos 算法实现的,先来看下两者之间的联系:
- 都存在一个 Leader 进程的角色,负责协调多个 Follower 进程的运行
- 都应用 Quroum 机制,Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交
- 在 Zab 协议中,Zxid 中通过 epoch 来代表当前 Leader 周期,在 Paxos 算法中,同样存在这样一个标识,叫做 Ballot Number
两者之间的区别是,Paxos是理论,Zab是实践,Paxos是论文性质的,目的是设计一种通用的分布式一致性算法,而Zab协议应用在ZooKeeper中,是一个特别设计的崩溃可恢复的原子消息广播算法。
Zab 协议增加了崩溃恢复的功能,当 Leader 服务器不可用,或者已经半数以上节点失去联系时,ZooKeeper 会进入恢复模式选举新的 Leader 服务器,使集群达到一个一致的状态。
6. 参考资料
https://cloud.tencent.com/developer/article/1469528
《分布式技术原理与实战45讲》