zk学习笔记
ZooKeeper之前也是Hadoop下的一个子项目,在读《Hadoop权威指南》时也有简单的介绍,感觉ZooKeeper还是在很多大公司属于经常用的基础,现在公司有用到但是自己接触的比较少,所以也正好网上查查相关资料做一个大概的总结。
分布式协调技术
在介绍ZooKeeper之前先来给大家介绍一种技术——分布式协调技术。那么什么是分布式协调技术?其实分布式协调技术主要用来解决分布式环境当中多个进程之间的同步控制,让他们有序的去访问某种临界资源,防止造成”脏数据”的后果。这时,有人可能会说这个简单,写一个调度算法就轻松解决了。说这句话的人,可能对分布式系统不是很了解,所以才会出现这种误解。如果这些进程全部是跑在一台机上的话,相对来说确实就好办了,问题就在于他是在一个分布式的环境下,这时问题又来了,那什么是分布式呢?来咱们看一下这张图

在这图中有三台机器,每台机器各跑一个应用程序。然后我们将这三台机器通过网络将其连接起来,构成一个系统来为用户提供服务,对用户来说这个系统的架构是透明的,他感觉不到我这个系统是一个什么样的架构。那么我们就可以把这种系统称作一个
分布式系统 。
那我们接下来再分析一下,在这个分布式系统中如何对进程进行调度,我假设在第一台机器上挂载了一个资源,然后这三个物理分布的进程都要竞争这个资源,但我们又不希望他们同时进行访问,这时候我们就需要一个
协调器 ,来让他们有序的来访问这个资源。这个协调器就是我们经常提到的那个 锁
,比如说”进程-1”在使用该资源的时候,会先去获得锁,”进程1”获得锁以后会对该资源保持 独占
,这样其他进程就无法访问该资源,”进程1”用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作
分布式锁 。这个分布式锁也就是我们 分布式协调技术 实现的核心内容,那么如何实现这个分布式呢,那就是我们后面要讲的内容。
好我们知道,为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分
布式锁 。那么这个锁怎么实现呢?这实现起来确实相对来说比较困难的。
有人可能会感觉这不是很难。无非是将原来在同一台机器上对进程调度的原语,通过网络实现在分布式环境中。是的,表面上是可以这么说。但是问题就在网络这,在分布式系统中,所有在同一台机器上的假设都不存在:因为网络是不可靠的。
比如,在同一台机器上,你对一个服务的调用如果成功,那就是成功,如果调用失败,比如抛出异常那就是调用失败。但是在分布式环境中,由于网络的不可靠,你对一个服务的调用失败了并不表示一定是失败的,可能是执行成功了,但是响应返回的时候失败了。还有,A和B都去调用C服务,在时间上
A还先调用一些,B后调用,那么最后的结果是不是一定A的请求就先于B到达呢?
这些在同一台机器上的种种假设,我们都要重新思考,我们还要思考这些问题给我们的设计和编码带来了哪些影响。还有,在分布式环境中为了提升可靠性,我们往往会部署多套服务,但是如何在多套服务中达到一致性,这在同一台机器上多个进程之间的同步相对来说比较容易办到,但在分布式环境中确实一个大难题。
所以分布式协调远比在同一台机器上对多个进程的调度要难得多,而且如果为每一个分布式应用都开发一个独立的协调程序。一方面,协调程序的反复编写浪费,且难以形成通用、伸缩性好的协调器。另一方面,协调程序开销比较大,会影响系统原有的性能。所以,急需一种高可靠、高可用的通用协调机制来用以协调分布式应用。
目前,在分布式协调技术方面做得比较好的就是Google的Chubby还有Apache的ZooKeeper他们都是分布式锁的实现者。有人会问既然有了Chubby为什么还要弄一个ZooKeeper,难道Chubby做得不够好吗?不是这样的,主要是Chbby是非开源的,Google自家用。后来雅虎模仿Chubby开发出了ZooKeeper,也实现了类似的分布式锁的功能,并且将ZooKeeper作为一种开源的程序捐献给了Apache,那么这样就可以使用ZooKeeper所提供锁服务。而且在分布式领域久经考验,它的可靠性,可用性都是经过理论和实践的验证的。所以我们在构建一些分布式系统的时候,就可以以这类系统为起点来构建我们的系统,这将节省不少成本,而且bug也
将更少。
什么是ZooKeeper
从本质上来说,Zookeeper就是一种分布式协调服务,在分布式环境中协调和管理服务是一个复杂的过程。ZooKeeper通过其简单的架构和API解决了这个问题。
ZooKeeper允许开发人员专注于核心应用程序逻辑,而不必担心应用程序的分布式特性。Zookeeper最早的应用是在Hadoop生态中,Apache
HBase使用ZooKeeper跟踪分布式数据的状态。
实际上从它的名字上就很好理解,Zoo - 动物园,Keeper -
管理员,动物园中有很多种动物,这里的动物就可以比作分布式环境下多种多样的服务,而Zookeeper做的就是管理这些服务。
ZooKeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
原语:
操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。
Zookeeper提供服务主要就是通过:数据结构 + 原语集 + watcher机制达到的。
分布式应用程序结合Zookeeper可以实现诸如
数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列 等功能。
ZooKeeper的数据模型

