ZAB协议是为分布式协调服务Zookeeper专门设计的一种支持崩溃恢复的原子广播协议。

ZAB协议包括两种基本的模式:崩溃恢复(leader崩溃后选举leader的过程)和消息广播(半数follower收到请求即commit)。

当整个zookeeper集群刚刚启动或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的Leader服务器,然后集群中Follower服务器开始与新的Leader服务器进行数据同步,当集群中超过半数机器与该Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。

Zab协议概述

Zab协议 的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播协议),基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间数据一致性。

Zookeeper 是通过 Zab 协议来保证分布式事务的最终一致性。

在解决分布式一致性方面,Zookeeper 并没有使用 Paxos ,而是采用了 ZAB 协议。很多论文和资料都证明zab其实就是paxos的一种简化实现,但Apache 自己的立场说zab不是paxos算法的实现,这个不需要去计较。zab协议解决的问题和paxos一样,是解决分布式系统的数据一致性问题.


系统架构可以参考下面这张图:

image.png

在 ZooKeeper 集群中,所有客户端的请求都是写入到 Leader 进程中的,然后,由 Leader 同步到其他节点,称为 Follower。在集群数据同步的过程中,如果出现 Follower 节点崩溃或者 Leader 进程崩溃时,都会通过 Zab 协议来保证数据一致性。

zab协议两部分

1.消息广播阶段

Leader 节点接受事务提交,并且将新的 Proposal 请求广播给 Follower 节点,收集各个节点的反馈,决定是否进行 Commit,在这个过程中,也会使用上一课时提到的 Quorum 选举机制。

2.崩溃恢复阶段

如果在同步过程中出现 Leader 节点宕机,会进入崩溃恢复阶段,重新进行 Leader 选举,崩溃恢复阶段还包含数据同步操作,同步集群中最新的数据,保持集群的数据一致性。

整个 ZooKeeper 集群的一致性保证就是在上面两个状态之前切换,当 Leader 服务正常时,就是正常的消息广播模式;当 Leader 不可用时,则进入崩溃恢复模式,崩溃恢复阶段会进行数据同步,完成以后,重新进入消息广播阶段。

ZAB协议下一种可能存在的数据一致性问题

follower挂掉一个之后对你整个集群几乎是一点影响都没有.
当你leader挂掉之后有两种情况会出现数据不一致的问题
假如说客户端往zookeeper写一条数据为 “hello”
第一种情况是 leader同步数据收到过半的ack之后,本来要发送commit给所有的follower之后,但是还没来得及发送commit给其它的follower,leader就挂掉了.这样就会导致”hello”这次的数据丢失.
此时客户端当连接新的leader之后,就会读不到”hello”这条数据,
在崩溃恢复之后新选举了一个leader会去磁盘日志文件里面检查某一个proposal提议并没有进行commit操作,这个新的leader就需要看一看其它的过半的follower是不是收到了proposal,如果有过半的follower收到了proposal,并且发送了ack,那么这个新上任的leader服务器就会代替老的leader 发这个commit指令发送给所有的follower,让客户端可以读到已经提交的这个 “hello”的数据.
第二种情况是你刚给数据写给leader,还没来得及将proposal(写数据)发送给其它的follower,此时leader宕机了, 然后客户端会重新将这个数据通过新的leader(刚刚选举的)写入.
当老的leader重启成功之后它的身份又变成了follower,follower发现自己本地有个proposal没有进行commit,然后这个老的leader就会和新的leader机器对比有一条数据没有提交. 那么这个老的leader就会给这条数据删了,重新从新的leader机器同步数据就可以了.

对于需要丢弃的消息是如何在ZAB协议中进行处理的


每一条事务的zxid是64位的,高32位是leader的epoch,就认为是leader的版本吧.低32为才是自增长的zxid
老leader发送出去了proposal,高32位就是1,低32位是11358
如果一个leader自己刚刚把一个proposal写入本地磁盘日志,就宕机了,还没来得及发送给全部的follower,此时新的leader选举出来,新leader的epoch会自增长1位.
然后老的leader重启恢复了以后连接集群它就变成follower角色了,此时发现自己比新的leader多出一条proposal,但是自己的epoch比新的leader的epoch低(说明这个proposal只有自己才有).所以就会丢弃掉这条数据. , 再去新的leader机器上同步新的数据.

paxos和zookeeper的zab算法区别

相同点:

(1)两者都存在一个类似于 Leader 进程的角色,由其负责协调多个 Follower 进程的运行

(2)Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交

(3)ZAB 协议中,每个 Proposal 中都包含一个 epoch 值来代表当前的 Leader周期,Paxos 中名字为 Ballot

不同点:

ZAB 用来构建高可用的分布式数据主备系统(Zookeeper),Paxos 是用来构建分布式一致性状态机系统。

