1. Master选举

在分布式系统中,Master往往用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权,如在读写分离的应用场景中,客户端的写请求往往是由Master来处理,或者其常常处理一些复杂的逻辑并将处理结果同步给其他系统单元。利用Zookeeper的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。

基于zookeeper的高可用 zookeeper高可用原理_基于zookeeper的高可用

利用Zookeeper的强一致性,能够很好地保证分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。也就是说,如果同时有多个客户端请求同一个节点,那么最终只有一个请求能够创建成功。利用这个特性,就能够很轻易地在分布式环境中进行Master选举。

首先创建/master_election/2016-11-12节点,客户端集群每天会定时往该节点下创建临时节点,如/master_election/2016-11-12/binding,这个过程中,只有一个客户端能够成功创建,此时其变成master,其他节点都会在节点/master_election/2016-11-12上注册一个子节点变更的Watcher,用于监控当前的Master机器是否存活,一旦发现当前Master挂了,其余客户端将会重新进行Master选举。

2. 分布式锁

分布式锁用于控制分布式系统之间同步访问共享资源的一种方式,可以保证不同系统访问一个或一组资源时的一致性,主要分为排它锁和共享锁。如更不同的系统是同一个系统的内部不同的主机之间共享了一个或者一组资源,那么访问这些资源的时候,往往通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁。

2.1 分布式锁定义

分布式锁主要可以分为排他锁和共享锁。

排它锁又称为写锁或独占锁,若事务T1对数据对象O1加上了排它锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作,直到T1释放了排它锁。

共享锁又称为读锁,若事务T1对数据对象O1加上共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。

2.2 排他锁

基于zookeeper的高可用 zookeeper高可用原理_共享锁_02

  • 获取锁,在需要获取排它锁时,所有客户端通过调用接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。Zookeeper可以保证只有一个客户端能够创建成功,没有成功的客户端需要注册/exclusive_lock节点监听。
  • 释放锁,当获取锁的客户端宕机或者正常完成业务逻辑都会导致临时节点的删除,此时,所有在/exclusive_lock节点上注册监听的客户端都会收到通知,可以重新发起分布式锁获取。

基于zookeeper的高可用 zookeeper高可用原理_子节点_03

2.3 共享锁

和排他锁一样,同样是通过Zookeeper上的数据节点来表示一个锁,是一个类似于"/shared_lock/[Hostname]-请求类型-序号"的临时顺序节点,例如/shared_lock/192.168.0.1-R-0000000001.那么,这个节点就表示一个共享锁。

基于zookeeper的高可用 zookeeper高可用原理_基于zookeeper的高可用_04

① 获取锁,在需要获取共享锁时,所有客户端都会到/shared_lock下面创建一个临时顺序节点,如果是读请求,那么就创建例如/shared_lock/host1-R-00000001的节点,如果是写请求,那么就创建例如/shared_lock/host2-W-00000002的节点。

② 判断读写顺序,不同事务可以同时对一个数据对象进行读写操作,而更新操作必须在当前没有任何事务进行读写情况下进行,通过Zookeeper来确定分布式读写顺序,大致分为四步。

  1. 创建完节点后,获取/shared_lock节点下所有子节点,并对该节点变更注册监听。
  2. 确定自己的节点序号在所有子节点中的顺序。
  3. 对于读请求:若没有比自己序号小的子节点或所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取到共享锁,同时开始执行读取逻辑,若有写请求,则需要等待。对于写请求:若自己不是序号最小的子节点,那么需要等待。
  4. 接收到Watcher通知后,重复步骤1。

③ 释放锁,其释放锁的流程与独占锁一致。

上述共享锁的实现方案,可以满足一般分布式集群竞争锁的需求,但是如果机器规模扩大会出现一些问题,下面着重分析判断读写顺序的步骤3。

共享锁的流程图,如下:

基于zookeeper的高可用 zookeeper高可用原理_子节点_05

2.4 羊群效应

host1客户端在移除自己的共享锁后,Zookeeper发送了子节点更变Watcher通知给所有机器,然而除了给host2产生影响外,对其他机器没有任何作用。大量的Watcher通知和子节点列表获取两个操作会重复运行,这样会造成系能鞥影响和网络开销,更为严重的是,如果同一时间有多个节点对应的客户端完成事务或事务中断引起节点小时,Zookeeper服务器就会在短时间内向其他所有客户端发送大量的事件通知,这就是所谓的羊群效应。

可以有如下改动来避免羊群效应。

  1. 客户端调用create接口常见类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点。
  2. 客户端调用getChildren接口获取所有已经创建的子节点列表(不注册任何Watcher)。
  3. 如果无法获取共享锁,就调用exist接口来对比自己小的节点注册Watcher。对于读请求:向比自己序号小的最后一个写请求节点注册Watcher监听。对于写请求:向比自己序号小的最后一个节点注册Watcher监听。
  4. 等待Watcher通知,继续进入步骤2。

此方案改动主要在于:每个锁竞争者,只需要关注/shared_lock节点下序号比自己小的那个节点是否存在即可。

3. 分布式队列

分布式队列可以简单分为先入先出队列模型和等待队列元素聚集后统一安排处理执行的Barrier模型。

3.1 FIFO 先进先出队列模型

FIFO先入先出,先进入队列的请求操作先完成后,才会开始处理后面的请求。FIFO队列就类似于全写的共享模型,所有客户端都会到/queue_fifo这个节点下创建一个临时节点,如/queue_fifo/host1-00000001。

基于zookeeper的高可用 zookeeper高可用原理_共享锁_06


创建完节点后,按照如下步骤执行。

  1. 通过调用getChildren接口来获取/queue_fifo节点的所有子节点,即获取队列中所有的元素。
  2. 确定自己的节点序号在所有子节点中的顺序。
  3. 如果自己的序号不是最小,那么需要等待,同时向比自己序号小的最后一个节点注册Watcher监听。
  4. 接收到Watcher通知后,重复步骤1。

FIFO先进先出队列模型的流程图:

基于zookeeper的高可用 zookeeper高可用原理_子节点_07

3.2 Barrier:分布式屏障

Barrier指的就是障碍物、屏障,而在分布式系统中,特指系统之间的一个协调条件,规定了一个队列元素必须都集聚之后才能统一进行安排。

最终的合并计算需要基于很多并行计算的子结果来进行,开始时,/queue_barrier节点已经默认存在,并且将结点数据内容赋值为数字n来代表Barrier值,之后,所有客户端都会到/queue_barrier节点下创建一个临时节点,例如/queue_barrier/host1。

基于zookeeper的高可用 zookeeper高可用原理_Zookeeper框架_08

创建完节点后,按照如下步骤执行。

  1. 通过调用getData接口获取/queue_barrier节点的数据内容,如10。
  2. 通过调用getChildren接口获取/queue_barrier节点下的所有子节点,同时注册对子节点变更的Watcher监听。
  3. 统计子节点的个数。
  4. 如果子节点个数还不足10个,那么需要等待。
  5. 接受到Wacher通知后,重复步骤3。