从上图可以看到,Zookeeper的数据模型和Unix的文件系统目录树很类似,拥有一个层次的命名空间。这里面的每一个节点都被称为 - ZNode,
节点可以拥有子节点,同时也允许少量数据节点存储在该节点之下。(可以理解成一个允许一个文件也可以是一个目录的文件系统)
节点引用方式
ZNode通过路径引用,如同Unix中的文件路径。路径必须是绝对的,因此他们必须有斜杠字符/来开头,除此之外,路径名必须是唯一的,且不能更改。
ZNode结构
Node兼具文件和目录两种特点,既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分。
ZNode由以下几部分组成:
- Stat数据结构
* 操作控制列表(ACL) - 每个节点都有一个ACL来做节点的操作控制,这个列表规定了用户的权限,限定了特定用户对目标节点的操作
* CREATE - 创建子节点的权限
* READ - 获取节点数据和子节点列表的权限
* WRITE - 更新节点数据的权限
* DELETE - 删除子节点的权限
* ADMIN - 设置节点ACL的权限
* 版本 - ZNode有三个数据版本
* **version** \- 当前ZNode的版本
* **cversion** \- 当前ZNode子节点的版本
* **aversion** \- 当前ACL列表的版本
* Zxid
* 可以理解成Zookeeper中 **时间戳的一种表现形式** ,也可以理解成 **事务ID** 的概念
* 如果Zxid1的值小于Zxid2的值,那么Zxid1所对应的事件发生在Zxid2所对应的事件之前。
* ZooKeeper的每个节点维护着三个Zxid值,分别为:cZxid、mZxid、pZxid。
* **cZxid** :节点创建时间 create
* **mZxid** :节点最近一次修改时间 modify
* **pZxid** :该节点的子节点列表最后一次被修改时的时间,子节点内容变更不会变更pZxid
data域
children节点
其中,
Stat。是状态信息/节点属性;
Data域,Zookeeper中每个节点存储的数据要被 原子性的操作
,也就是说读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。值得注意的是,Zookeeper虽然可以存储数据,但是
从设计目的上,并不是为了做数据库或者大数据存储,相反,它是用来管理调度数据,比如分布式应用中的配置文件信息、状态信息、汇集位置等
,这些数据通常是很小的数据,KB为大小单位。ZNode对数据大小也有限制,至多1M。实际上从这里,就可以推导出Zookeeper用于分布式配置中心的可行性。
Zxid,在ZooKeeper中,
能改变ZooKeeper服务器状态的操作称为事务操作。一般包括数据节点创建与删除、数据内容更新和客户端会话创建与失效等操作
。对应每一个事务请求,ZooKeeper都会为其分配一个全局唯一的 事务ID ,用Zxid表示。Zxid是一个64位的数字。 前32位
叫做epoch,用来 标识Zookeeper 集群中的Leader节点,当Leader节点更换时,就会有一个新的epoch。
后32位 则为递增序列。从这些Zxid中可以间接地识别出ZooKeeper处理这些事务操作请求的全局顺序。
节点类型
ZNode节点类型严格来说有四种: 持久节点、临时节点、持久顺序节点、临时顺序节点
- PERSISTENT 持久节点 - 该节点的生命周期不依赖于session,创建之后客户端断开连接,节点依旧存在,只有客户端执行删除操作,节点才能被删除;
- EPHEMERAL 临时节点 - 该节点的声明周期依赖于session,客户端断开连接,临时节点就会自动删除。另外, 临时节点不允许有子节点。
- SEQUENTIAL 顺序节点 - 当选择创建顺序节点时,ZooKeeper通过将10位的序列号附加到原始名称来设置znode的路径。例如,如果将具有路径
/myapp的znode创建为顺序节点,则ZooKeeper会将路径更改为/myapp0000000001,并将下一个序列号设置为0000000002。如果两个顺序节点是同时创建的,那么ZooKeeper不会对每个znode使用相同的数字。 顺序节点在锁定和同步中起重要作用。 这个计数 对于此节点的父节点来说 是唯一的,它的格式为”%10d”(10位数字,没有数值的数位用0补充,例如”0000000001”)。当计数值大于2^32-1时,计数器将溢出。
节点属性

