ZooKeeper Atomic Broadcast (ZAB)协议是ZooKeeper作为数据一致性的核心算法,一种支持崩溃恢复的原子广播协议。ZAB的核心是定义了那些会改变ZooKeeper服务器数据状态的事务请求的处理方式,即:
所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为leader,而余下的其他服务器为follower。leader服务器负责将每一个client事务请求转换成一个事务proposal(提议),并将该proposal分发给集群中的每台follower服务器。之后leader服务器需要等待follower服务器的反馈,一旦超过半数的follower服务器进行了正确的反馈,那么leader就会再次向所有的follower分发commit消息,要求将前一个proposal进行提交。
ZAB协议包含两种基本模式:崩溃恢复模式和消息广播模式。
当整个服务框架在启动过程中,或是leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB协议就进入崩溃恢复模式并选举出新的leader服务器。当选举出了新的leader服务器,同时集群中已有过半服务器与该leader服务器完成了数据同步之后,ZAB协议就退出崩溃恢复模式。
当集群中有过半的follower服务器与leader服务器完成了数据同步,则进入到消息广播模式下。当一台同样遵循ZAB协议的服务器加入到集群中时,会自觉的进入数据恢复模式:找到leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播中。leader服务器收到client的事务请求后,会生成对应的事务提议proposal并发起一轮广播协议;如果是集群中的follower收到client的事务请求,则会把事务请求转发给leader服务器来处理。
当leader服务器出现崩溃退出或者机器重启,亦或是集群中不存在半数以上的follower机器与其正常通信,那么在新一轮的原子广播事务操作前,所有进程首先使用崩溃恢复模式来使彼此达到一个一致的状态。于是这个ZAB流程就会从消息广播模式进入到崩溃恢复模式。
整个消息广播过程中,leader服务器会为每一个事务请求生成相应的proposal来进行广播,并且在广播proposal前,leader会为其分配全局单调递增的唯一ID,称之为事务ID(即ZXID)。由于ZAB协议需要保证消息严格的因果关系,因此必须将proposal按照ZXID的先后顺序进行排序与处理。
具体的,leader服务器会为每一个follower服务器分配一个单独的消息队列,然后将需要广播的事务proposal一次放入到这些队列中,并根据FIFO的策略进行消息发送。每个follower在接收到proposal后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在写成功后反馈给leader服务器一个Ack响应。当leader服务器收到超过半数的follower的Ack响应后,就会广播一个Commit消息给所有的follower服务器,以通知其进行事务提交,同时leader自身也会完成事务的提交,而每一个follower服务器在收到Commit消息后,将磁盘中的事务日志写入到内存中。
谁ZXID大选谁为Leader,ZXID相同,谁SID大选谁为Leader。相关细节的源码实现,可以参考这篇文章ZooKeeper leader选举 源码分析
崩溃恢复过程中,可能会出现2个数据不一致的隐患及针对这2种情况ZAB协议需要保证如下特征:
特性一:确保那些已经在leader服务器中提交的事务最终被所有服务器都提交
假设一个事务在leader服务器中被提交了,并且已经得到过半follower服务器的Ack反馈,但时在它将Commit消息发送给所有follower机器前,leader服务器挂了,如下图:
上图中C2就是个典型的例子:在集群正常运行的某一刻,server1是leader服务器,先后广播了消息P1、P2、C1、P3和C2,其中,当leader将C2发出后就崩溃退出了(说明:此时leader并未给所有的follower广播C2,因为每个follower都有专门的队列,可能给部分队列广播后就崩溃了)。针对这种情况,ZAB协议需要保证事务proposal2最终能被所有的服务器都提交成功,否则将出现不一致。
如何做到?
选举出来的新leader必须拥有 所有机器中最高编号(即ZXID) 的事务proposal,那么就可以保证新选举出来的leader一定具有所有已提交的提案。
如何同步数据
选举出来的leader为每一个follower创建一个队列,并将那些没有被各follower同步的事务以proposal的形式逐个发给每个follower,并随之发送Commit消息,表示该事务已被提交。等到所有follower将未同步的事务都从leader同步到本地内存后,leader服务器就会将follower服务器加入到真正可用的follower列表汇总,并开始其他流程。
特性二:保证丢弃那些只在leader服务器上提出的事务(注意是提出,而非提交)。
相反,如果在崩溃恢复过程中出现一个需要被丢弃的提议,那么在崩溃恢复后需要跳过该proposal,如下图:
假设leader服务器在提出了一个事务proposal3后就崩溃退出了。于是,当server1恢复过来再次加入到集群中的时候,ZAB需要确保丢弃proposal3。(很明显该事务并没有被Commit过,当然要被遗弃)
如何做到?
首先了解下事务编号ZXID,是1个64位的数字,其中低32位是一个单调递增的计数器。针对client的每一个事务请求,leader在创建proposal的时候,都会对该计数器加1操作;而高32位,则代表leader周期epoch的编号,每当选举出一个新leader服务器,就会从该服务器本地日志中取出最大事务proposal的ZXID,并从中解析出高32位的epoch值,并加1操作,之后此编号作为新的epoch,并将低32位置为0来开始新的ZXID。ZAB中通过epoch编号来区分leader周期变化的策略,有效的避免了不同leader周期使用相同的ZXID编号来处理不同proposal的异常情况。
基于这样的策略,当一个包含了上个leader周期中尚未提交的proposal的服务器启动后,肯定无法成为leader,原因很简单,它的proposal的ZXID是上个epoch的ID,必定小于当前leader周期的任何一个proposal的ZXID,所以它只能以follower角色加入到当前集群中,并建立和leader的连接,leader服务器根据自己服务器上最后被提交的proposal来和follower上的proposal进行对比,对比的结果是leader会要求follower服务器进行回退操作–回退到一个确实已经被集群中过半机器提交的最新事务proposal。如上图,leader要求server1去除P3。去除后,进行如上的数据同步。