Zookeeper应用场景和ZAB协议

Zookeeper应用场景

数据发布/订阅(配置中心)

我们平常的开发过程中,经常会碰到这样的需求:系统中需要一些通用的配置信息,如一些运行时的开关、前端需要展示的通知信息、数据库配置信息等等。这些需求通常都要求具备3个特性:

  • 数据量比较小
  • 数据内容在运行时会变化
  • 集群中的所有机器共享,数据一致

我们假设把这些数据存储在应用的内存中,那么数据共享是一个问题,数据变更的通知又是一个问题。

那我们使用Zookeeper怎么实现呢?

  1. 配置存储

我们可以先把数据存储在Zookeeper的一个节点上。如/app/database_config
2. 配置获取

应用启动时,首先会去前面存储的节点上去拿数据,并且在这个节点上注册一个数据变更的Watcher监听。一旦发送数据变化,集群中所有应用都能收到通知
3. 配置变更

我们需要修改的时候,就利用Zookeeper的修改内容接口对节点上的数据进行修改即可。Zookeeper会自动帮我们发送数据变更通知。

命名服务

命名服务是分布式系统中比较常见的一个场景。比如我们需要在创单的时候给这个订单生成一个全局唯一的订单号。如果是单体应用那么还比较好做,数据库就有自增ID,再拼上一些订单号前缀就可以了。但是如果是分布式环境下,就会麻烦一些。

那我们使用Zookeeper怎么实现呢?

  • 利用Zookeeper的顺序节点能够维护每个数据节点的顺序的特性,就可以做到。

Master选举

我们有时候有这样的一个需求,在集群环境下,只需要一个应用来处理某个耗时资源。我们可以怎么处理呢?

先假设我们通过数据库来处理,我们向一个表中插入同样的一个id的数据,根据数据库主键的唯一特性,只有一个应用能够操作成功,那么这个应用就可以被选中成为Master,来执行这个耗时的操作。

但是我们考虑下面这个问题:如果被选中的这个Master机器宕机了,怎么告诉其他机器重新选举呢?不好实现。

利用Zookeeper可以实现这样的需求。

  1. 我们所有的应用同时在Zookeeper上创建一个临时节点,如:/master/binding,只有一个应用能够创建成功,我们称他为master应用,那么其他没创建成功的应用可以监听这个节点。
  2. 一旦Master应用宕机了,这个节点因为是临时节点会被自动删除,就将通知到其他的应用进行重新选举。

分布式锁

Zookeeper是一个比较知名的实现分布式锁的一个方案。我们分排他锁和共享锁两类来讲解。

排他锁

排他锁,又称写锁或独占锁。如果事务T1对数据对象A加了排他锁,那么整个加锁期间,其他事务都不能对这个对象A进行任何类型的操作,知道事务T1释放了排他锁。

下⾯我们就来看看如何借助ZooKeeper实现排他锁:

  1. 获取锁。我们先定义一个临时节点作为锁的节点。如:/order/lock。在执行时所有应用都会来创建这个节点,但是只有一个应用能够创建成功。创建成功的应用我们认为他获取到了锁。此时它可以执行它的业务方法。同时没有获取到锁的应用可以注册一个Watcher监听,以便实时监听节点变化
  2. 释放锁。当业务执行完毕后,我们要主动删除这个临时节点。此时其他的应用就可以去重新获取锁。

当持有锁的机器宕机了,也不会造成死锁。因为临时节点会自动删除。

也可以创建临时有序节点,只监听比自己小一位的那个子节点。避免持有锁的节点执行完释放锁的时候就需要通知所有监听着的应用(羊群效应)。

共享锁