观察
客户端可以在节点上设置watch,我们称之为 监视器
。当节点状态发生改变时(Znode的增、删、改)将会触发watch所对应的操作。当watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次,这样可以减少网络流量。
Zookeeper服务基本操作

更新ZooKeeper操作是有限制的。delete或setData必须明确要更新的Znode的版本号,我们可以调用exists找到。如果版本号不匹配,更新将会失败。
更新ZooKeeper操作是非阻塞式的。因此客户端如果失去了一个更新(由于另一个进程在同时更新这个Znode),他可以在不阻塞其他进程执行的情况下,选择重新尝试或进行其他操作。
尽管ZooKeeper可以被看做是一个文件系统,但是处于便利,摒弃了一些文件系统地操作原语。因为文件非常的小并且使整体读写的,所以不需要打开、关闭或是寻地的操作。
Sessions
在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接 。客户端启动的时候,首先会与服务器建立一个 TCP
连接,从第一次连接建立开始,客户端会话的生命周期也开始了。
通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。
客户端以特定的时间间隔发送心跳以保持会话有效。如果ZooKeeper Server
Ensembles在超过服务器开启时指定的期间(会话超时)都没有从客户端接收到心跳,则它会判定客户端死机。
会话超时通常以毫秒为单位。当会话由于任何原因结束时,在该会话期间创建的临时节点也会被删除。
Watches
Watches - 监听事件 ,是Zookeeper中一个很重要的特性,也是实现Zookeeper大多数功能的核心特性之一。简单来说,
Zookeeper允许Client端在指定节点上注册Watches,在某些特定事件触发的时候,Zookeeper服务端会将事件异步通知到感兴趣(即注册了Watches)的客户端上去
。可以理解成一个订阅/发布系统,是不是。
Znode更改是与znode相关的数据的修改或znode的子项中的更改。只触发一次watches。如果客户端想要再次通知,则必须通过另一个读取操作来完成。当连接会话过期时,客户端将与服务器断开连接,相关的watches也将被删除。
下面说完简单的,来说点复杂的部分。
几个特性先了解下:
- One-time trigger 一次watch时间只会被触发一遍,如果节点再次发生变化,除非之前有重新设置过watches,不然会收到通知;
- Sent to Client 当watch的对象状态发生改变时,将会触发此对象上watch所对应的事件。watch事件将被异步地发送给客户端,并且ZooKeeper为watch机制提供了**有序的一致性保证(Ordering guarantee)**。
- The data for which the watch was set 发送给客户端的数据信息,实际上就是你这个watch监视的类型,见下文介绍
Zookeeper的Watches 分为两种, 数据监听器(Data Watches)和子节点监听器(Children Watches)
。即你可以对某个节点的Data设置watches,也可以对某个子节点设置watches。
可以看下Zookeeper Java 客户端 Zkclient 中的设置watches的代码:
// listener 监听器
// path 节点路径
// 子节点监听器
private List<String> addTargetChildListener(String path, IZkChildListener listener) {
return client.subscribeChildChanges(path, listener);
}
// 节点数据的监听器
public void addChildDataListener(String path, IZkDataListener listener) {
try {
// 递归创建节点
client.subscribeDataChanges(path, listener);
} catch (ZkNodeExistsException e) {
}
}
作为开发者,需要知道监控节点的什么操作会触发你设置的watches。
- 一个成功的setData操作将触发Znode的数据watches
- 一个成功的create操作将触发Znode的数据watches以及子节点watches
- 一个成功的delete操作将触发Znode的数据watches和子节点watches
再看下ZkClient中的数据监听器接口IZkDataListener
public interface IZkDataListener {
// 监控节点数据更新的时候会触发 这段逻辑
public void handleDataChange(String dataPath, Object data) throws Exception;
// 监控节点被删除的时候会触发 这段逻辑
public void handleDataDeleted(String dataPath) throws Exception;
}
再看下ZkClient中的子节点监听器接口IZkChildListener
public interface IZkChildListener {
/**
* Called when the children of the given path changed.
* 监控节点的子节点列表改变时会触发这段逻辑
*
* @param parentPath
* The parent path
* @param currentChilds
* The children or null if the root node (parent path) was deleted.
* @throws Exception
*/
public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception;
}
实际上看到这就能联想到,Zookeeper是可以当做分布式配置中心来使用的,只不过你需要自己扩展他异步通知节点数据变化之后的逻辑,更新你的配置。
(1) watch概述
ZooKeeper可以为所有的 读操作
设置watch,这些读操作包括:exists()、getChildren()及getData()。watch事件是 一次性的触发器
,当watch的对象状态发生改变时,将会触发此对象上watch所对应的事件。watch事件将被 异步
地发送给客户端,并且ZooKeeper为watch机制提供了有序的 一致性保证
。理论上,客户端接收watch事件的时间要快于其看到watch对象状态变化的时间。
(2) watch类型
ZooKeeper所管理的watch可以分为两类:
① 数据watch(data watches): getData 和 exists 负责设置数据watch
② 孩子watch(child watches): getChildren 负责设置孩子watch
我们可以通过操作 返回的数据 来设置不同的watch:
① getData和exists: 返回关于节点的数据信息
② getChildren: 返回孩子列表
因此
① 一个成功的 setData操作 将触发Znode的数据watch
② 一个成功的 create操作 将触发Znode的数据watch以及孩子watch
③ 一个成功的 delete操作 将触发Znode的数据watch以及孩子watch
(3) watch注册与处触发

