Disclaimer
This archive is the public resource collected and collated from the Internet for studying purpose.
We respect copyright owners. Please contact us in case of any infringement.
zookeeper 是什么?
zookeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 google chubby 的开源实现,是 hadoop 和 hbase 的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
zookeeper 都有哪些功能?
ZooKeeper 概览中,我们介绍到使用其通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
下面选几个典型的应用场景来专门说说:
-
分布式锁 :通过创建唯一节点获得分布式锁,当获得锁的一方执行完相关代码或者是挂掉之后就释放锁。
-
命名服务 :可以通过 ZooKeeper 的顺序节点生成全局唯一 ID
-
数据发布/订阅 :通过 Watcher 机制 可以很方便地实现数据发布/订阅。当你将数据发布到 ZooKeeper 被监听的节点上,其他机器可通过监听 ZooKeeper 上节点的变化来实现配置的动态更新。
-
注册中心:比如dubbo的服务注册中心
-
Master 选举:比如elastic-job里用它进行选主
实际上,这些功能的实现基本都得益于 ZooKeeper 可以保存数据的功能,但是 ZooKeeper 不适合保存大量数据,这一点需要注意。
集群支持动态添加机器吗?
其实就是水平扩容了,Zookeeper在这方面不太好。两种方式:
-
全部重启:关闭所有Zookeeper服务,修改配置之后启动。不影响之前客户端的会话。
-
逐个重启:在过半存活即可用的原则下,一台机器重启不影响整个集群对外提供服务。这是比较常用的方式。
3.5版本开始支持动态扩容。
Zookeeper的java客户端都有哪些?
java客户端:
-
zk自带的zkclient
-
Apache开源的Curator。
集群最少要几台机器,集群规则是怎样的?
集群规则为2N+1台,N>0,即3台。
分布式集群中Master节点的作用?
在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行leader选举。
zookeeper是如何保证事务的顺序一致性的?
zookeeper采用了全局递增的事务Id来标识,所有的proposal(提议)都在被提出的时候加上了zxid。
zxid实际上是一个64位的数字,高32位是epoch(时期; 纪元; 世; 新时代)用来标识leader周期,如果有新的leader产生出来,epoch会自增,低32位用来递增计数。
当新产生proposal的时候,会依据数据库的两阶段过程,首先会向其他的server发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。
zookeeper 有几种部署模式?
zookeeper 有三种部署模式:
单机部署:一台集群上运行;
集群部署:多台集群运行;
伪集群部署:一台集群启动多个 zookeeper 实例运行。
zookeeper 怎么保证主从节点的状态同步?
zookeeper 的核心是原子广播,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 zab 协议。
zab 协议有两种模式,分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 server 具有相同的系统状态。
集群中有 3 台服务器,其中一个节点宕机,这个时候 zookeeper 还可以使用吗?
可以继续使用,单数服务器只要没超过一半的服务器宕机就可以继续使用。
说一下 zookeeper 的通知机制?
客户端端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,这些客户端会收到 zookeeper 的通知,然后客户端可以根据 znode 变化来做出业务上的改变。
eureka和zookeeper都可以提供服务注册与发现的功能,请说说两个的区别?
zookeeper 是CP原则,强一致性和分区容错性。
eureka 是AP 原则 可用性和分区容错性。
zookeeper当主节点故障时,zk会在剩余节点重新选择主节点,耗时过长,虽然最终能够恢复,但是选取主节点期间会导致服务不可用,这是不能容忍的。
eureka各个节点是平等的,一个节点挂掉,其他节点仍会正常保证服务。
ZooKeeper 特点有哪些?
顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
单一系统映像 : 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。
Zookeeper Watcher(事件监听器)?
Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。