Zab流程具体分析

ZAB协议 - 图2

崩溃恢复

zookeeper集群刚刚启动时候没有Leader服务器,或者Leader服务器宕机、重启或者网络故障导致不存在过半的服务器与Leader服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的Leader服务器,然后集群中Follower服务器开始与新的Leader服务器进行数据同步,当集群中超过半数机器(设置超过半数儿是为了防止脑裂情况)与该Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式,Leader服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。


领导选举(FastLeaderElection算法)
总规则:先比对ZXID,ZXID比较大的服务器优先选成Leader,如果ZXID相同就比较myid(myid是程序员自己在搭集群的时候配置的),myid大的服务器就会成为Leader服务器.

问题:为什么优先选大的zxid
我们每做一次事务的操作都会更新ZXID,ZXID越大说明当前机器的日志数据是越全的

下面是详细介绍
若进行Leader选举,则至少需要两台机器,这里选取3台机器组成的服务器集群为例。在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下
1. 每个Server发出一个投票。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
2. 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。
3. 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下
· 优先检查ZXID。ZXID比较大的服务器优先作为Leader。
  · 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。
对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。

4. 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器(过半是为了防止脑裂的现象)接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader。
5. 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。




最终目的(恢复成什么样)
ZAB协议崩溃恢复要求满足如下2个要求:
确保已经被leader提交的proposal必须最终被所有的follower服务器提交。
确保丢弃已经被leader出的但是没有被提交的proposal。

新选举出来的leader不能包含未提交的proposal,即新选举的leader必须都是已经提交了的proposal的follower服务器节点。同时,新选举的leader节点中含有最高的ZXID。这样做的好处就是可以避免了leader服务器检查proposal的提交和丢弃工作。

数据同步

数据同步是在Leader选举完成之后进行的
同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。只有当 集群过半机器都同步完成,准 leader 才会成为真正的 leader。follower 只会接收 zxid 比自己的 lastZxid 大的提议。


当所有的 Follwer 服务器都成功同步之后,Leader 会将这些服务器加入到可用服务器列表中。
实际上,Leader 服务器处理或丢弃事务都是依赖着 ZXID 的,那么这个 ZXID 如何生成呢?
答:在 ZAB 协议的事务编号 ZXID 设计中,ZXID 是一个 64 位的数字,其中低 32 位可以看作是一个简单的递增的计数器,针对客户端的每一个事务请求,Leader 都会产生一个新的事务 Proposal 并对该计数器进行 + 1 操作。
而高 32 位则代表了 Leader 服务器上取出本地日志中最大事务 Proposal 的 ZXID,并从该 ZXID 中解析出对应的 epoch 值,然后再对这个值加一。

ZAB协议 - 图3

高 32 位代表了每代 Leader 的唯一性,低 32 代表了每代 Leader 中事务的唯一性。同时,也能让 Follwer 通过高 32 位识别不同的 Leader。简化了数据恢复流程。
基于这样的策略:当 Follower 链接上 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 和 Follower 上的 ZXID 进行比对,比对结果要么回滚,要么和 Leader 同步。

消息广播

写的事务指的是zookeeper进行增删改操作.
如果你有写的事务型请求过来了,我需要进行一个投票,我要确定这些事务型的请求被同步到了其它的机器上.
当事务型过来之后Leader会发送一个投票的请求,首先给Follower投票,Follower如果同意写的操作就把相应的请求写到日志里面,如果写成功了就会发送ACK给Leader.
Leader内部有一个ackset集合,每次Follower响应回来ack,Leader会把ACK记录到ackset里面,记录到一定个数的时候,Leader会判断ackset的里面的ack数量有没有大于整个ackset容量的一半儿,ackset数量也是zookeeper集群数量.
当ackset里面的ack数量大于ackset容量的一半儿的时候,Leader就会向Follower发送COMMIT指令,首先自己的事务会提交,然后Follower收到COMMIT命令后也会提交自己的数据.这样就达到数据一致性了.


在Zookeeper中主要依赖Zab协议来实现数据一致性,基于该协议,zk实现了一种主备模型(即Leader和Follower模型)的系统架构来保证集群中各个副本之间数据的一致性。
这里的主备系统架构模型,就是指只有一台客户端(Leader)负责处理外部的写事务请求,然后Leader客户端将数据同步到其他Follower节点。

ZAB 协议类似一个二阶段提交过程,zab协议的消息广播过程使用的是一个原子广播协议,核心是在整个Zookeeper集群中只有Leader节点接收所有的写请求,即使Follower接收到了写请求也会转发给Leader去处理,
Leader将所有客户端的写操作转化为事务(提议proposal),Leader节点再数据写完之后,将向所有的Follower节点发送数据广播请求(数据复制),等所有的Follower节点的反馈,在zab协议中,只要超过半数follower节点反馈ok,leader节点会向所有follower服务器发送commit消息,既将leader节点上的数据同步到follower节点之上。