① exists操作上的watch,在被监视的Znode 创建 、 删除 或 数据更新 时被触发。
② getData操作上的watch,在被监视的Znode 删除 或 数据更新
时被触发。在被创建时不能被触发,因为只有Znode一定存在,getData操作才会成功。
③ getChildren操作上的watch,在被监视的Znode的子节点 创建 或 删除 ,或是这个Znode自身被
删除
时被触发。可以通过查看watch事件类型来区分是Znode,还是他的子节点被删除:NodeDelete表示Znode被删除,NodeDeletedChanged表示子节点被删除。
Watch由客户端所连接的ZooKeeper服务器在本地维护,因此watch可以非常容易地设置、管理和分派。当客户端连接到一个新的服务器时,任何的会话事件都将可能触发watch。另外,当从服务器断开连接的时候,watch将不会被接收。但是,当一个客户端重新建立连接的时候,任何先前注册过的watch都会被重新注册。
(4) 需要注意的几点
Zookeeper的watch实际上要处理两类事件:
① 连接状态事件 (type=None, path=null)
这类事件不需要注册,也不需要我们连续触发,我们只要处理就行了。
② 节点事件
节点的建立,删除,数据的修改。它是one time trigger,我们需要不停的注册触发,还可能发生事件丢失的情况。
上面2类事件都在Watch中处理,也就是重载的 process(Event event)
节点事件的触发,通过函数exists,getData或getChildren来处理这类函数,有双重作用:
① 注册触发事件
② 函数本身的功能
函数的本身的功能又可以用异步的回调函数来实现,重载processResult()过程中处理函数本身的的功能。
Zookeeper特性总结
现在我们再回过头来看看Zookeeper的特性:
① 顺序一致性 从同一个客户端发起的事务请求,最终将会严格按照其发起顺序被应用到ZooKeeper中。
② 原子性
所有事务请求的结果在集群中所有机器上的应用情况是一致的,也就是说要么整个集群所有集群都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况。
③ 单一视图 无论客户端连接的是哪个ZooKeeper服务器,其看到的服务端数据模型都是一致的。
④ 可靠性 一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。
⑤ 实时性
通常人们看到实时性的第一反应是,一旦一个事务被成功应用,那么客户端能够立即从服务端上读取到这个事务变更后的最新数据状态。这里需要注意的是,ZooKeeper仅仅保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
顺序一致性 是通过ZXid来实现的,全局唯一,顺序递增,同一个session中请求是FIFO的; 可靠性
的描述也可以通过今天的知识进行理解,一次事务的应用,服务端状态的变更会以Zxid、Znode数据版本、数据、节点路径的形式保存下来。剩下的几种特性是怎么实现的,后面会进行介绍
Zookeeper运行模式
Zookeeper 有两种运行模式, 单点模式 和 集群模式 。
单点模式(standalone mode)- Zookeeper 只运行在单个服务器上,常用于开发测试阶段,这种模式比较简单,但是不能保证Zookeeper服务的高可用性和恢复性。
集群模式(replicated mode)- 英文原文这种模式叫做“复制模式”;这个模式下,Zookeeper运行于一个集群上,适合生产环境。
同一个集群下的server节点被称为 quorum ,翻译过来就是“一个正式会议的法定人数”,如果你看完下一章介绍的 ZAB协议的两种模式
之后,应该会觉得这个比喻实际上很形象。
NOTE:
在集群模式下,最少需要三个server节点。并且官方推荐你使用奇数数量的server节点来组成集群。至于为什么,和Zookeeper的读写策略和一致性协议有关,在后面的章节会介绍。
Zookeeper的集群架构