Watch机制官方声明:一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。
Zookeeper watch机制的特点:
1、一次性触发数据发生改变时,一个watcher event会被发送到client,但是client只会收到一次这样的信息。
2、watcher event异步发送watcher的通知事件从server发送到client是异步的,这就存在一个问题,不同的客户端和服务器之间通过socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于Zookeeper本身提供了ordering guarantee,即客户端监听事件后,才会感知它所监视znode发生了变化。所以我们使用Zookeeper不能期望能够监控到节点每次的变化。Zookeeper只能保证最终的一致性,而无法保证强一致性。
3、数据监视Zookeeper有数据监视和子数据监视getdata() and exists()设置数据监视,getchildren()设置了子节点监视。
4、注册watcher getData、exists、getChildren
5、触发watcher create、delete、setData
6、setData()会触发znode上设置的data watch(如果set成功的话)。一个成功的create() 操作会触发被创建的znode上的数据watch,以及其父节点上的child watch。而一个成功的delete()操作将会同时触发一个znode的data watch和child watch(因为这样就没有子节点了),同时也会触发其父节点的child watch。
7、当一个客户端连接到一个新的服务器上时,watch将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到watch的。而当client重新连接时,如果需要的话,所有先前注册过的watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch可能会丢失:对于一个未创建的znode的exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个watch事件可能会被丢失。
8、Watch是轻量级的,其实就是本地JVM的Callback,服务器端只是存了是否有设置了Watcher的布尔类型
Zookeeper对节点的watch监听通知是永久的吗?为什么不是永久的?
不是。官方声明:一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了Watch的客户端,以便通知它们。
为什么不是永久的,举个例子,如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。
一般是客户端执行getData(“/节点A”,true),如果节点A发生了变更或删除,客户端会得到它的watch事件,但是在之后节点A又发生了变更,而客户端又没有设置watch事件,就不再给客户端发送。
在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。
Zookeeper 会话(Session)是什么?
Session 可以看作是 ZooKeeper 服务器与客户端的之间的一个 TCP 长连接,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watcher 事件通知。
Session 有一个属性叫做:sessionTimeout ,sessionTimeout 代表会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。
另外,在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID。由于 sessionID是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。
ZooKeeper 集群 ?
为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。
通常 3 台服务器就可以构成一个 ZooKeeper 集群了。ZooKeeper 官方提供的架构图就是一个 ZooKeeper 集群整体对外提供服务。

上图中每一个 Server 代表一个安装 ZooKeeper 服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 ZAB 协议(ZooKeeper Atomic Broadcast)来保持数据的一致性。
最典型集群模式:Master/Slave 模式(主备模式)。在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
ZooKeeper 集群角色 ?
但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了 Leader、Follower 和 Observer 三种角色。如下图所示

