分布式一致性协议之ZAB
上文我们探讨了2PC和3PC,虽然3PC解决了2PC的超时(同步阻塞或者说无限等待)问题,但架构上并没有改变,协调者依然是固定的一台。如果它出现问题,整个系统将无法正常工作。
所以,后面又有了Paxos算法,引入了“过半”的概念,通俗的讲就是少数服从多数的原则。解决了3PC单点故障和数据不一致的问题。Paxos算法是一种基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。但是其理论繁杂,工程实现比较困难,我们今天研究它的一种变种在工程上的应用广泛的ZAB协议。
一、ZAB协议概念
ZooKeeper是Chubby的开源实现,而Chubby是Paxos的工程实现,所以很多人以为ZooKeeper也是Paxos算法的工程实现。事实上,ZooKeeper并没有完全采用Paxos算法,而是使用了一种称为ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子广播协议)的协议作为其数据一致性的核心算法。
ZAB协议并不像Paxos算法和Raft协议一样,是通用的分布式一致性算法,它是一种特别为ZooKeeper设计的崩溃可恢复的原子广播算法。
基于ZAB协议,ZooKeeper实现了一种主备模式(Leader、Follower)的系统架构来保持集群中各副本之间数据的一致性。主备系统架构模型中,只有Leader负责处理外部的写事务请求,然后Leader将数据同步到其他Follower节点,其中只要超过半数节点写入成功,该事务就会被提交。
二、ZAB协议工作原理
2.1 ZAB协议的两种模式
从以上概念我们可以看出ZAB协议设计成了两种模式,或者说两个过程。
(1)消息广播模式:把数据更新到所有的Follower
(2)崩溃恢复模式:Leader发生崩溃时,如何恢复
整个 Zookeeper 就是在这两个模式之间切换。简而言之,当 Leader 服务可以正常使用,就进入消息广播模式,当 Leader 不可用时,则进入崩溃恢复模式。
2.1.1、消息广播模式
如果你了解过2PC协议的话,理解起来就简单很多了,消息广播的过程实际上是一个简化版本的2PC二阶段提交过程。我们来看一下这个过程:
(1)leader首先把proposal发送到FIFO队列里
(2)FIFO取出队头proposal给Follower
(3)Follower首先会以事务日志的形式写入到本地磁盘,成功后反馈一个ACK给队列
(4)队列把ACK交给leader
(5)leader收到半数以上ACK,就会发送commit指令给FIFO队列
(6)FIFO队列把commit给Follower。
和2PC比较主要有两处不同。
首先ZAB加了FIFO队列,整个消息是基于具有FIFO特性的TCP协议来进行网络通信的,是为了保证消息接收和发送的顺序性。即ZAB协议必须能够保证一个全局的变更序列被顺序应用。也就是说,ZAB协议需要保证如果一个状态变更已经被处理了,那么所有依赖的状态变更都应该已经被提前处理掉了。使用队列消息还可以做到异步解耦,解除同步阻塞,提高性能。
第二处,比2PC简化的地方在于ZAB采用了Quorum机制,也称作”过半“机制,不需要像2PC等待全部节点返回ACK才能进行commit提交,而ZAB只需要过半节点返回正确就可以提交。而过半机制同样用在接下来的崩溃恢复模式的选举过程中和数据同步过程中。同时如果在失败情况下,移除了2PC的中断逻辑,follow就会抛弃事物proposal。
2.1.2、崩溃恢复模式
一旦 Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进入崩溃恢复模式。
1、一个事务在 Leader 上提交了,并且过半的 Folower 都响应 Ack 了,但是 Leader 在 Commit 消息发出之前挂了。
2、假设一个事务在 Leader 提出之后,Leader 挂了。
针对这些问题,ZAB 定义了 2 个原则:
- ZAB 协议确保那些已经在 Leader 提交的事务最终会被所有服务器提交。
- ZAB 协议确保丢弃那些只在 Leader 提出的但没有提交的事务。
所以,ZAB 设计了下面这样一个选举算法:能够确保提交已经被 Leader 提交的事务,同时丢弃已经被跳过的事务。
针对这个要求,如果让 Leader 选举算法能够保证新选举出来的 Leader 服务器拥有集群总所有机器编号(即 ZXID 最大)的事务,那么就能够保证这个新选举出来的 Leader 一定具有所有已经提交的提案。
而且这么做有一个好处是:可以省去 Leader 服务器检查事务的提交和丢弃工作的这一步操作。
Zab 如何数据同步
1)完成 Leader 选举后(新的 Leader 具有最高的zxid),在正式开始工作之前(接收事务请求,然后提出新的 Proposal),Leader 服务器会首先确认事务日志中的所有的 Proposal 是否已经被集群中过半的服务器 Commit。
2)具体分成3小步:
a. Leader 服务器会为每一个Follower服务器都准备一个队列
b. 并将那些没有被同步的事务以Proposal消息的形式逐个发送给Follower服务器
c. 并在每一条Proposal消息后面紧接着再发送一个Commit消息,以表示该事务已提交
简而言之,等到 Follower 将所有尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并且应用到内存数据中以后,Leader 才会把该 Follower 加入到真正可用的 Follower 列表中。
Zab 数据同步过程中,如何处理需要丢弃的 Proposal
在 Zab 的事务编号 zxid 设计中,zxid是一个64位的数字。
其中低32位可以看成一个简单的单增计数器,针对客户端每一个事务请求,Leader 在产生新的 Proposal 事务时,都会对该计数器加1。而高32位则代表了 Leader 周期的 epoch 编号。
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 的异常情况。
总之,高 32 位代表了每代 Leader 的唯一性,低 32 代表了每代 Leader 中事务的唯一性。同时,也能让 Follwer 通过高 32 位识别不同的 Leader。简化了数据恢复流程。
基于以上策略:当一个包含了上一个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时,当这台机器加入集群中,以 Follower 角色连上 Leader 服务器后,Leader 服务器会根据自己服务器上最后提交的 Proposal 来和 Follower 服务器的 Proposal 进行比对,比对的结果肯定是 Leader 要求 Follower 进行一个回退操作,回退到一个确实已经被集群中过半机器 Commit 的最新 Proposal。
2.2 ZAB算法的三个阶段
以上我们描述了整个ZAB协议主要包括消息广播和崩溃恢复两个过程,进一步可以细分为三个阶段,分别是发现(Discovery)、同步(Synchronization)、广播(Broadcast)阶段。
组成ZAB协议的每一个分布式进程,会循环执行这三个阶段,我们将这样一个循环称为一个主进程周期。
-
发现:要求zookeeper集群必须选举出一个Leader进程,同时Leader 会维护一个Follower可用客户端列表。将来客户端可以和这些Follower节点进行通信。
-
同步:Leader要负责将本身的数据与Follower完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
-
广播:Leader可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。
运行分析
在ZAB协议中,每一个进程都有可能处于以下三种状态之一。
-
LOOKING: Leader选举阶段
-
FOLLOWING: Follower服务器和Leader保持同步状态
-
LEADING: Leader服务器作为主进程领导状态
组成ZAB协议的所有进程启动的时候,其初始化状态都是LOOKING状态。而随后开始选举、同步和广播,在运行过程过程中,每个进程都会在LOOKING、FOLLOWING、LEADING状态之间不断地切换。
需要注意的是,只有完成了阶段二,即完成各个进程之间的数据同步之后,准Leader进程才能真正成为新的主进程周期中的Leader。而到了阶段三也就是原子广播阶段中,同一时刻,一个Follower只能和一个Leader保持同步,Leader进程与所有的Follower进程之间通过心跳检测机制来感知彼此的情况。
三、总结
ZAB协议和常见的(Redis、LevelDB等一些开源库)Raft协议实际上是有相似之处的,比如都有一个Leader,用来保证一致性(Paxos并没有使用 Leader机制保证一致性)。再有采取过半即成功的机制保证服务可用(实际上 Paxos和Raft都是这么做的)。
ZAB让整个ZooKeeper集群在两个模式之间转换,消息广播和崩溃恢复,消息广播可以说是一个简化版本的2PC,通过崩溃恢复解决了2PC 的单点问题,通过队列解决了2PC的同步阻塞问题。
而支持崩溃恢复后数据准确性的就是数据同步了,数据同步基于事务的ZXID的唯一性来保证。通过+ 1操作可以辨别事务的先后顺序。这个设计非常优秀,像Raft协议也有epoch周期,但是没有记录事务的。
参考资料:
《从Paxos到ZooKeeper:分布式一致性原理与实践》
https://www.cnblogs.com/stateis0/p/9062133.html
https://www.jianshu.com/p/2bceacd60b8a