Zookeeper集群中的角色
Zookeeper中,
能改变ZooKeeper服务器状态的操作称为事务操作。一般包括数据节点创建与删除、数据内容更新和客户端会话创建与失效等操作 。
- Leader 领导者 :Leader 节点负责Zookeeper集群内部投票的发起和决议(一次事务操作),更新系统的状态;同时它也能接收并且响应Client端发送的请求。
- Learner 学习者
- Follower 跟随者 : Follower 节点用于接收并且响应Client端的请求,如果是事务操作,会将请求转发给Leader节点,发起投票,参与集群的内部投票,
- Observer 观察者 :Observer 节点功能和Follower相同,只是Observer 节点不参与投票过程,只会同步Leader节点的状态。
- Client 客户端
Zookeeper 通过 复制 来实现 高可用 。在上面提到的集群模式(replicated
mode)下,以Leader节点为准,Zookeeper的ZNode树上面的每一个修改都会被同步(复制)到其他的Server 节点上面。
上面实际上只是一个概念性的简单叙述,在看完下文的 读写机制 和 ZAB协议的两种模式 之后,你就会对这几种角色有一个更加深刻的认识。
Zookeeper 读写机制
下图就是集群模式下一个Zookeeper Server节点提供读写服务的一个流程。

如上图所示,每个Zookeeper
Server节点除了包含一个请求处理器来处理请求以外,都会有一个**内存数据库(ReplicatedDatabase)**用于持久化数据。ReplicatedDatabase
包含了整个Data Tree。
来自于Client的读服务(Read Requst),是直接由对应Server的本地副本来进行服务的。
至于来自于Client的写服务(Write
Requst),因为Zookeeper要保证每台Server的本地副本是一致的(单一系统映像),需要通过一致性协议(后文提到的ZAB协议)来处理,成功处理的写请求(数据更新)会先序列化到每个Server节点的本地磁盘(为了再次启动的数据恢复)再保存到内存数据库中。
集群模式下,Zookeeper使用简单的同步策略,通过以下三条基本保证来实现 数据的一致性 :
- 全局 串行化 所有的 写操作
串行化可以把变量包括对象,转化成连续bytes数据. 你可以将串行化后的变量存在一个文件里或在网络上传输. 然后再反串行化还原为原来的数据。
- 保证 同一客户 端的指令被FIFO执行(以及消息通知的FIFO)
FIFO -先入先出
- 自定义的原子性消息协议
简单来说,对数据的写请求,都会被转发到Leader节点来处理,Leader节点会对这次的更新发起投票,并且发送提议消息给集群中的其他节点,当半数以上的Follower节点将本次修改持久化之后,Leader
节点会认为这次写请求处理成功了,提交本次的事务。
乐观锁
Zookeeper 的核心思想就是,提供一个 非锁机制的Wait Free 的用于分布式系统同步的核心服务 。其核心对于文件、数据的读写服务,并
不提供加锁互斥的服务 。
但是由于Zookeeper的每次更新操作都会更新ZNode的版本,也就是客户端可以自己基于版本的对比,来实现更新数据时的加锁逻辑。例如下图。