ZooKeeper 集群中的所有机器通过一个 Leader 选举过程 来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。
除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。
| 角色 | 说明 |
|---|---|
| Leader | 为客户端提供读和写的服务,负责投票的发起和决议,更新系统状态。 |
| Follower | 为客户端提供读服务,如果是写服务则转发给 Leader。在选举过程中参与投票。 |
| Observer | 为客户端提供读服务器,如果是写服务则转发给 Leader。不参与选举过程中的投票,也不参与“过半写成功”策略。在不影响写性能的情况下提升集群的读性能。此角色于 ZooKeeper3.3 系列新增的角色。 |
当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,就会进入 Leader 选举过程,这个过程会选举产生新的 Leader 服务器。
这个过程大致是这样的:
-
Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。 -
Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。 -
Synchronization(同步阶段):同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后 准 leader 才会成为真正的 leader。 -
Broadcast(广播阶段):到了这个阶段,ZooKeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。
ZooKeeper 集群中的服务器状态?
LOOKING:寻找 Leader。LEADING:Leader 状态,对应的节点为 Leader。FOLLOWING:Follower 状态,对应的节点为 Follower。OBSERVING:Observer 状态,对应节点为 Observer,该节点不参与 Leader 选举。
Zookeper znode 4种类型 是什么?
-
持久(PERSISTENT)节点:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。 -
临时(EPHEMERAL)节点:临时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。并且,临时节点只能做叶子节点 ,不能创建子节点。 -
持久顺序(PERSISTENT_SEQUENTIAL)节点:除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 /node1/app0000000001 、/node1/app0000000002 。 -
临时顺序(EPHEMERAL_SEQUENTIAL)节点:除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
Zookeper znode 数据结构?
每个 znode 由 2 部分组成:
- stat :状态信息
- data :节点存放的数据的具体内容
如下所示,我通过 get 命令来获取 根目录下的 dubbo 节点的内容。(get 命令在下面会介绍到)。
[zk: 127.0.0.1:2181(CONNECTED) 6] get /dubbo# 该数据节点关联的数据内容为空null# 下面是该数据节点的一些状态信息,其实就是 Stat 对象的格式化输出cZxid = 0x2ctime = Tue Nov 27 11:05:34 CST 2018mZxid = 0x2mtime = Tue Nov 27 11:05:34 CST 2018pZxid = 0x3cversion = 1dataVersion = 0aclVersion = 0ephemeralOwner = 0x0dataLength = 0numChildren = 1Copy to clipboardErrorCopied
Stat 类中包含了一个数据节点的所有状态信息的字段,包括事务 ID-cZxid、节点创建时间-ctime 和子节点个数-numChildren 等等。
下面我们来看一下每个 znode 状态信息究竟代表的是什么吧!(下面的内容来源于《从 Paxos 到 ZooKeeper 分布式一致性原理与实践》 :
| znode 状态信息 | 解释 |
|---|---|
| cZxid | create ZXID,即该数据节点被创建时的事务 id |
| ctime | create time,即该节点的创建时间 |
| mZxid | modified ZXID,即该节点最终一次更新时的事务 id |
| mtime | modified time,即该节点最后一次的更新时间 |
| pZxid | 该节点的子节点列表最后一次修改时的事务 id,只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新 |
| cversion | 子节点版本号,当前节点的子节点每次变化时值增加 1 |
| dataVersion | 数据节点内容版本号,节点创建时为 0,每更新一次节点内容(不管内容有无变化)该版本号的值增加 1 |
| aclVersion | 节点的 ACL 版本号,表示该节点 ACL 信息变更次数 |
| ephemeralOwner | 创建该临时节点的会话的 sessionId;如果当前节点为持久节点,则 ephemeralOwner=0 |
| dataLength | 数据节点内容长度 |
| numChildren | 当前节点的子节点个数 |
ZooKeeper 集群为啥最好奇数台?
ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后,如果剩下的 ZooKeeper 服务器个数大于宕掉的个数的话,整个 ZooKeeper 才依然可用。
假如我们的集群中有 n 台 ZooKeeper 服务器,那么也就是剩下的服务数必须大于 n/2。
先说一下结论,2n 和 2n-1 的容忍度是一样的,都是 n-1,大家可以先自己仔细想一想,这应该是一个很简单的数学问题了。
比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。
假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时候也同样只允许宕掉 2 台。
ZAB和Paxos算法的联系与区别?
相同点:
-
两者都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行
-
Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交
-
ZAB协议中,每个Proposal中都包含一个 epoch 值来代表当前的Leader周期,Paxos中名字为Ballot
不同点:
- ZAB用来构建高可用的分布式数据主备系统(Zookeeper),Paxos是用来构建分布式一致性状态机系统。
Zookeeper 如何选举master 主节点?
选择机制中的概念
serverId(服务器ID 既 myid)
-
比如有三台服务器,编号分别是1,2,3。
-
编号越大在选择算法中的权重越大。
zxid(最新的事物ID 既 LastLoggedZxid)
-
服务器中存放的最大数据ID。
-
ID值越大说明数据越新,在选举算法中数据越新权重越大。
epoch (逻辑时钟 既 PeerEpoch)
-
每个服务器都会给自己投票,或者叫投票次数,同一轮投票过程中的逻辑时钟值是相同的。
-
每投完一次票这个数据就会增加,然后与接收到的其它服务器返回的投票信息中的数值相比。
-
如果收到低于当前轮次的投票结果,该投票无效,需更新到当前轮次和当前的投票结果。
选举状态
-
LOOKING,竞选状态。
-
FOLLOWING,随从状态,同步leader状态,参与投票。
-
OBSERVING,观察状态,同步leader状态,不参与投票。
-
LEADING,领导者状态。
选举算法
通过 zoo.cfg 配置文件中的 electionAlg 属性指定 (1-3),要理解算法,需要一些paxos算法的理论基础。
-
对应:LeaderElection 算法。
-
对应:AuthFastLeaderElection 算法。
-
对应:FastLeaderElection 默认的算法。
投票内容
-
选举人ID
-
选举人数据ID
-
选举人选举轮数
-
选举人选举状态
-
推举人ID
-
推举人选举轮数
Leader选举
Leader选举有如下两种
-
第一种:服务器初始化启动的Leader选举。
-
第二种:服务器运行时期的Leader选举(服务器运行期间无法和Leader保持连接)。
Leader选举的前提条件
-
只有服务器状态在LOOKING(竞选状态)状态才会去执行选举算法。
-
Zookeeper 的集群规模至少是2台机器,才可以选举Leader,这里以3台机器集群为例。
-
当一台服务器启动是不能选举的,等第二台服务器启动后,两台机器之间可以互相通信,才可以进行Leader选举
-
服务器运行期间无法和Leader保持连接的时候。
服务器启动时期的 Leader 选举
在集群初始化阶段,当有一台服务器Server1启动时,其单独无法进行和完成Leader选举,当第二台服务器Server2启动后,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程如下:
(1) 每个Server发出一个投票投给自己。由于是初始情况,Server1和Server2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。
(2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。
(3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,PK规则如下
-
优先检查ZXID。ZXID比较大的服务器优先作为Leader。
-
如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。
对于Server1而言,它的投票是(1, 0),接收Server2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时Server2的myid最大,于是更新自己的投票为(2, 0),然后重新投票,对于Server2而言,其无须更新自己的投票,只是再次向集群中所有机器发出上一次投票信息即可。
(4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,对于Server1、Server2而言,都统计出集群中已经有两台机器接受了(2, 0)的投票信息,此时便认为已经选出了Leader。
(5) 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。
服务器运行时期的 Leader 选举
在Zookeeper运行期间,即便当有非Leader服务器宕机或新加入,此时也不会影响Leader,但是一旦Leader服务器挂了,那么整个集群将暂停对外服务,进入新一轮Leader选举,其过程和启动时期的Leader选举过程基本一致。假设正在运行的有Server1、Server2、Server3三台服务器,当前Leader是Server2,若某一时刻Leader挂了,此时便开始Leader选举。选举过程如下:
(1)变更状态。Leader挂后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举流程。
(2)每个Server会发出一个投票。在这个过程中,需要生成投票信息(myid,ZXID)每个服务器上的ZXID可能不同,我们假定Server1的ZXID为123,而Server3的ZXID为122;在第一轮投票中,Server1和Server3都会投自己,产生投票(1, 123),(3, 122),然后各自将投票发送给集群中所有机器。
(3)接收来自各个服务器的投票。与启动时过程相同。
(4)处理投票。与启动时过程相同,此时,Server1将会成为Leader。
(5)统计投票。与启动时过程相同。
(6)改变服务器的状态。与启动时过程相同。
Leader选举算法分析
在3.4.0后的Zookeeper的版本只保留了TCP版本的FastLeaderElection选举算法。当一台机器进入Leader选举时,当前集群可能会处于以下两种状态
当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举。
-
服务器初始化启动。
-
服务器运行期间无法和Leader保持连接。
而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态。
-
集群中本来就巳经存在一个Leader。
-
集群中确实不存在Leader。
我们先来看第一种巳经存在Leader的情况。此种情况一般都是某台机器启动得较晚,在其启动之前,集群已经在正常工作,对这种情况,该机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器而言,仅仅需要和Leader机器建立起连接,并进行状态同步即可。
下面我们重点来看在集群中Leader不存在的情况下,如何进行Leader选举。
开始第一次投票
通常有两种情况会导致集群中不存在Leader
-
一种情况是在整个服务器刚刚初始化启动时,此时尚未产生一台Leader服务器。
-
另一种情况就是在运行期间当前Leader所在的服务器挂了。
无论是哪种情况,此时集群中的所有机器都处于一种试图选举出一个Leader的状态,我们把这种状态称为“LOOKING”,意思是说正在寻找Leader。当一台服务器处于LOOKING状态的时候,那么它就会向集群中所有其他机器发送消息,我们称这个消息为“投票”。
在这个投票消息中包含两个最基本的信息:
所推举的服务器的SID和ZXID,分别表示了被推举服务器的唯一标识和事务ID。
下文中我们将以“(SID, ZXID)”这样的形式 来标识一次投票信息。举例来说,如果当前服务器要推举SID为1、ZXID为8的服务器成为Leader,那么它的这次投票信息可以表示为(1,8)。
我们假设ZooKeeper由5台机器组成,SID分別为1、2、3、4和5, ZXID分别为9、9、9、8和8,并且此时SID为2的机器是Leader服务器。某一时刻,1和2所在的机器出现故障,因此集群开始进行Leader选举。
在第一次投票的时候,由于还无法检测到集群中其他机器的状态信息,因此每台机器都是将自己作为被推举的对象来进行投票,于是SID为3、4和5的机器,投票情况分别为:(3, 9)、(4, 8)和(5, 8)。
变更投票
集群中的每台机器发出自己的投票后,也会接收到来自集群中其他机器的投票。每台机器都会根据一定的规则,来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票。这个规则也成为了整个Leader选举算法的核心所在。为了便于描述,我们首先定义一些术语。
-
vote_sid:接收到的投票中所推举Leader服务器的SID。
-
vote_zxid:接收到的投票中所推举Leader服务器的ZXID,既事物ID。
-
self_sid::当前服务器自己的SID。
-
self_zxid:当前服务器自己的ZXID。
每次对于收到的投票的处理,都是一个对(votesid,votezxid)和(selfsid,selfzxid) 对比的过程,假设Epoch相同的情况下。
规则如下:
1、如果votezxid大于自己的selfzxid,就认可当前收到的投票,并再次将该投票发送出去。
2、如果votezxid小于自己的selfzxid,那么就坚持自己的投票,不做任何变更。
3、如果votezxid等于自己的selfzxid,那么就对比两者的SID。如果votesid大于selfsid,那么就认可当前接收到的投票,并再次将该投票发送出去。
4、如果votezxid等于自己的selfzxid,并且votesid小于selfsid,那么同样坚持自己的投票,不做变更。
根据上面这个规则,我们结合图来分折上面提到的5台机器组成的ZooKeeper集群的投票变更过程。

每台机器都把投票发出后,同时也会接收到来自另外两台机器的投票。
对干Server3来说,它接收到了(4, 8)和(5, 8)两个投票,对比后,由子自己的ZXID要大于接收到的两个投票,因此不需要做任何变更。
对于Server4来说,它接收到了(3, 9)和(5, 8)两个投票,对比后,由于(3, 9)这个投票的ZXID大于自己,因此需要变更投票为(3, 9),然后继续将这个投票发送给另外两台机器。
同样,对TServer5来说,它接收到了(3, 9)和(4, 8)两个投票,对比后,由于(3, 9)这个投票的ZXID大于自己,因此需要变更投票为(3, 9),然后继续将这个投票发送给另外两台机器。
确定Leader
经过这第二次投票后,集群中的每台机器都会再次收到其他机器的投票,然后开始统计投票。如果一台机器收到了超过半数的相同的投票,那么这个投票对应的SID机器即为 Leader。
如上图所示的Leader选举例子中,因为ZooKeeper集群的总机器数为5台,那么 quorum = ( 5/2 + 1 ) = 3
也就是说,只要收到3个或3个以上(含当前服务器自身在内)一致的投票即可。在这里,Server3、Server4 和Server5 都投票(3, 9),因此确定了 Server3为 Leader。
小结
简单地说,通常哪台服务器上的数据越新(ZXID会越大),那么越有可能成为Leader,原因很简单,数据越新,那么它的ZXID也就越大,也就越能够保证数据的恢复。当然,如果集群中有几个服务器有相同的ZXID,那么SID较大的那台服务器成为Leader。
Leader选举实现细节
1、服务器状态
服务器具有四种状态,分别是LOOKING、FOLLOWING、LEADING、OBSERVING。
-
LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有Leader,因此需要进入Leader选举状态。
-
FOLLOWING:跟随者状态。表明当前服务器角色是Follower。
-
LEADING:领导者状态。表明当前服务器角色是Leader。
-
OBSERVING:观察者状态。表明当前服务器角色是Observer。
2、投票数据结构
每个投票中包含了两个最基本的信息,所推举服务器的SID和ZXID,投票(Vote)在Zookeeper中包含字段如下。
-
id:被推举的Leader的SID。
-
zxid:被推举的Leader事务ID。
-
electionEpoch:逻辑时钟,用来判断多个投票是否在同一轮选举周期中,该值在服务端是一个自增序列,每次进入新一轮的投票后,都会对该值进行加1操作。
-
peerEpoch:被推举的Leader的epoch。
-
state:当前服务器的状态。
3、QuorumCnxManager:网络I/O
每台服务器在启动的过程中,会启动一个QuorumPeerManager,负责各台服务器之间的底层Leader选举过程中的网络通信。
(1)消息队列。QuorumCnxManager内部维护了一系列的队列,用来保存接收到的、待发送的消息以及消息的发送器,除接收队列以外,其他队列都按照SID分组形成队列集合,如一个集群中除了自身还有3台机器,那么就会为这3台机器分别创建一个发送队列,互不干扰。
-
recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息。
-
queueSendMap:消息发送队列,用于保存那些待发送的消息,按照SID进行分组。
-
senderWorkerMap:发送器集合,每个SenderWorker消息发送器,都对应一台远程Zookeeper服务器,负责消息的发送,也按照SID进行分组。
-
lastMessageSent:最近发送过的消息,为每个SID保留最近发送过的一个消息。
(2)建立连接。为了能够相互投票,Zookeeper集群中的所有机器都需要两两建立起网络连接。QuorumCnxManager在启动时会创建一个ServerSocket来监听Leader选举的通信端口(默认为3888)。开启监听后,Zookeeper能够不断地接收到来自其他服务器的创建连接请求,在接收到其他服务器的TCP连接请求时,会进行处理。为了避免两台机器之间重复地创建TCP连接,Zookeeper只允许SID大的服务器主动和其他机器建立连接,否则断开连接。在接收到创建连接请求后,服务器通过对比自己和远程服务器的SID值来判断是否接收连接请求,如果当前服务器发现自己的SID更大,那么会断开当前连接,然后自己主动和远程服务器建立连接。一旦连接建立,就会根据远程服务器的SID来创建相应的消息发送器SendWorker和消息接收器RecvWorker,并启动。
(3)消息接收与消息发送。
消息接收:由消息接收器RecvWorker负责,由于Zookeeper为每个远程服务器都分配一个单独的RecvWorker,因此,每个RecvWorker只需要不断地从这个TCP连接中读取消息,并将其保存到recvQueue队列中。
消息发送:由于Zookeeper为每个远程服务器都分配一个单独的SendWorker,因此,每个SendWorker只需要不断地从对应的消息发送队列中获取出一个消息发送即可,同时将这个消息放入lastMessageSent中。在SendWorker中,一旦Zookeeper发现针对当前服务器的消息发送队列为空,那么此时需要从lastMessageSent中取出一个最近发送过的消息来进行再次发送,这是为了解决接收方在消息接收前或者接收到消息后服务器挂了,导致消息尚未被正确处理。同时,Zookeeper能够保证接收方在处理消息时,会对重复消息进行正确的处理。
4、FastLeaderElection:选举算法核心
-
外部投票:特指其他服务器发来的投票。
-
内部投票:服务器自身当前的投票。
-
选举轮次:Zookeeper服务器Leader选举的轮次,即logicalclock。
-
PK:对内部投票和外部投票进行对比来确定是否需要变更内部投票。
(1) 选票管理
-
sendqueue:选票发送队列,用于保存待发送的选票。
-
recvqueue:选票接收队列,用于保存接收到的外部投票。
-
WorkerReceiver:选票接收器。其会不断地从QuorumCnxManager中获取其他服务器发来的选举消息,并将其转换成一个选票,然后保存到recvqueue中,在选票接收过程中,如果发现该外部选票的选举轮次小于当前服务器的,那么忽略该外部投票,同时立即发送自己的内部投票。
-
WorkerSender:选票发送器,不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中。
(2) 算法核心

上图展示了FastLeaderElection模块是如何与底层网络I/O进行交互的。Leader选举的基本流程如下
1、自增选举轮次。Zookeeper规定所有有效的投票都必须在同一轮次中,在开始新一轮投票时,会首先对logicalclock进行自增操作。
2、初始化选票。在开始进行新一轮投票之前,每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为Leader。
3、发送初始化选票。完成选票的初始化后,服务器就会发起第一次投票。Zookeeper会将刚刚初始化好的选票放入sendqueue中,由发送器WorkerSender负责发送出去。
4、接收外部投票。每台服务器会不断地从recvqueue队列中获取外部选票。如果服务器发现无法获取到任何外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,如果已经建立了连接,则再次发送自己当前的内部投票。
5、判断选举轮次。在发送完初始化选票之后,接着开始处理外部投票。在处理外部投票时,会根据选举轮次来进行不同的处理。
-
外部投票的选举轮次大于内部投票。若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票。最终再将内部投票发送出去。
-
外部投票的选举轮次小于内部投票。若服务器接收的外选票的选举轮次落后于自身的选举轮次,那么Zookeeper就会直接忽略该外部投票,不做任何处理,并返回步骤4。
-
外部投票的选举轮次等于内部投票。此时可以开始进行选票PK。
6、选票PK。在进行选票PK时,符合任意一个条件就需要变更投票。
-
若外部投票中推举的Leader服务器的选举轮次大于内部投票,那么需要变更投票。
-
若选举轮次一致,那么就对比两者的ZXID,若外部投票的ZXID大,那么需要变更投票。
-
若两者的ZXID一致,那么就对比两者的SID,若外部投票的SID大,那么就需要变更投票。
7、变更投票。经过PK后,若确定了外部投票优于内部投票,那么就变更投票,即使用外部投票的选票信息来覆盖内部投票,变更完成后,再次将这个变更后的内部投票发送出去。
8、选票归档。无论是否变更了投票,都会将刚刚收到的那份外部投票放入选票集合recvset中进行归档。recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票(按照服务队的SID区别,如{(1, vote1), (2, vote2)…})。
9、统计投票。完成选票归档后,就可以开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票,如果确定已经有过半服务器认可了该投票,则终止投票。否则返回步骤4。
10、更新服务器状态。若已经确定可以终止投票,那么就开始更新服务器状态,服务器首选判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,若是自己,则将自己的服务器状态更新为LEADING,若不是,则根据具体情况来确定自己是FOLLOWING或是OBSERVING。
以上10个步骤就是FastLeaderElection的核心,其中步骤4-9会经过几轮循环,直到有Leader选举产生。
Zab协议
什么是Zab协议?
Zab协议 的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播)。 Zookeeper 是通过 Zab 协议来保证分布式事务的最终一致性。
Zab协议是为分布式协调服务Zookeeper专门设计的一种 支持崩溃恢复 的 原子广播协议 ,是Zookeeper保证数据一致性的核心算法。Zab借鉴了Paxos算法,但又不像Paxos那样,是一种通用的分布式一致性算法。它是特别为Zookeeper设计的支持崩溃恢复的原子广播协议。
在Zookeeper中主要依赖Zab协议来实现数据一致性,基于该协议,zk实现了一种主备模型(即Leader和Follower模型)的系统架构来保证集群中各个副本之间数据的一致性。
这里的主备系统架构模型,就是指只有一台客户端(Leader)负责处理外部的写事务请求,然后Leader客户端将数据同步到其他Follower节点。
Zookeeper 客户端会随机的链接到 zookeeper 集群中的一个节点,如果是读请求,就直接从当前节点中读取数据;如果是写请求,那么节点就会向 Leader 提交事务,Leader 接收到事务提交,会广播该事务,只要超过半数节点写入成功,该事务就会被提交。
Zab 协议的特性:
1)Zab 协议需要确保那些已经在 Leader 服务器上提交(Commit)的事务最终被所有的服务器提交。
2)Zab 协议需要确保丢弃那些只在 Leader 上被提出而没有被提交的事务。
Zab 协议实现的作用
1)使用一个单一的主进程(Leader)来接收并处理客户端的事务请求(也就是写请求),并采用了Zab的原子广播协议,将服务器数据的状态变更以 事务proposal (事务提议)的形式广播到所有的副本(Follower)进程上去。
2)保证一个全局的变更序列被顺序引用。
Zookeeper是一个树形结构,很多操作都要先检查才能确定是否可以执行,比如P1的事务t1可能是创建节点”/a”,t2可能是创建节点”/a/bb”,只有先创建了父节点”/a”,才能创建子节点”/a/b”
为了保证这一点,Zab要保证同一个Leader发起的事务要按顺序被apply,同时还要保证只有先前Leader的事务被apply之后,新选举出来的Leader才能再次发起事务。
3)当主进程出现异常的时候,整个zk集群依旧能正常工作。
Zab协议原理
Zab协议要求每个 Leader 都要经历三个阶段:发现,同步,广播。
-
发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。
-
同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
-
广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。
Zab协议核心
Zab协议的核心:定义了事务请求的处理方式
1)所有的事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被叫做 Leader服务器。其他剩余的服务器则是 Follower服务器。
2)Leader服务器 负责将一个客户端事务请求,转换成一个 事务Proposal,并将该 Proposal 分发给集群中所有的 Follower 服务器,也就是向所有 Follower 节点发送数据广播请求(或数据复制)
3)分发之后Leader服务器需要等待所有Follower服务器的反馈(Ack请求),在Zab协议中,只要超过半数的Follower服务器进行了正确的反馈后(也就是收到半数以上的Follower的Ack请求),那么 Leader 就会再次向所有的 Follower服务器发送 Commit 消息,要求其将上一个 事务proposal 进行提交。