基本上,整个广播流程分为 3 步骤:

将数据都复制到 Follwer 中

ZAB协议 - 图4
等待 Follwer 回应 Ack,最低超过半数即成功
ZAB协议 - 图5
当超过半数成功回应,则执行 commit ,同时提交自己
ZAB协议 - 图6
通过以上 3 个步骤,就能够保持集群之间数据的一致性。实际上,在 Leader 和 Follwer 之间还有一个消息队列,用来解耦他们之间的耦合,避免同步,实现异步解耦。

还有一些细节:

1. Leader 在收到客户端请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一 ID,称为事务ID(ZXID),ZAB 兮协议需要保证事务的顺序,因此必须将每一个事务按照 ZXID 进行先后排序然后处理。
2. 在 Leader 和 Follwer 之间还有一个消息队列,用来解耦他们之间的耦合,解除同步阻塞。
3. zookeeper集群中为保证任何所有进程能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是 Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进行处理。
4. 实际上,这是一种简化版本的 2PC,不能解决单点问题。等会我们会讲述 ZAB 如何解决单点问题(即 Leader 崩溃问题)。

消息广播是什么,就是类似于2PC

Leader 接收到消息请求后,将消息赋予一个全局唯一的 64 位自增 id,叫做:zxid,通过 zxid 的大小比较即可实现因果有序这一特性。
Leader 通过先进先出队列(通过 TCP 协议来实现,以此实现了全局有序这一特性)将带有 zxid 的消息作为一个提案(proposal)分发给所有 follower。
当 follower 接收到 proposal,先将 proposal 写到硬盘,写硬盘成功后再向 leader 回一个 ACK。
当 leader 接收到合法数量的 ACKs 后,leader 就向所有 follower 发送 COMMIT 命令,会在本地执行该消息。
当 follower 收到消息的 COMMIT 命令时,就会执行该消息



为了保证分区容错性,zookeeper是要让每个节点副本必须是一致的

1. 在zookeeper集群中数据副本的传递策略就是采用的广播模式
2. Zab协议中的leader等待follower的ack反馈,只要半数以上的follower成功反馈就好,不需要收到全部的follower反馈。


image.png
zookeeper中消息广播的具体步骤如下:
1. 客户端发起一个写操作请求
2. Leader服务器将客户端的request请求转化为事物proposql提案,同时为每个proposal分配一个全局唯一的ID,即ZXID。
3. leader服务器与每个follower之间都有一个队列,leader将消息发送到该队列
4. follower机器从队列中取出消息处理完(写入本地事物日志中)毕后,向leader服务器发送ACK确认。
5. leader服务器收到半数以上的follower的ACK后,即认为可以发送commit
6. leader向所有的follower服务器发送commit消息。

zookeeper采用ZAB协议的核心就是只要有一台服务器提交了proposal,就要确保所有的服务器最终都能正确提交proposal。这也是CAP/BASE最终实现一致性的一个体现。

回顾一下:前面还讲了2pc协议,也就是两阶段提交,发现流程2pc和zab还是挺像的,
zookeeper中数据副本的同步方式与二阶段提交相似但是却又不同。二阶段提交的要求协调者必须等到所有的参与者全部反馈ACK确认消息后,再发送commit消息。要求所有的参与者要么全部成功要么全部失败。二阶段提交会产生严重阻塞问题,但paxos和2pc没有这要求。

为了进一步防止阻塞,leader服务器与每个follower之间都有一个单独的队列进行收发消息,使用队列消息可以做到异步解耦。leader和follower之间只要往队列中发送了消息即可。如果使用同步方式容易引起阻塞。性能上要下降很多

Zab 协议中的 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 中事务的唯一性。



说白了ZXID值越大,这个ZXID对应的Zookeeper的机器的数据越是最新的数据.

致使ZooKeeper节点状态改变的每一个操作都将使节点接收到一个Zxid格式的时间戳,并且这个时间戳全局有序。也就是说,每个对节点的改变都将产生一个唯一的Zxid。如果Zxid1的值小于Zxid2的值,那么Zxid1所对应的事件发生在Zxid2所对应的事件之前。实际上,ZooKeeper的每个节点维护者两个Zxid值,为别为:cZxid、mZxid。

(1)cZxid: 是节点的创建时间所对应的Zxid格式时间戳。

(2)mZxid:是节点的修改时间所对应的Zxid格式时间戳。

实现中Zxid是一个64为的数字,它高32位是epoch用来标识Leader关系是否改变,每次一个Leader被选出来,它都会有一个新的epoch。低32位是个递增计数。