就像我们更新数据库时,会新增一个version字段,通过更新前后的版本对比来实现乐观锁。
ZAB协议
ZAB 协议是为分布式协调服务ZooKeeper专门设计的一种 支持崩溃恢复 的 一致性协议
,这个机制保证了各个server之间的同步。全称 Zookeeper Atomic Broadcast Protocol - Zookeeper
原子广播协议。
两种模式
Zab协议有两种模式,它们分别是 恢复模式 和 广播模式 。
广播模式
广播模式类似于分布式事务中的 Two-phase commit
(两阶段式提交),因为Zookeeper中一次写操作就是被当做一个事务,所以这实际上本质是相同的。
在 广播模式 ,一次写请求要经历以下的步骤
- ZooKeeper Server接受到Client的写请求
- 写请求都被转发给
Leader节点 Leader节点先将更新持久化到本地Leader节点将此次更新提议(propose)给Followers,进入收集选票的流程Follower节点接收请求,成功将修改持久化到本地,发送一个ACK给LeaderLeader接收到半数以上的ACK时,Leader将广播commit消息并在本地deliver该消息。- 当收到
Leader发来的commit消息时,Follower也会deliver该消息。
广播协议在所有的通讯过程中使用TCP的FIFO信道,通过使用该信道,使保持有序性变得非常的容易。通过FIFO信道,消息被有序的deliver。只要收到的消息一被处理,其顺序就会被保存下来。
但是这种模式下,如果Leader自身发生了故障,Zookeeper的集群不就提供不了写服务了吗?这就引入了下面的恢复模式。
恢复模式
简单点来说,当集群中的Leader 故障 或者 服务启动
的时候,ZAB就会进入恢复模式,其中包括Leader选举和完成其他Server和Leader之间的 状态同步 。
单点故障
在分布式锁服务中,有一种最典型应用场景,就是通过对集群进行 Master选举 ,来解决分布式系统中的 单点故障
。什么是分布式系统中的单点故障:通常分布式系统采用主从模式,就是一个主控机连接多个处理节点。主节点负责分发任务,从节点负责处理任务,当我们的主节点发生故障时,那么整个系统就都瘫痪了,那么我们把这种故障叫作单点故障。


传统解决方案
传统方式是采用一个备用节点,这个备用节点定期给当前主节点发送ping包,主节点收到ping包以后向备用节点发送回复Ack,当备用节点收到回复的时候就会认为当前主节点还活着,让他继续提供服务。

当主节点挂了,这时候备用节点收不到回复了,然后他就认为主节点挂了接替他成为主节点

但是这种方式就是有一个隐患,就是网络问题,来看一看网络问题会造成什么后果

也就是说我们的主节点的并没有挂,只是在回复的时候网络发生故障,这样我们的备用节点同样收不到回复,就会认为主节点挂了,然后备用节点将他的Master实例启动起来,这样我们的分布式系统当中就有了两个主节点也就是—
双Master
,出现Master以后我们的从节点就会将它所做的事一部分汇报给了主节点,一部分汇报给了从节点,这样服务就全乱了。为了防止出现这种情况,我们引入了ZooKeeper,它虽然不能避免网络故障,但它能够保证每时每刻只有一个Master。我么来看一下ZooKeeper是如何实现的。
zookeeper解决方案
首先每个Server在工作过程中有四种状态:
LOOKING:竞选状态,当前Server不知道leader是谁,正在搜寻。
LEADING:领导者状态,表明当前服务器角色是leader。
FOLLOWING:随从状态,表明当前服务器角色是follower,同步leader状态,参与投票。
OBSERVING,观察状态,表明当前服务器角色是observer,同步leader状态,不参与投票。