共享锁,又称读锁。如果事务T1对对象A加了共享锁,那么当前事务只能对A进行读操作。其他事务T2、T3也能对这个对象加共享锁。

  1. 我们需要在节点上定义是读操作还是写操作。如/order/lock-W-0000001 、/order/lock-R-0000002。
  2. 应用调用创建节点接口,来创建临时顺序节点
  3. 应用调用getChildren接口获取所有已创建的所有子节点列表
  4. 如果应用是写请求,判断它是不是最小的节点的话,如果是就获取锁成功。否正向比它序号小的前一个节点注册监听
  5. 如果应用是读操作,判断它是不是最小的节点,如果是最小的节点或者比它小的都是读节点,就获取锁成功。否则向比自己序号小的最后一个写节点注册监听。
  6. 等待watcher通知,继续进入步骤2

分布式队列

分布式队列可以简单分为两⼤类:⼀种是常规的FIFO先⼊先出队列模型,还有⼀种是等待队列元素聚集后统⼀安排处理执⾏的Barrier模型

FIFO(First Input First Output)

使用Zookeeper实现FIFO队列,也是依赖于Zookeeper的顺序节点特性。思路如下:

  1. 所有应用都在/queue这个节点下创建临时顺序节点
  2. 调用getChildren接口来获取他下面的所有子节点,相当于获取队列中的所有元素
  3. 判断自己的序号是不是最小的,如果是就执行业务,执行完后删除节点。如果不是,就监听比自己序号小的最后一个节点
  4. 收到watcher后,重复步骤2

Barrier:分布式屏障

分布式屏障特指系统的一个协调条件,规定队列中的元素聚齐后才能进行统一安排。比如所有的员工都把工作做完了,才一起下班去吃饭。

设计思路如下:

创建一个/queue_barrier节点,并且给这个节点赋值数字n,这个n代表只有当/queue_barrier节点下有n个子节点时,才能进行下一步操作。

  1. 调用getData获取/queue_barrier节点的内容,假设内容是10
  2. 调用getChildren接口看看子节点个数到达10了没,没有就注册子节点变更监听.达到了就开始执行业务
  3. 接收到通知后,需要重复步骤2

ZAB协议

zookeeper就是使用了ZAB协议来实现的分布式数据一致性。ZAB协议是专门为zookeeper设计的一种支持崩溃恢复的原子广播协议。

ZAB核心

ZAB协议定义了那些会改变Zookeeper服务器数据状态的事务请求的处理方式。

所有这些会修改状态的事务请求必须由一个全局唯一的服务器来协调处理,也就是Leader服务器。Leader服务器负责将客户端请求转化为事务Proposal(提议),并将Proposal分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,Leader接下来就会再次向所有的Follower服务器发Commit消息,把刚才的Proposal进行提交。

ZAB两种模式:崩溃恢复和消息广播

消息广播

在消息⼴播过程中,Leader服务器会为每⼀个Follower服务器都各⾃分配⼀个单独的队列,然后将需要⼴播的事务Proposal依次放⼊这些队列中去,并且根据 FIFO策略进⾏消息发送。每⼀个Follower服务器在接收到这个事务Proposal之后,都会⾸先将其以事务⽇志的形式写⼊到本地磁盘中去,并且在成功写⼊后反馈给Leader服务器⼀个Ack响应。当Leader服务器接收到超过半数Follower的Ack响应后,就会⼴播⼀个Commit消息给所有的Follower服务器以通知其进⾏事务提交,同时Leader⾃身也会完成对事务的提交,⽽每⼀个Follower服务器在接收到Commit消息后,也会完成对事务的提交。

我们考虑如下两个问题:

  1. Leader服务器发出proposal还没有提交时宕机了
  2. Leader服务器自己提交后,发出部分让Follower COMMIT的请求后就宕机了

解决这个问题需要ZAB的崩溃恢复模式

崩溃恢复

Leader宕机后

  • 要保证已经在Leader服务器提交的事务被所有服务器提交
  • 丢弃那些只是在Leader服务器提出proposal未提交的事务

针对上面两个要求,如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最⾼编号(即ZXID最⼤)的事务Proposal,那么就可以保证这个新选举出来的Leader⼀定具有所有已经提交的提案。他成了Leader后他有的事务肯定是已提交的所有事务了。

