【深入 Zookeeper】— ZAB 协议
ZAB 协议
ZAB(Zookeeper Atomic Broadcast) 协议是为分布式分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。
在 Zookeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各副本之间数据的一致性。
ZAB 协议的核心是定义了对于那些会改变 ZooKeeper 服务器数据状态的事务请求的处理方式:
所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器称为 Leader 服务器,而余下的其他服务器则称为 Follower 服务器。Leader 服务器负责将一个客户端请求转换成一个事务 Proposal(提议),并将该提议分发给集群中所有的 Follower 服务器。之后 Leader 服务器需要等待所有 Follower 服务器的反馈,一旦超过半数的 Follower 服务器进行了正确的反馈后,那么 Leader 就会再次向所有的 Follower 服务器分发 commit 消息,要求其将前一个 Proposal 进行提交。
协议介绍
ZAB 协议包括两种基本的模式:崩溃恢复和消息广播。
当这个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入崩溃恢复模式并选举新的 Leader 服务器。当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式。
消息广播(数据一致性)
ZAB 协议的消息广播过程使用的是一个原子广播协议,类似于一个二阶段提交的过程。针对客户端的事务请求,非 Leader 服务器首先将这个事务请求转发给 Leader 服务器。Leader 服务器会为其生成对应的事务 Proposal。在广播 Proposal 之前,Leader 服务器首先会为这个事务分配一个全局单调递增的唯一 ID,称之为事务 ID(即 ZXID),接着将 Proposal 发送给集群中其他的所有机器,然后再分别收集各自的选票。每一个 Follower 服务器在接收到这个事务 Proposal 之后,都会首先将其以事务日志的形式写入到本地磁盘中,并且在写入成功后反馈给 Leader 服务器一个 ACK 响应。如果收到过半数的响应后(不需要等待集群中所有的 Follower 服务器都反馈响应),Leader 就会再次向所有的其他机器发送 commit 消息,要求将前一个 Proposal 进行提交。示意图如下:
总体流程如下:
- Follower 服务器将事务请求转发给 Leader 服务器
- Leader 服务器为该事务请求生成 Proposal,为 Proposal 分配 ZXID,广播 Proposal
- Follower 服务器收到 Proposal 后,以事务日志的形式写入本地磁盘,反馈 Leader 服务器 ACK 响应
- Leader 服务器接收到超过半数的反馈响应后,广播 Commit 消息,自身完成对事物的提交
- Follower 服务器接收到 commit 消息,完成对事物的提交
崩溃恢复(Master 选举)
ZAB 的原子广播协议在正常情况下运行良好,但天有不测风云,一旦 Leader 服务器挂掉或者由于网络原因导致与半数的 Follower 的服务器失去联系,那么就会进入崩溃恢复模式。整个恢复过程结束后需要选举出一个新的 Leader 服务器。
两个特性
根据 ZAB 消息广播的过程可知,如果一个事务 Proposal 在一台机器上被处理成功,那么就应该在所有的机器上处理成功,哪怕机器出现故障。所以在崩溃恢复过程结束后,为了保证新选举出来的 Leader 服务器能正常工作,需要保证 2 个基本特性:
- 确保那些已经在 Leader 服务器上提交的事务最终被所有的服务器都提交
- 确保丢弃那些只在 Leader 服务上被提出的事务
首先看第一个特性:确保那些已经在 Leader 服务器上提交的事务最终被所有的服务器都提交
。试想这么个场景:Leader 服务器在收到超半数的 ACK 返回响应后,本应该广播 commit 消息,但这时候 Leader 服务器挂掉了(Leader 服务器已经提交了事务?),这时候就会导致 Follower 服务器和 Leader 服务器数据不一致的情况。ZAB 协议就须确保这种况下,所有的 Follower 服务器也都成功提交该事物。
第二个特性:确保丢弃那些只在 Leader 服务上被提出的事务
。试想这么个场景:Leader 服务器在生成 Proposal 后就挂掉了,其他的服务器都没收到该 Proposal。于是,当该机器再次加入集群中的时候,需要确保丢弃该事物 Proposal。
所以上面两个特性总结就是下面两句话:
- 提交已经被 Leader 提交的事务
- 丢弃已经被跳过的事务
基于这两个特性,如果让新选举出来的 Leader 具有最大的 ZXID 的事务 Proposal,那么就可以保证该 Leader 一定具有所有已提交的提案。更为重要的是,如果让具有最高 ZXID 的事务 Proposal 的机器来成为 Leader,就可以省去 Leader 服务器检查 Proposal 的提交和丢弃工作这一步操作了。
数据同步
在完成 Leader 选举之后,在正式开始工作(即接收客户端的事务请求,然后提出新的天)之前,Leader 服务器首先会确保事务日志中的所有 Proposal 是否都已经被集群中过半的机器提交了,即是否完成数据同步。
对于那些没有被 Follower 服务器提交的事务,Leader 会为每个 Follower 服务器准备一个队列,并将那些没有被各 Follower 服务器同步的事务已 Proposal 消息的形式逐个发送给 Follower 服务器,并在每一个 Proposal 消息后面紧接着再发送一个 commit 消息,以表示该事务已被提交。等到 Follower 服务器将所有未同步的事务 Proposal 都从 Leader 服务器上同步过来并应用到本地数据库,Leader 服务器就该 Follower 服务器加入到真正可用的 Follower 列表中。
那 ZAB 是如何处理那些需要被丢弃的事务 Proposal 呢?ZXID 是一个 64 位的数字,其中低 32 位可看作是计数器,Leader 服务器每产生一个新的事务 Proposal 的时候,都会该计数器进行加 1 操作。而高 32 位表示 Leader 周期 epoch 的编号,每当选举一个新的 Leader 服务器,就会从该服务器本地的事务日志中最大 Proposal 的 ZXID 中解析出对应的 epoch 值,然后对其加 1 操作,这个值就作为新的 epoch 值,并将低 32 位初始化为 0 来开始生成新的 ZXID。
基于这样的策略,当一个包含上一个 Leader 周期中尚未提交的事务 Proposal 的服务器启动时,已 Follower 角色加入集群中之后,Leader 服务器会根据自己服务器上最后被提交的 Proposal 来和 Follower 服务器的 Proposal 进行比对,比对结果就是 Leader 会要求 Follower 进行一个回退操作——回退到一个确实已经被集群中过半机器提交的最新的事务 Proposal。
Master 选举实现细节
上文说过,新选举出来的 Leader 具有最大的 ZXID 的事务 Proposal,那这是怎么实现的呢?
ZAB 默认采用 TCP 版本的 FastLeaderElection 选举算法。在选举投票消息中包含了两个最基本的信息:所推举的服务器 SID 和 ZXID,分别表示被推举服务器的唯一标识(每台机器不一样)和事务 ID。假如投票信息为 (SID, ZXID)的形式。在第一次投票的时候,由于还无法检测集群其他机器的状态信息,因此每台机器都将自己作为被推举的对象来进行投票。每次对收到的投票,都是一个对投票信息(SID, ZXID)对比的过程,规则如下:
- 如果接收到的投票 ZXID 大于自己的 ZXID,就认可当前收到的投票,并再次将该投票发送出去。
- 如果 ZXID 小于自己的 ZXID,那么就坚持的投票,不做任何变更。
- 如果 ZXID 等于自己的 ZXID,再对比 SID,比自己大,就认可当前收到的投票,再讲该投票发送出去;如果比自己小,那就坚持自己的投票,不做变更。
经过第二次投票后,集群中每台机器都会再次收到其他机器的投票,然后开始统计,如果一台机器收到超过了半数的相同投票,那么这个投票对应的 SID 机器即为 Leader。
简单来说,通常哪台服务器上的数据越新,那么越有可能成为 Leader。原因很简答,数据越新,也就越能够保证数据的恢复。当然,如果集群中有几个服务器具有相同的 ZXID,那么 SID 较大的那台服务器成为 Leader。