假设我们目前有一个3个节点构成的ZooKeeper集群,myid的编号分别是0,1,2,又因为集群当前是一个空的集群,所以
每个节点的ZXID初始都为0 ,该集群启动的时候Leader的选举流程如下:
我们首先启动myid为0的服务,但是目前只有一台ZooKeeper服务,所以是无法完成Leader选举的,ZooKeeper集群要求Leader进行投票选举条件是至少有2台服务才行,不然都没法进行通信投票。
启动myid为1的服务,第二台启动了以后,这两台ZooKeeper就可以相互通信了,接下来就可以进行投票选举了。
2台ZooKeeper进行投票选举的时候,第一次都是推荐自己为Leader,投票包含的信息是:服务器本身的myid和ZXID。比如第一台投自己的话,它会发送给第二台机器的投票是(0,0),第一个0代表的是机器的myid,第二个0代表是的ZXID。故两台机器收到的投票情况如下:
第一台:(1,0)
第二台:(0,0)
- 两台服务器在接收到投票后,将别人的票和自己的投票进行PK。PK的是规则是:
(a)优先对比ZXID,ZXID大的优先作为Leader(ZXID大的表示数据多)
(b)如果ZXID一样的话,那么就比较myid,让myid大的作为Leader服务器。
那根据这个规则的话,第一台服务器,接受到的投票是(1,0),跟自己的投票(0,0)比,ZXID是一样的,但是myid比接收到的投票的小,所以第一台原先是推荐自己投票为(0,0),现在进行了PK以后,投票修改为(1,0)。第二台服务器,接受到的投票是(0,0),跟自己的投票(1,0)比,ZXID是一样的,但是myid是比接受到的投票的大,所以坚持自己的投票(1,0)。两台服务器再次进行投票。
- 每次投票以后,服务器都会统计所有的投票,只要过半的机器投了相同的机器,那么Leader就选举成功了,上面的两台服务器进行第二次投票之后,两台服务器都会收到相同的投票(1,0)。那么此时myid为1的服务器就是Leader了。
如上的Leader选举其实在集群启动的过程中只需要几毫秒就完成了,所以如果有搭建ZooKeeper集群经验的同学会发现,我们如果按顺序启动服务的话,启动到第二台机器的时候,Leader就已经选出来了,所以大家会看到一般第二台就是Leader。第三台启动的时候就作为Follower。
上面我们描述的是集群在初始化过程中Leader的选举流程,如果集群在运行的过程中Follower节点宕机了,对Leader节点是不影响的,如果集群在运行的过程中Leader节点宕机了,就会进行重新选举,重新选举的流程跟上述一致。

关于Zookeeper 集群的一些其他讨论
Zookeeper(读性能)可伸缩性 和 Observer节点
一个集群的可伸缩性即可以引入更多的集群节点,来提升某种性能。Zookeeper实际上就是提供读服务和写服务。在最早的时候,Zookeeper是通过引入Follower节点来提升
读服务 的性能。但是根据我们之前学习过的读写机制和ZAB协议的内容,引入新的Follower节点,会造成Zookeeper
写服务的下降,因为Leader发起的投票是要半数以上的Follower节点响应才会成功,你Follower多了,就增加了协议中投票过程的压力,可能会拖慢整个投票响应的速度。结果就是,**Follower节点增加,集群的写操作吞吐会下降**。
在这种情况下,Zookeeper
在3.3.3版本之后,在集群架构中引入了Observer角色,和Follower唯一的区别的就是不参与投票不参与选主。这样既提升了读性能,又不会影响写性能。
另外提一句,Zookeeper的写性能是不能被扩展的,这也是他不适合当做服务注册发现中心的一个原因之一,在服务发现和健康监测场景下,随着服务规模的增大,无论是应用频繁发布时的服务注册带来的写请求,还是刷毫秒级的服务健康状态带来的写请求,都会Zookeeper带来很大的写压力,因为它本身的写性能是无法扩展的。后文引的文章会详细介绍。
Zookeeper 与 CAP 理论
分布式领域中存在CAP理论:
- C:Consistency ,一致性,数据一致更新,所有数据变动都是同步的。
- A:Availability ,可用性,系统具有好的响应性能。
- P:Partition tolerance ,分区容错性。以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择,也就是说无论任何消息丢失,系统都可用。
该理论已被 证明 :任何分布式系统只可同时满足两点,无法三者兼顾。
因此,将精力浪费在思考如何设计能满足三者的完美系统上是愚钝的,应该根据应用场景进行适当取舍。
根据我们前面学习过的读写机制和ZAB协议的内容,Zookeeper本质应该是一个偏向CP的分布式系统。因为广播协议本质上是牺牲了系统的响应性能的。另外从它的以下几个特点也可以看出。也就是在第一章最后提出的几个特点。
① 顺序一致性 从同一个客户端发起的事务请求,最终将会严格按照其发起顺序被应用到ZooKeeper中。
② 原子性
所有事务请求的结果在集群中所有机器上的应用情况是一致的,也就是说要么整个集群所有集群都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况。
③ 单一视图 无论客户端连接的是哪个ZooKeeper服务器,其看到的服务端数据模型都是一致的。
④ 可靠性 一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。
Zookeeper 作为 服务注册中心的局限性
直接引一篇阿里中间件的文章吧,讲的比我好。实际在生产情况下,大多数公司没有达到像大公司那样的微服务量级,Zookeeper是完全能满足服务注册中心的需求的。