ZooKeeper分布式一致性协议ZAB源码剖析

ZAB协议介绍

ZAB协议全称:Zookeeper Atomic Broadcast(Zookeeper原子广播协议)。

整个ZooKeeper就是一个多节点分布式一致性算法的实现,底层采用的实现协议是ZAB。

ZooKeeper是一个为分布式应用提供高效且可靠的分布式协调服务。在解决分布式一致性方面,ZooKeeper并没有使用Paxos,而是采用了ZAB协议,ZAB是Paxos算法的一种简化实现。

ZAB协议定义:ZAB协议是为分布式协调服务ZooKeeper专门设计的一种支持崩溃恢复和原子广播的协议。下面我们会重点讲这两个东西。

基于该协议,ZooKeeper实现了一种主备模式的系统架构来保持集群中各个副本之间数据一致性。具体如下图所示:
ZooKeeper分布式一致性协议ZAB源码剖析_第1张图片
上图显示了ZooKeeper如何处理集群中的数据。所有客户端写入数据都是写入到Leader节点,然后由Leader复制到Follower节点中,从而保证数据一致性。

那么复制过程又是如何的呢?
复制过程类似两阶段提交(2PC),ZAB只需要Follower(含leader自己的ack)有一半以上返回Ack信息就可以执行提交,大大减小了同步阻塞,也提高了可用性。

简单介绍完,开始重点介绍消息广播和崩溃恢复。整个ZooKeeper就是在这两个模式之间切换。 简而言之,当 Leader服务可以正常使用,就进入消息广播模式,当Leader不可用时,则进入崩溃恢复模式。

消息广播

ZAB协议的消息广播过程使用的是一个原子广播协议,类似一个两阶段提交过程。对于客户端发送的写请求,全部由Leader接收,Leader将请求封装成一个事务Proposal,将其发送给所有Follwer,然后根据所有Follwer的反馈,如果超过半数(含leader自己)成功响应,则执行commit操作。

整个广播流程如下:
ZooKeeper分布式一致性协议ZAB源码剖析_第2张图片
通过以上步骤,就能够保持集群之间数据的一致性。

大概步骤:

  1. leader收到客户端写请求,发送proposal提议并携带数据给follower,写log文件到本地,并给自己发送一个ack;
  2. follower收到proposal后,将log文件写到本地,并返回ack给leader;
  3. leader接收到半数以上ack(包括自己的ack)后,发送commit给follower,follower将数据写入内存(Node节点,map结构)。leader还会发送inform让observer存储消息,然后leader再将数据写入自己的内存;
  4. 响应客户端请求;
  5. zk节点收到查询请求,只从内存中读取数据,因为内存数据是commit后才会放入;

还有一些细节:

  1. Leader在收到客户端请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一ID,称为事务ID(ZXID),ZAB协议需要保证事务的顺序,因此必须将每一个事务按照ZXID进行先后排序然后处理,主要通过消息队列实现。
  2. 在Leader和Follwer之间还有一个消息队列,用来解耦他们之间的耦合,解除同步阻塞。
  3. ZooKeeper集群中为保证任何所有进程能够有序地顺序执行,只能是Leader服务器接受写请求,即使是Follower服务器接受到客户端的写请求,也会转发到Leader服务器进行处理,Follower只能处理读请求。
  4. ZAB协议规定了如果一个事务在一台机器上被处理(commit)成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。

崩溃恢复

刚刚我们说消息广播过程中,Leader崩溃怎么办?还能保证数据一致吗?

实际上,当Leader崩溃,即进入我们开头所说的崩溃恢复模式(崩溃即:Leader失去与过半Follwer的联系)。下面来详细讲述。
两个假设:

  1. 假设1(事务提交前崩溃):Leader在复制数据给所有Follwer之后,还没来得及收到Follower的ack返回就崩溃,怎么办?
  2. 假设2(事务提交后崩溃):Leader在收到ack并提交了自己,同时发送了部分commit出去之后崩溃怎么办?

针对这些问题,ZAB定义了 2 个原则:

  1. ZAB协议确保丢弃那些只在Leader提出/复制,但没有提交的事务。
  2. ZAB协议确保那些已经在Leader提交的事务最终会被所有服务器提交。

所以,ZAB设计了下面这样一个选举算法:
能够确保提交已经被Leader提交的事务,同时丢弃已经被跳过的事务。

针对这个要求,如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器ZXID最大的事务,那么就能够保证这个新选举出来的Leader一定具有所有已经提交的提案。
而且这么做有一个好处是:可以省去Leader服务器检查事务的提交和丢弃工作的这一步操作。

数据同步

当崩溃恢复之后,需要在正式工作之前(接收客户端请求),Leader服务器首先确认事务是否都已经被过半的Follwer提交了,即是否完成了数据同步。目的是为了保持数据一致。
当Follwer服务器成功同步之后,Leader会将这些服务器加入到可用服务器列表中。

实际上,Leader服务器处理或丢弃事务都是依赖着ZXID的,那么这个ZXID如何生成呢?
在ZAB协议的事务编号ZXID设计中,ZXID是一个64位的数字,其中低32位可以看作是一个简单的递增的计数器(Aotmic原子类实现),针对客户端的每一个事务请求,Leader都会产生一个新的事务Proposal并对该计数器进行+1操作。
而高32位则代表了Leader服务器上取出本地日志中最大事务Proposal的ZXID,并从该ZXID中解析出对应的 epoch值(leader选举周),当一轮新的选举结束后,会对这个值加一,并且事务id又从0开始自增。
ZooKeeper分布式一致性协议ZAB源码剖析_第3张图片
高32位代表了每代Leader的唯一性,低32代表了每代Leader中事务的唯一性。同时,也能让Follwer通过高32位识别不同的Leader。简化了数据恢复流程。
基于这样的策略:当Follower连接上Leader之后,Leader服务器会根据自己服务器上最后被提交的ZXID和Follower上的ZXID进行比对,比对结果要么回滚,要么和Leader同步。

ZAB写数据源码流程图

ZooKeeper中NIO&BIO&Netty的理解

ZK开始不支持Netty,通过NIO与客户端通讯,官方建议使用Netty;
BIO用于集群节点间的通讯,因为BIO比NIO简单不容易出错,而且集群中的BIO链接不会很多,使用BIO效果更优;

ZooKeeper如何避免脑裂

什么是脑裂?
当ZK集群发生网络分区时,原来的Leader被隔离了,新的Leader在新的集群中通过选举产生,此时对外提供服务就有两个Leader节点了,此时就存在脑裂问题。
如何解决?
当集群存在多个Leader时,读数据时可能存在数据不一致情况(其实,正常的集群中由于ZK是半数即成功也会存在读数据不一致的情况,保证最终一致性),但是在老的Leader上只能读到已提交的内存数据,未提交的数据只会存在于磁盘log文件中。Leader会定时地发ping命令与所有Follower保持联系,当联系流失时Leader会变成LOOKING状态重新发起选举。当分区恢复后,节点之间建立连接时会比对epoch纪元,新的Leader纪元比老Leader大,所以老的Leader会同步新Leader数据并更新自己为Follower。

你可能感兴趣的:(#,ZooKeeper,分布式,zookeeper,云原生,zab,源码,脑裂)