上一节介绍了ZAB协议的内容,本节将从系统模型、问题描述、算法描述和运行分析四方面来深入了解 ZAB 协议。
系统模型
在一个由一组进程 n ={P1,P2,...Pn}组成的分布式系统中,每一个进程都具有各自的存储设备,各进程之间通过相互通信来实现消息的传递。每一个进程都随时有可能会出现一次或多次的崩溃退出,这些进程会在恢复之后再次加人到进程组 n 中去。如果一个进程正常工作,那么我们称该进程处于 UP 状态,如果一个进程崩溃了,那么我们称其处于 DOWN 状态。事实上,当集群中存在过半的处于 UP 状态的进程组成一个进程 f 集之后,就可以进行正常的消息广播了。我们将这样的一个进程子集称为 Quorum (下文中使用“ Q ”来表示),并假设这样的 Q 已经存在,其满足:
上述集合关系式表示,存在这样的一个进程子集 Q ,其必定是进程组n的子集,同时,存在任意两个进程子集 Q ,和 Q 2 ,其交集必定非空,用 Pi 和 Pj 来分别表示进程组 n 中的两个不同进程,使用Cij来表示进程Pi和Pj之间的网络通信通道。
问题描述
ZAB 协议是整个 ZooKeeper 框架的核心所在,其规定了任何时候都需要保证只有一个主进程负责进行消息广播,而如果主进程崩溃了,就需要选举出一个新的主进程。主进程的选举机制和消息广播机制是紧密相关的。随着时间的推移,会出现无限多个主进程并构成一个主进程序列: P1,P2, …,Pe,e表示主进程序列号,也被称作主进程周期。由于各个进程都会发生崩溃然后再次恢复,因此会出现这样的情况:存在这样的Pi和Pj,它们本质上是同一个进程,只是处于不同的周期中。
主进程周期:
为了保证主进程每次广播出来的事务消息都是一致的,我们必须确保 ZAB 协议只有在充分完成崩溃恢复阶段之后,新的主进程才可以开始生成新的事务消息广播。为了实现这个目的,各个进程都实现了类似于 ready ( e )的一个函数调用,在运行过程中, ZAB 协议能够非常明确地告知上层系统(指主进程和其他副本进程)是否可以开始进行事务消息的广播,同时,在调用 ready ( e )函数之后, ZAB 还需要为当前主进程设置一个实例值。实例值用于唯一标识当前主进程的周期,在进行消息广播的时候,主进程使用该实例值来设置事务标识中的 epoch 字段,即事务Proposal的ZXID的高32位值。如果一个主进程周期 e 早于另一个主进程周期 e ’,将其表示为e<e ’。
事务:
假设各个进程都存在一个类似于transactions(v,z)的函数调用,来实现主进程对状态变更的广播。主进程每次对transaction(v,z)函数的调用都包含了两个字段:事务内容v和事务标识 z(即zxid) ,而每一个事务标识 z = < e , c >也包含两个组成部分,前者是主进程周期e,后者是当前主进程周期内的事务计数c。我们使用epoch( Z )来表示一个事务标识中的主进程周期epoch,使用counter(z)来表示事务标识中的事务计数。
针对毎一个新的事务,主进程都会首先将事务计数c递增。在实际运行过程中,如果一个事务标识z优先于另一个事务标识z’,有两种情况:一种情况是主进程周期不同,即epoch(z) < epoch(z’); 另一种情况是主进程周期一致,但是事务计数不同,
即 epoch(z) = epoch(z‘)且 counter(z) < counter(z’),两种情况均用 z<z‘来表示。
算法描述:
整个ZAB协议主要包括消息广播和崩溃恢复两个过程,进一步可以细分为三个阶段,分别是发现(Discovery)、同步(Synchronization)和广播(Broadcast)阶段。组成ZAB协议的每一个分布式进程,会循环地执行这三个阶段,我们将这样一个循环称为一个主进程周期。
ZAB协议算法表述语介绍:
阶段一:发现(Discovery)
步骤F.1.1 Follower F将自己最后接受的事务 Proposal的epoch值 CEPOCH(F. p) 发送给准 Leader L 。
步骤L.1.1当接收到来自过半Follower的CEPOCH(F.p)消息后,准Leader L会生成NEWEPOCH(e)消息给这些过半的Follower。
关于这个epoch值e‘,准Leader L会从所有接收到的CEPOCH(F. p )消息中选取出最大的epoch值,然后对其进行加1操作,即为e’。
步骤F.1.2当 Follower接收到来自准Leader L的NEWEPOCH(e‘)消息后,如果其检测到当前的CEPOCH(F.p )值小于e’,那么就会将CEPOCH(F.p)赋值为e‘,同时向这个准LeaderL反馈Ack消息。在这个反馈消息(ACK-E(F. p ,hf ))中,包含了当前该Follower的 epoch CEPOCH(F.p),以及该Follower的历史事务Proposal集合:hf。
当Leader L接收到来自过半Follower的确认消息Ack之后,Leader L就会从这过半服务器中选取出一个Follower F,并使用其作为初始化事务集合Ie。
关于这个 Follower F 的选取,对于Quorum中其他任意一个Follower F‘,F需要满足以下两个条件中的一个:
CEPOCH (F‘. p) < CEPOCH (F. p)
(CEPOCH (F‘.p) = CEPOCH (F.p)) & ( F‘. zxid<F. zxid 或 F‘. zxid=F. zxid)
阶段二:同步
步骤 L.2.1 Leader L 会将 e‘ 和 Ie’ 以 NEWLEADER(e’,Ie’) 消息的形式发送给所有 Quorum中的 Follower 。
步骤 F.2.1 当 Follower 接收到来自 Leader L 的 NEWLEADER(e’,Ie’) 消息后,如果Follower 发现 CEPOCH (F.p) 不等于e‘, 那么直接进入下一轮循环,因为此时Follower 发现自己还在上一轮,或者更上轮,无法参与本轮的同步。
如果CEPOCH (F.p) = e’ ,那么Follower就会执行事务应用操作。
最后,Follower会反馈给Leader,表明自己已经接受并处理了所有Ie中的事务 Proposal。
步骤L.2.2 当Leader接收到来自过半Follower针对NEWLEADER(e’,Ie’)的反馈消息后,就会向所有的Follower发送Commit消息。至此Leader完成阶段二。
步骤F.2.2 当Follower收到来自Leader的Commit消息后,就会依次处理并提交所有在Ie’中未处理的事务。至此Follower完成阶段二。
阶段三:广播
步骤L.3.1 Leader L接收到客户端新的事务请求后,会生成对应的事务Proposal,并根据ZXID的顺序向所有Follower发送提案<e’,<v,z>> ,其中epoch(z)=e’。
步骤F.3.1 Follower根据消息接收的先后次序来处理这些来自Leader的事务Proposal,并将他们追加到hf中去,之后再反馈给Leader。
步骤L.3.1当Leader接收到来自过半Follower针对事务Proposal<e’,<v,z>>的Ack消息后,就会发送Commit<e,<v,z>>消息给所有的Follower,要求它们进行事务的提交。
步骤F.3.2 当Follower F接收到来自Leader的Commit <e’,<v, z>>消息后,就会开始提交事务Proposal <e,<v,z>>。