[TOC]
什么是Zab协议?
Zab协议 的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播)。
Zookeeper 是通过 Zab 协议来保证分布式事务的最终一致性。
Zab协议是为分布式协调服务Zookeeper专门设计的一种 支持崩溃恢复 的 原子广播协议 ,是Zookeeper保证数据一致性的核心算法。Zab借鉴了Paxos算法,但又不像Paxos那样,是一种通用的分布式一致性算法。它是特别为Zookeeper设计的支持崩溃恢复的原子广播协议。
在Zookeeper中主要依赖Zab协议来实现数据一致性,基于该协议,zk实现了一种主备模型(即Leader和Follower模型)的系统架构来保证集群中各个副本之间数据的一致性。
这里的主备系统架构模型,就是指只有一台客户端(Leader)负责处理外部的写事务请求,然后Leader客户端将数据同步到其他Follower节点。
Zookeeper 客户端会随机的链接到 zookeeper 集群中的一个节点,如果是读请求,就直接从当前节点中读取数据;如果是写请求,那么节点就会向 Leader 提交事务,Leader 接收到事务提交,会广播该事务,只要超过半数节点写入成功,该事务就会被提交。
Zab 协议的特性:
1)Zab 协议需要确保那些已经在 Leader 服务器上提交(Commit)的事务最终被所有的服务器提交。
2)Zab 协议需要确保丢弃那些只在 Leader 上被提出而没有被提交的事务。
Zab 协议实现的作用
1)使用一个单一的主进程(Leader)来接收并处理客户端的事务请求(也就是写请求),并采用了Zab的原子广播协议,将服务器数据的状态变更以 事务proposal (事务提议)的形式广播到所有的副本(Follower)进程上去。
2)保证一个全局的变更序列被顺序引用。
Zookeeper是一个树形结构,很多操作都要先检查才能确定是否可以执行,比如P1的事务t1可能是创建节点"/a",t2可能是创建节点"/a/bb",只有先创建了父节点"/a",才能创建子节点"/a/b"。
为了保证这一点,Zab要保证同一个Leader发起的事务要按顺序被apply,同时还要保证只有先前Leader的事务被apply之后,新选举出来的Leader才能再次发起事务。
3)当主进程出现异常的时候,整个zk集群依旧能正常工作。
Zab协议原理
Zab协议要求每个 Leader 都要经历三个阶段:发现,同步,广播。
- 发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。
- 同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
- 广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。
Zab协议核心
Zab协议的核心:定义了事务请求的处理方式
1)所有的事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被叫做 Leader服务器。其他剩余的服务器则是 Follower服务器。
2)Leader服务器 负责将一个客户端事务请求,转换成一个 事务Proposal,并将该 Proposal 分发给集群中所有的 Follower 服务器,也就是向所有 Follower 节点发送数据广播请求(或数据复制)
3)分发之后Leader服务器需要等待所有Follower服务器的反馈(Ack请求),在Zab协议中,只要超过半数的Follower服务器进行了正确的反馈后(也就是收到半数以上的Follower的Ack请求),那么 Leader 就会再次向所有的 Follower服务器发送 Commit 消息,要求其将上一个 事务proposal 进行提交。
Zab协议内容
Zab 协议包括两种基本的模式:崩溃恢复 和 消息广播
协议过程
当整个集群启动过程中,或者当 Leader 服务器出现网络中弄断、崩溃退出或重启等异常时,Zab协议就会 进入崩溃恢复模式,选举产生新的Leader。
当选举产生了新的 Leader,同时集群中有过半的机器与该 Leader 服务器完成了状态同步(即数据同步)之后,Zab协议就会退出崩溃恢复模式,进入消息广播模式。
这时,如果有一台遵守Zab协议的服务器加入集群,因为此时集群中已经存在一个Leader服务器在广播消息,那么该新加入的服务器自动进入恢复模式:找到Leader服务器,并且完成数据同步。同步完成后,作为新的Follower一起参与到消息广播流程中。
协议状态切换
当Leader出现崩溃退出或者机器重启,亦或是集群中不存在超过半数的服务器与Leader保存正常通信,Zab就会再一次进入崩溃恢复,发起新一轮Leader选举并实现数据同步。同步完成后又会进入消息广播模式,接收事务请求。
保证消息有序
在整个消息广播中,Leader会将每一个事务请求转换成对应的 proposal 来进行广播,并且在广播 事务Proposal 之前,Leader服务器会首先为这个事务Proposal分配一个全局单递增的唯一ID,称之为事务ID(即zxid),由于Zab协议需要保证每一个消息的严格的顺序关系,因此必须将每一个proposal按照其zxid的先后顺序进行排序和处理。
消息广播
1)在zookeeper集群中,数据副本的传递策略就是采用消息广播模式。zookeeper中农数据副本的同步方式与二段提交相似,但是却又不同。二段提交要求协调者必须等到所有的参与者全部反馈ACK确认消息后,再发送commit消息。要求所有的参与者要么全部成功,要么全部失败。二段提交会产生严重的阻塞问题。
2)Zab协议中 Leader 等待 Follower 的ACK反馈消息是指“只要半数以上的Follower成功反馈即可,不需要收到全部Follower反馈”
消息广播具体步骤
1)客户端发起一个写操作请求。
2)Leader 服务器将客户端的请求转化为事务 Proposal 提案,同时为每个 Proposal 分配一个全局的ID,即zxid。
3)Leader 服务器为每个 Follower 服务器分配一个单独的队列,然后将需要广播的 Proposal 依次放到队列中取,并且根据 FIFO 策略进行消息发送。
4)Follower 接收到 Proposal 后,会首先将其以事务日志的方式写入本地磁盘中,写入成功后向 Leader 反馈一个 Ack 响应消息。
5)Leader 接收到超过半数以上 Follower 的 Ack 响应消息后,即认为消息发送成功,可以发送 commit 消息。
6)Leader 向所有 Follower 广播 commit 消息,同时自身也会完成事务提交。Follower 接收到 commit 消息后,会将上一条事务提交。
zookeeper 采用 Zab 协议的核心,就是只要有一台服务器提交了 Proposal,就要确保所有的服务器最终都能正确提交 Proposal。这也是 CAP/BASE 实现最终一致性的一个体现。
Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往队列中发消息即可。如果使用同步的方式会引起阻塞,性能要下降很多。
崩溃恢复
一旦 Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进入崩溃恢复模式。
在 Zab 协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的 Leader 服务器。因此 Zab 协议需要一个高效且可靠的 Leader 选举算法,从而确保能够快速选举出新的 Leader 。
Leader 选举算法不仅仅需要让 Leader 自己知道自己已经被选举为 Leader ,同时还需要让集群中的所有其他机器也能够快速感知到选举产生的新 Leader 服务器。
崩溃恢复主要包括两部分:Leader选举 和 数据恢复
Zab 协议如何保证数据一致性
假设两种异常情况:
1、一个事务在 Leader 上提交了,并且过半的 Folower 都响应 Ack 了,但是 Leader 在 Commit 消息发出之前挂了。
2、假设一个事务在 Leader 提出之后,Leader 挂了。
要确保如果发生上述两种情况,数据还能保持一致性,那么 Zab 协议选举算法必须满足以下要求:
Zab 协议崩溃恢复要求满足以下两个要求:
1)确保已经被 Leader 提交的 Proposal 必须最终被所有的 Follower 服务器提交。
2)确保丢弃已经被 Leader 提出的但是没有被提交的 Proposal。
根据上述要求
Zab协议需要保证选举出来的Leader需要满足以下条件:
1)新选举出来的 Leader 不能包含未提交的 Proposal 。
即新选举的 Leader 必须都是已经提交了 Proposal 的 Follower 服务器节点。
2)新选举的 Leader 节点中含有最大的 zxid 。
这样做的好处是可以避免 Leader 服务器检查 Proposal 的提交和丢弃工作。
Zookeeper是什么,我们看看官方的定义:
ZooKeeper: A Distributed Coordination Service for Distributed Applications。
即ZK是为分布式系统提供协调服务的一个系统。那么什么场景需要协调呢?
ZK本质上是一个基于内存的KV系统,但与一般KV系统不同的是ZK是以Path来作为Key,以DataTree树视图来组织这些Path。
ZK内部用ConcurrentHashMap<String /Path/, DataNode>来维持所有的Path,每个DataNode会挂一个子节点的Path列表。**
所以ZK对某个Path的插入和查询性能很高,并不需要遍历什么树,是直接对HashMap的操作。
ZK的可用性和可靠性的核心基础便是ZAB(Zookpeeper原子广播协议),也就是本文和大家分享的点儿。
我们会围绕《ZooKeeper’s atomic broadcast protocol: Theory and practice》这个论文来讲解ZAB。掌握了ZAB基本也就掌握了Zookeeper的精髓之处。
概述
ZAB协议是为Zookeeper专门设计的一种支持奔溃恢复的原子广播协议。他不像Paxos算法是一种通用的分布式一致性算法,所以不能把ZAB和Paxos进行等同。
在ZK中,主要依赖ZAB来实现分布式数据的一致性,ZK本质上是一种主备模式(Leader-Follower-Observer)的系统架构来保持集群中各副本之间的一致性,即单点写Leader。同时也能在Leader crash后进行恢复重新选主。
ZAB协议的核心是定义了那些会改变Zookeeper服务器数据状态的事务请求(Transaction)的处理方式,即:
所有事务请求必须由一个全局唯一的服务器(Leader)来协调处理,剩余的服务器称为Follower(由权利参与选主)或者Observer(只负责从Leader同步数据,用于读扩展和分担集群整体连接数)。
所有事务请求必须由一个全局唯一的服务器(Leader)来协调处理,剩余的服务器称为Follower(由权利参与选主)或者Observer(只负责从Leader同步数据,用于读扩展和分担集群整体连接数)。
Leader负责将一个客户端事务请求转换成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower节点(Leader会维持一个Follower列表)。之后Leader需要等待Follower的反馈,一旦超过半数的Follower进行了正确的反馈,Leader则会对所有Follower发起Commit消息,要求所有Follower将该Proposal进行提交(即写入Transaction Log)。
一、Crash-recovery system model (奔溃恢复模型)
Zookeeper是一个crash-recovery系统模型。即有能力从奔溃状态中自我恢复,比如某个节点挂掉或者Leader挂掉,只要过半的节点存活并且能通信就能保证Zookeeper的高可用和高可靠能力。
假设系统是由N个进程组成 Π = {p1, p2, . . . , pN },每个进程称为Peer,Peer之间能相互通信,并且各自有自己的存储设备来存储事务日志和DataTree快照(Snapshot)。
Q (quorum of Π)表示进程集合中的过半节点集合,满足 Q ⊆ Π 和 |Q| > N/2。任何的两个Q必然会有Peer交集。
进程(Process)有两种状态:up和down。ZK节点什么时候提供写能力呢?一个Follower节点挂了重启后不是立马就能对外响应请求的,因为Follower落后Leader的Proposal需要进行同步。只有同步完成后才能对外提供服务。那么Peer crash后到恢复阶段称为down状态,从恢复阶段到下次crash称为up状态。
二、Expected properties (ZAB所具备基本属性)
奔溃恢复模型中,必须保证同一时间只有一个Leader存在,每个时间(epoch)的Leader节点可以是不一样的,比如 , ρe ∈ Π。epoch用int来表示。
每个Proposal用<v, z>表示,其中v是提交的新状态值,z则是zxid。zxid唯一标示一个事务请求(Transaction),一个事务请求有两种状态,proposed和commited,proposed表示一个事务已经被Leader提出,但尚未被Quorum所ACK, commited表示这个Transaction已经被Quorum所ACK,Leader对所有的follower发出了commit的请求,所有节点进行了本地commit操作。
为了满足Zookeeper的副本一致性,ZAB需要满足的一致性基本条件:
- Integrity(正确性):如果有节点收到commit proposal的请求,那么肯定是有节点进行了广播。即proposal不能是拜占庭错误。
- Total order:全局顺序性,即某个Peer提交了两个Proposal,<v, z>和<v', z'>。 如果<v, z>比<v', z'>先被提交,那么从任何其他Peer上看,也必然是<v, z>比<v', z'>先被提交。
- Agreement:如果Pi提交了<v, z>, Pj提交了<v', z'>,那么要不pi已经提交了了<v', z'>, 要不pj已经提交了<v, z>
另外,对于Leader来说,需要满足的顺序性属性为:
- Local primary order(本地顺序性):如果Leader在原子广播阶段先后commit了<v, z>和<v', z'>, 如果一个Follower commit了<v', z'>, 那么Follower肯定先commit了<v, z>。
- Global primary order(全局一致性):如果ρi是epoch为i的leader,ρj 是epoch为j的leader,而且i < j, 表示 ρi是之前的leader, ρj是之后的leader。ρi commit了<v, z>, pj commit了<v', z'>, 如果leader commit了<v, z> 和 <v', z'>, 那么肯定是<v, z> 先于 <v', z'>被commit.
- Primary integrity (正确性):ρi 如果广播了<v, z> ,并且其他Follower commit了<v', z'>,而<v', z'> 是pj先于pi提交的,即pj的epoch比pi的epoch小,那么pi肯定也commit了<v, z>。
三、Atomic broadcast protocol(原子广播协议)
在ZAB协议中,每个Peer有三种可能的状态:following、leading、election。其中处于following的Peer称为Follower,处于leading的Peer称为Leader.
ZAB协议整体分为四个阶段:0)Leader election 、 1)discovery、 2)synchronization、 3)broadcast
其中处于阶段0-阶段2的Zookeeper集群还处于不可用的状态,即不能响应客户端的读写请求操作。只有选出主,完成上一个epoch期间的proposal的广播以及补齐已经commit的proposal后,整个集群才能对外服务。
也就是只有处于阶段3即原子广播阶段才能对外服务。
ZAB的每个阶段是顺序推进的,如果在阶段1-3任何一个阶段出现故障,比如失败或者超时,则会进入阶段0,重新再来一轮。
先看几个概念:
- zxid:zxid作为Zookeeper最核心的一个概念,唯一标识一个transaction,全局唯一递增的64位正数。即proposal表示为<value, zxid>, zxid由 <epoch, count>构造,
- epoch:每个leader生命周期的一个标识,简单来说就是年号,newEpoch等于上一个epoch + 1,即newEpoch = lastEpoch + 1,zxid的高32位;
- count:标识每个epoch期间的每个transaction id,每个count都从0开始加一递增,zxid的低32位。
- zxid的对比:我们称zxid <e, c> 大于 zxid'<e', c'>,当满足 (e > e' ) || (e = e' & c > c').
每个Peer会存储4个核心变量:
- history:被Peer提交的历史proposal
- acceptedEpoch:接收最新NEWEPOCH的epoch
- currentEpoch:接收最新NEWLEADER的epoch
- lastZxid:history中最近一个提交的proposal的zxid
简单的来说,acceptedEpoch用于Discovery阶段来判断要不要接收先的NEWEPOCH。currentEpoch用于存储上个epoch的值。
这几个值都会进行存储,其中acceptedEpoch和currentEpoch会存储在磁盘上,history和lastZxid可以从DataTree的snapshot中恢复。
阶段0: Leader Election
这个阶段的目的是选出一个Leader,然后进入到后续的阶段。具体的算法,我们在Zookeeper的实现小节中阐述,也就是FLE(Fast Leader Election)算法。
主流程:
- Follower节点F知道准Leader节点后,发送一个FOLLOWERINFO类型消息,将自己的信息上报给准Leader,该信息包括自己的epoch内容F. acceptedEpoch
- Leader节点L等待收到过半的FOLLOWERINFO消息后,从这些F.acceptedEpoch中取出最大的epoch,并且加1,即newEpoch = max {F. acceptedEpoch} + 1, 然后将新的epoch信息NEWEPOCH发给Quorum中的节点,等待Quorum的ACK确认
- Follower节点收到NEWEPOCH后,将新的epoch与自己的epoch比较
a. 如果准Leader提过来的新epoch > acceptedEpoch, 即接收新的epoch。更新自己的acceptedEpoch为新epoch,然后给Leader回复一个
ACKEPOCH信息,该信息包括上个epoch,history和lastZxid。
b. 如果准Leader提过来的新epoch < acceptedEpoch,则回退到阶段0 - Leader收到Quorum中所有follower的ACKEPOCH后,从所有的follower找出currentEpoch最大的或者lastZxid最大的follower,然后把该 follower的history作为自己的history。
有点需要注意的是,Quorum是包括Leader自身的。这里的Leader还只是准Leader。
总结下来:
Discovery的目的就是确定一个新leader的epoch值,然后找到上个epoch周期内的拥有最大zxid的follower节点,然后进入同步阶段,把这个history进行重 新同步。
之所以可以取最大zxid作为新的leader的history,是基于一个假设,因为zxid是全局递增的,也就是拥有最大zxid的节点也用了最新的proposal提交记录。
阶段2:Synchronization
同步阶段的目的就是上一个阶段的时候Leader拿到了最新的history,需要进行同步给所有Follower,处理上一个epoch阶段遗留proposal,该commit的 commit,该清理的清理.
主流程:
- 在上阶段准Leader拿到过半的ACKEPOCH后,也就是有了最新的proprosal history。
- Leader给所有Follower发一个NEWLEADER类型消息,把最新的epoch和histroy带过去。
- Follower收到NEWLEADER消息后,判断当选举轮次自己的acceptedEpoch是否和新epoch相同
a. Follower的acceptedEpoch和新epoch相同,表示自己已经跟上了新的epoch,那么
i. 更新自己的currentEpoch为新的epoch,表示进入新的朝代了,
ii. 按照zxid的大小逐一进行本地proposed,此时这些transaction还未commit iii. 更新自己的history为最新的history
iv. 返回一个ACKNEWLEADER给Leader,表示本Follower已经同步完数据
b. 如果Follower的acceptedEpoch和新epoch不相同,那么退回到阶段0,重新进行选主
- Leader收到Quorum中节点的ACKNEWLEADER后,对history这些proposal进行commit,所有Follower节点即收到commit请求
- Follower收到Leader的对history的COMMIT消息后,对于outstanding的事务(即已经proposed,但是还未commit的transaction),进行按
zxid顺序进行commit。 - LEADER和FOLLOWER都同步完成后进入阶段3.
阶段3:Broadcast
如果Peers都安然无恙,那么会永远停留在这个阶段,也就是原子广播阶段,该阶段才是真正对外服务的阶段,也就是开始接收外界写请求(Transaction) 了。
这个阶段不可能会存在双主,这个阶段也会又新的Follower或者Observer加入进来。
主流程:
Leader接收到一个write请求后,生成一个新的proposal <value, zxid>, zxid = lastZxid + 1. 然后对Quorum中的follower发起propose请求
- Follower收到新的propose后,讲propose放到自己的history队列中。返回返回ACK给leader,表示我准备好了。
Leader收到过半的针对propose的ACK后,认为获得了大部分的同意,则进行commit,对所有follower发起COMMIT请求。
Follower收到propose <v, z> 的commit后,开始提交。
a. 但是为了满足zxid的全局一致性,如果此时存在比该zxid更小的zxid‘还未提交,那么需要等待zxid’的propose <v', z'>的commit。
b. 等待比zxid都要小的zxid‘都commit后,zxid开始进行本地提交。-
在原子广播阶段,Leader也能接收新的Follower加入,然后放到自己的Quorum中。这块就比较简单了。
a. 新加入的节点会给Leader发一个FOLLOWERINFO请求
b. Leader收到FOLLOWERINFO请求后,会回NEWEPOCH和NEWLEADER,即告诉他当前的epoch和当前的history c. 新节点收到NEWLEADER后,如果正常逻辑处理后,回一个ACKNEWLEADER给Leader
d. Leader收到ACKNEWLEADER后,给该新节点一个COMMIT请求,让新节点提交history
e. Leader最后把新加入的Follower节点放入自己的Quorum列表中。
注意点:
- 整个propose其实是并行的,对于Leader来说,一个proposal不会等上一个commit才会发起新proposal的propose。
- 每个Peer进行本地commit proposal的时候是有序的,即zxid小的需要先commit。这也是为了保障全局顺序性。
四、Implementation (算法实现)
在Zookeeper实现过程中,对上述的几个阶段进行了优化。如下:
其中的Fast Leader Election阶段的算法核心:尝试选出一个Peer作为Leader,该Peer拥有最新的history数据即最大的lastZxid。那么就可以把Discovery阶 段省掉,所以实现上就简化成了三阶段:
1)Fast Leader Election 、2)Recovery 、3)Broadcast
另外Recovery阶段即恢复阶段的算法就需要做调整了,如下:
Recovery阶段:
主流程:
- Leader当前已经通过FAST LEADER ELECTION阶段选出,即该Leader已经拥有最大的zxid。
- Leader从lastZxid中拿出epoch进行加1作为新的epoch,count低32位重置为0,即 LastZxid ← {lastZxid.epoch + 1, 0}
- Follower节点连接上准Leader后,发送FOLLOWERINFO消息,并且把自己的lastZxid带上
a. 如果Leader拒绝了连接,可能是Leader的epoch比自己的小等原因,那么重新Follower状态设置回election,回退到阶段0,即FLE阶段