新Leader产生后,在正式开始⼯作(即接收客户端的事务请求,然后提出新的提案)之前,
Leader服务器会⾸先确认事务⽇志中的所有Proposal是否都已经被集群中过半的机器提交了,即是否完成数据同步。

数据同步

它会为每一个Follower服务器准备一个队列,将那些没有被各Follower服务器同步的事务以Proposal消息的方式发送给Follower服务器,并在每一个Proposal消息后紧接着再发送一个Commit消息。等到Follower服务器将所有尚未同步的事务都成功应用到本地数据库后,Leader服务器就会将该Follower服务器加入真正可用的Follower列表中。

运⾏时状态分析

在ZAB协议的设计中,每个进程都有可能处于如下三种状态之⼀

  • LOOKING:Leader选举阶段。
  • FOLLOWING:Follower服务器和Leader服务器保持同步状态。
  • LEADING:Leader服务器作为主进程领导状态。

所有进程初始状态都是LOOKING状态,此时不存在Leader。接下来,进程会试图选举出⼀个新的Leader,如果进程发现已经选举出新的Leader了,那么它就会切换到FOLLOWING状态,并开始和Leader保持同步。处于FOLLOWING状态的进程称为Follower,LEADING状态的进程称为Leader,当Leader崩溃或放弃领导地位时,其余的Follower进程就会转换到LOOKING状态开始新⼀轮的Leader选举。

⼀个Follower只能和⼀个Leader保持同步,Leader进程和所有的Follower进程之间都通过⼼跳检测机制来感知彼此的情况。若Leader能够在超时时间内正常收到⼼跳检测,那么Follower就会⼀直与该Leader保持连接,⽽如果在指定时间内Leader⽆法从过半的Follower进程那⾥接收到⼼跳检测,或者TCP连接断开,那么Leader会放弃当前周期的领导,并转换到LOOKING状态,其他的Follower也会选择放弃这个Leader,同时转换到LOOKING状态,之后会进⾏新⼀轮的Leader选举。

ZAB与Paxos的联系和区别

联系:

  • 都存在一个类似Leader的角色
  • Leader进程都会等待超半数的Follower做出正确反馈后,才会将一个提议提交
  • 在ZAB协议中,每个Proposal中都包含了⼀个epoch值,⽤来代表当前的Leader周期,在Paxos算法中,同样存在这样的⼀个标识,名字为Ballot。

区别:

  • Paxos算法中,新选举产⽣的主进程会进⾏两个阶段的⼯作,第⼀阶段称为读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第⼆阶段称为写阶段,当前主进程开始提出⾃⼰的提议
  • ZAB协议在Paxos基础上添加了同步阶段,此时,新的Leader会确保存在过半的Follower已经提交了之前的Leader周期中的所有事务Proposal。这⼀同步阶段的引⼊,能够有效地保证Leader在新的周期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal的提交。

总的来说,ZAB协议和Paxos算法的本质区别在于,两者的设计⽬标不太⼀样。ZAB协议主要⽤于构建⼀个⾼可⽤的分布式数据主备系统,⽽Paxos算法则⽤于构建⼀个分布式的⼀致性状态机系统。

最终,我们讨论一个问题

Zookeeper是属于强一致性还是弱一致性?

答案:写入是强一致,读取是顺序一致性。Zookeeper官方说是顺序一致性。

因为客户读连接ZooKeeper集群后,所有的写操作都必须发送给集群唯一的leader,这个leader在内部同步块中赋予每个写操作一个顺序序列号(内部称为zxid,是单调增加的),上一个写操作不commit,下一个写操作就不执行,这一点实际上已经实现了写入的强一致性(线性化)了

通过严格按照ZXID的顺序生效提案保证其顺序一致性的。

Zookeeper它还为我们提供sync()方法,强制读取在时候从Leader同步数据。