Zab协议内容
Zab 协议包括两种基本的模式:崩溃恢复 和 消息广播
协议过程
当整个集群启动过程中,或者当 Leader 服务器出现网络中弄断、崩溃退出或重启等异常时,Zab协议就会 进入崩溃恢复模式,选举产生新的Leader。
当选举产生了新的 Leader,同时集群中有过半的机器与该 Leader 服务器完成了状态同步(即数据同步)之后,Zab协议就会退出崩溃恢复模式,进入消息广播模式。
这时,如果有一台遵守Zab协议的服务器加入集群,因为此时集群中已经存在一个Leader服务器在广播消息,那么该新加入的服务器自动进入恢复模式:找到Leader服务器,并且完成数据同步。同步完成后,作为新的Follower一起参与到消息广播流程中。
协议状态切换
当Leader出现崩溃退出或者机器重启,亦或是集群中不存在超过半数的服务器与Leader保存正常通信,Zab就会再一次进入崩溃恢复,发起新一轮Leader选举并实现数据同步。同步完成后又会进入消息广播模式,接收事务请求。
保证消息有序
在整个消息广播中,Leader会将每一个事务请求转换成对应的 proposal 来进行广播,并且在广播 事务Proposal 之前,Leader服务器会首先为这个事务Proposal分配一个全局单递增的唯一ID,称之为事务ID(即zxid),由于Zab协议需要保证每一个消息的严格的顺序关系,因此必须将每一个proposal按照其zxid的先后顺序进行排序和处理。
消息广播
1)在zookeeper集群中,数据副本的传递策略就是采用消息广播模式。zookeeper中农数据副本的同步方式与二段提交相似,但是却又不同。二段提交要求协调者必须等到所有的参与者全部反馈ACK确认消息后,再发送commit消息。要求所有的参与者要么全部成功,要么全部失败。二段提交会产生严重的阻塞问题。
2)Zab协议中 Leader 等待 Follower 的ACK反馈消息是指“只要半数以上的Follower成功反馈即可,不需要收到全部Follower反馈”

