众所周知,ZooKeeper 是一个开源的分布式协调服务,很多分布式的应用都是基于 ZooKeeper 来实现分布式锁、服务管理、通知订阅等功能。
那么 ZooKeeper 自身是如何在分布式环境下实现数据的一致性的呢?
结构
既然 ZooKeeper 是在分布式环境下提供服务的,那么它必须要解决的问题就是单点问题,因此 ZooKeeper 是一个主备的结构。ZooKeeper 存在 leader、follower、observer三种角色,这三种角色在实际服务集群中都是服务节点。
- leader:处理所有请求,为客户的提供读和写服务
- follower:只提供读服务,有机会通过选举成为leader
- observer:只提供读服务
由以上三种角色的介绍可知,ZooKeeper 中所有请求都是交给 leader 处理的,因此,如果leader挂了,ZooKeeper 就无法再提供服务,这就是单点问题。所幸在leader 挂了之后,followe r能够通过选举成为新的 leader。
那么问题来了,follower 是如何被选举成为新的 leader 的?新的 leader 又是如何保证数据的一致性的?这些问题的答案都在于 ZooKeeper 所用的分布式一致性协议ZAB。
ZAB协议
ZAB协议是为 ZooKeeper 专门设计的一种支持奔溃恢复的原子广播协议。虽然它不像 Paxos 算法那样通用通用,但是它却比 Paxos 算法易于理解。在我看来ZAB协议主要的作用在于三个方面:
- 选举出 leader
- 同步节点之间的状态达到数据一致
- 数据的广播
在了解ZAB协议具体过程之前不要先了解几个概念。
事务
ZooKeeper 作为一个分布式协调服务,需要 leader 节点去接受外部请求,转化为内部操作(比如创建,修改,删除节点),需要原子性执行的几个操作就组成了事务,这里用 T 代表。ZooKeeper 要求有序的处理事务,因此给每个事务一个编号,每进来一个事务编号加1,假设 leader 节点中进来 n 个事务,可以表示为 T1,T2,T3…Tn。为了防止单点问题导致事务数据丢失,leader 节点会把事务数据同步到 follower 节点中。
事务队列
leader 和 follower 都会保存一个事务队列,用 L 表示,L=T1,T2,T3…Tn,leader 的事务队列是接受请求保存下来的,follower 的事务队列是 leader 同步过来的,因此leader 的事务队列是最新,最全的。
任期
在 ZooKeeper 的工作过程中,leader 节点奔溃,重新选举出新的 leader 是很正常的事情,所以 ZooKeeper 的运行历史中会产生多个 leader,就好比一个国家的历史中会相继出现多为领导人。为了区分各个 leader,ZAB协议用一个整数来表示任期,我们假设用E表示任务。ZooKeeper 刚运行时选举出的第一个 leader 的任期为E=1;第一个 leader 奔溃后,下面选举出来的 leader,任期会加1,E=2;一次类推。加入任期概念的事务应该表示为 T(E,n)
协议过程
选举leader
- 每个 follower 广播自己事务队列中最大事务编号 maxId
- 获取集群中其他 follower 发出来的 maxId,选取出最大的 maxId 所属的 follower,投票给该 follower,选它为 leader
- 统计所有投票,获取投票数超过一半的 follower 被推选为 leader
同步数据
- 各个 follower 向 leader 发送自己保存的任期E
- leader,比较所有的任期,选取最大的E,加1后作为当前的任期E=E+1
- 将任务E广播给所有follower
- follower 将任期改为 leader 发过来的值,并且返回给 leader 事务队列 L
- leader 从队列集合中选取任期最大的队列,如果有多个队列任期都是最大,则选取事务编号 n 最大的队列 Lmax。将 Lmax 置为 leader 队列,并且广播给各个 follower。
- follower 接收队列替换自己的事务队列,并且执行提交队列中的事务。
至此各个节点的数据达成一致,ZooKeeper 恢复正常服务。
广播
- leader 节点接收到请求,将事务加入事务队列,并且将事务广播给各个 follower。
- follower 接收事务并加入都事务队列,然后给 leader 发送准备提交请求。
- leader 接收到半数以上的准备提交请求后,提交事务同时向 follower 发送提交事务请求
- follower 提交事务。