消息广播具体步骤
1)客户端发起一个写操作请求。
2)Leader 服务器将客户端的请求转化为事务 Proposal 提案,同时为每个 Proposal 分配一个全局的ID,即zxid。
3)Leader 服务器为每个 Follower 服务器分配一个单独的队列,然后将需要广播的 Proposal 依次放到队列中取,并且根据 FIFO 策略进行消息发送。
4)Follower 接收到 Proposal 后,会首先将其以事务日志的方式写入本地磁盘中,写入成功后向 Leader 反馈一个 Ack 响应消息。
5)Leader 接收到超过半数以上 Follower 的 Ack 响应消息后,即认为消息发送成功,可以发送 commit 消息。
6)Leader 向所有 Follower 广播 commit 消息,同时自身也会完成事务提交。Follower 接收到 commit 消息后,会将上一条事务提交。
zookeeper 采用 Zab 协议的核心,就是只要有一台服务器提交了 Proposal,就要确保所有的服务器最终都能正确提交 Proposal。这也是 CAP/BASE 实现最终一致性的一个体现。
Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往队列中发消息即可。如果使用同步的方式会引起阻塞,性能要下降很多。
崩溃恢复
一旦 Leader 服务器出现崩溃或者由于网络原因导致 Leader 服务器失去了与过半 Follower 的联系,那么就会进入崩溃恢复模式。
在 Zab 协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的 Leader 服务器。因此 Zab 协议需要一个高效且可靠的 Leader 选举算法,从而确保能够快速选举出新的 Leader 。
Leader 选举算法不仅仅需要让 Leader 自己知道自己已经被选举为 Leader ,同时还需要让集群中的所有其他机器也能够快速感知到选举产生的新 Leader 服务器。
崩溃恢复主要包括两部分:Leader选举 和 数据恢复
Zab 协议如何保证数据一致性
假设两种异常情况:
1、一个事务在 Leader 上提交了,并且过半的 Folower 都响应 Ack 了,但是 Leader 在 Commit 消息发出之前挂了。
2、假设一个事务在 Leader 提出之后,Leader 挂了。
要确保如果发生上述两种情况,数据还能保持一致性,那么 Zab 协议选举算法必须满足以下要求:
Zab 协议崩溃恢复要求满足以下两个要求:
1)确保已经被 Leader 提交的 Proposal 必须最终被所有的 Follower 服务器提交。
2)确保丢弃已经被 Leader 提出的但是没有被提交的 Proposal。
根据上述要求
Zab协议需要保证选举出来的Leader需要满足以下条件:
1)新选举出来的 Leader 不能包含未提交的 Proposal 。 即新选举的 Leader 必须都是已经提交了 Proposal 的 Follower 服务器节点。
2)新选举的 Leader 节点中含有最大的 zxid 。 这样做的好处是可以避免 Leader 服务器检查 Proposal 的提交和丢弃工作。
Zab 如何数据同步
1)完成 Leader 选举后(新的 Leader 具有最高的zxid),在正式开始工作之前(接收事务请求,然后提出新的 Proposal),Leader 服务器会首先确认事务日志中的所有的 Proposal 是否已经被集群中过半的服务器 Commit。
2)Leader 服务器需要确保所有的 Follower 服务器能够接收到每一条事务的 Proposal ,并且能将所有已经提交的事务 Proposal 应用到内存数据中。等到 Follower 将所有尚未同步的事务 Proposal 都从 Leader 服务器上同步过啦并且应用到内存数据中以后,Leader 才会把该 Follower 加入到真正可用的 Follower 列表中。
Zab 数据同步过程中,如何处理需要丢弃的 Proposal
在 Zab 的事务编号 zxid 设计中,zxid是一个64位的数字。
其中低32位可以看成一个简单的单增计数器,针对客户端每一个事务请求,Leader 在产生新的 Proposal 事务时,都会对该计数器加1。而高32位则代表了 Leader 周期的 epoch 编号。
epoch 编号可以理解为当前集群所处的年代,或者周期。每次Leader变更之后都会在 epoch 的基础上加1,这样旧的 Leader 崩溃恢复之后,其他Follower 也不会听它的了,因为 Follower 只服从epoch最高的 Leader 命令。
每当选举产生一个新的 Leader ,就会从这个 Leader 服务器上取出本地事务日志充最大编号 Proposal 的 zxid,并从 zxid 中解析得到对应的 epoch 编号,然后再对其加1,之后该编号就作为新的 epoch 值,并将低32位数字归零,由0开始重新生成zxid。
Zab 协议通过 epoch 编号来区分 Leader 变化周期,能够有效避免不同的 Leader 错误的使用了相同的 zxid 编号提出了不一样的 Proposal 的异常情况。
基于以上策略
当一个包含了上一个 Leader 周期中尚未提交过的事务 Proposal 的服务器启动时,当这台机器加入集群中,以 Follower 角色连上 Leader 服务器后,Leader 服务器会根据自己服务器上最后提交的 Proposal 来和 Follower 服务器的 Proposal 进行比对,比对的结果肯定是 Leader 要求 Follower 进行一个回退操作,回退到一个确实已经被集群中过半机器 Commit 的最新 Proposal。