Zookeeper技术手册
Zookeeper技术手册
什么是zookeeper
ZooKeeper 是一个开放源码的分布式协调服务,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应。
Zookeeper 保证了如下分布式一致性特性:
- 顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到ZooKeeper中去[FIFO]。
- 原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群所有机器都成功应用了某一个事务,要么都没有应用,一定不会出现集群中部分机器应用了该事务,而另外一部分没有应用的情况。
- 单一视图:无论客户端连接的是哪个 ZooKeeper服务器,其看到的服务端数据模型都是一致的。
- 可靠性:一旦服务端成功地应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务又对其进行了变更。
- 实时性(最终一致性):通常人们看到实时性的第一反应是,一旦一个事务被成功应用,那么客户端能够立即从服务端上读取到这个事务变更后的最新数据状态。这里需要注意的是ZooKeeper仅仅保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态。
zookeeper满足的是cp原则,但是这里的一致性,指的是弱一致性,而不是强一致性。
客户端的读请求可以被集群中的任意一台机器处理,如果读请求在节点上注册了监听器,这个监听器也是由所连接的 zookeeper 机器来处理。对于写请求, 这些请求会先发送给leader节点,同时发给其他 zookeeper 机器并且达成一致后(这里使用额是过半机制),请求才会返回成功。因此,随着 zookeeper的集群机器增多,读请求的吞吐会提高但是写请求的吞吐会下降。
有序性是 zookeeper 中非常重要的一个特性,所有的更新都是全局有序的,每个更新都有一个唯一的时间戳 ,这个时间戳称为 zxid(Zookeeper Transaction Id)。而读请求只会相对于更新有序,也就是读请求的返回结果中会带有这个 zookeeper 最新的 zxid。
谈谈你对ZooKeeper的理解?
Zookeeper 作为一个分布式的服务框架,主要用来解决分布式集群中应用系统的数据一致性问题,多个个体之间需要相互通信,有两种方式去实现:
- 一种是让个体与个体之间进行迭代时交流,比如a和b,b和c以此类推这种方式。
- 另一种是引入一个外部系统,让这个外部系统作为一个中间人去协调各个外部的独立个体相互通信。
总的来说,代表两种处理问题的方式:
- ⼀种是独⽴个体之间互相直接交流来解决,
- ⼀种是需要第三⽅介⼊来协调解决。
这里可以类比在hadoop中引入yarn外部调度框架机制,解耦行很好。
那么对于多个进程之间的互相交流的解决⽅法也是这样的,由⼀个第三⽅⽆关进程介⼊来协调处理,此时这个第三⽅就是ZooKeeper。所以zookeeper就像一个家长或者调度者,来协调各个实体之间进行通信。
这种⽅法还有⼀个好处,就是在⼀定程度上降低了个体的复杂性与要求,以及由此产⽣的额外问题。
对于进程来说,降低了对业务开发⼈员的要求,不需要具备完整的进程间通信相关知识,同时降低了进程本⾝的复杂度,不需要⽀持完整的进程间通信,可能只需⽀持客⼾端即可。
这种⽅式的另⼀个好处是可以被抽象出来做成⼀个独⽴的中间件供⼤家使⽤,ZooKeeper就是这样的。
所以从本质来说,ZooKeeper就是⼀个第三⽅,也称中间⼈,它搭建了⼀个平台,让所有其它进程通过它来进⾏间接的交流,保证整个系统的数据一致性 。
zookeeper提供什么服务
我们从最常⻅的场景⼊⼿,从宏观上了解下zookeeper是如何使⽤的,以及它应该具备哪些能⼒。
场景⼀
有两个应⽤程序进程A和B,A先处理数据,处理完后通知B,B再接着处理。我们应该如何利⽤zookeeper来完成这个呢?⼀起来分析⼀下。
⾸先,进程A连接上zookeeper,在上⾯创建⼀个节点来表⽰⾃⼰的存在,假设节点名称就叫foo吧。
然后在节点上设置⼀个数据叫doing,表⽰⾃⼰正在处理数据。过了⼀会处理完后,把节点上的数据更新为done。这样进程A的⼯作就算完了。可是这怎么去影响到进程B呢?我们知道zookeeper完成的是进程间的间接交流,即进程之间是不碰⾯的。因此只能借助于这个树形⾥的节点。
进程B也要连上zookeeper,然后找到foo节点,看好它上⾯的数据是否由doing变成了done,如果是⾃⼰就开始处理数据,如果否那就继续等着。
问题是进程B不能⾃⼰⽼盯着foo节点啊,这样太累了,伤神,况且它还要做其它事情呢。那这个事情应该由谁来做呢?很显然是zookeeper嘛。
于是进程B就对zookeeper说,你给我盯着foo节点,什么时候变成done了通知我⼀声,我就开⼲了。
因此,zookeeper需要具有盯梢能⼒和通知其它进程的能⼒。这在zookeeper中对应⼀个专业术语,叫Watch。
Watch的作⽤和⽤法与上⾯描述的⼀样。就是进程B找到foo节点,在上⾯放⼀个Watch就可以了。
这样zookeeper就知道进程B对foo节点⽐较关注,于是zookeeper就盯着foo节点,⼀有⻛吹草动,⻢上通知进程B。
注:关于Watch有⾮常多的细节问题,这⾥就不谈了。
需要注意的是,这个Watch是⼀次性的,即只能使⽤⼀次。也就是说,zookeeper通知过进程B之后,Watch就被⽤掉了,以后就不会再通知了。
如果进程B还需要被通知怎么办?很简单,那就在foo节点上再放⼀个新的Watch即可。如此这般下去,就可以保证⼀直被通知了。
我想这个Watch之所以被设计成⼀次性的,就是zookeeper不想让⾃⼰太累。睁着⼀双⼤眼,盯的东西太多太久的话,确实很累。
另外,zookeeper在通知进程B的时候,是可以把foo节点存放的数据⼀并发送过去的。
细⼼的朋友可能已经发现,zookeeper可以主动向进程B发通知或推数据,说明zookeeper和进程B之间的连接需要被⼀直保持。
因为进程B的位置⽐较随意,本来就是业务进程嘛。⼀旦连接断开,就像断了线的⻛筝,zookeeper再也⽆法找到进程B了。
不过zookeeper的位置是固定的,⼀旦连接断掉后,进程B可以再次向zookeeper发起连接请求,如果断开的时间⾜够短的话,进程B应该还可以在zookeeper上找回⾃⼰曾经拥有的⼀切。
这就涉及到了会话,因此zookeeper还要有⼀定的会话延续能⼒,⽅便在断开时间不⻓的时候找回原来的会话。
因此zookeeper应该有,监视节点、通知进程、保持⻓连接,会话延续等这样的能⼒。
场景⼆
有时为了⾼可⽤或⾼性能,通常会把⼀个应⽤程序运⾏多份。假如运⾏了四份,那就是四个进程,分别是A、B、C、D。
当⼀个调⽤过来时,发现A、B、C、D都可以调,那就根据配置的负载均衡策略选出⼀个调⽤即可。
假设D进程所在的机器不幸掉电了,其实就是D挂了,那么此时再来⼀个调⽤的话,会发现只有A、B、C可以调,D⾃动就不存在了。
这其实就是Dubbo功能的⼀部分,那该如何基于zookeeper实现呢?照例⼀起分析下吧。
由于zookeeper是基于树形的数据结构,所以还是要拿节点说事。当进程A启动时,需要连接上zookeeper,然后创建⼀个节点来代表⾃⼰。
节点名称和节点上存放的数据可以根据实际情况来定,⾄少要包括该进程运⾏的IP和端⼝信息。进程B、C、D也做同样的事情。
如果让进程A、B、C、D的节点都位于同⼀个⽗节点下⾯,这样当⼀个调⽤过来后,只要找到这个⽗节点,读出它的所有⼦节点,就得到了所有可调⽤的进程信息。
如果某⼀时刻,进程D挂掉了,那么⽗节点下⾯进程D对应的那个节点应该会⾃动被zookeeper删除。这在
zookeeper⾥有个专业术语,叫临时节点(Ephemeral Node)。那么与之对应的⾃然就是永久节点了。
其实⼯作过程是这样的,业务进程启动后与zookeeper建⽴连接,然后在zookeeper⾥创建临时节点并写⼊⾃⼰的相关信息。接着通过周期性的⼼跳和zookeeper保持住连接。
⼀旦业务进程挂掉,zookeeper将接受不到⼼跳了,那么在超过⼀定的时间后,zookeeper将会删除与之对应的临时节点,表⽰这个业务进程不再可⽤了。
Dubbo的做法是将接⼝名称和IP端⼝信息和我们设置的信息整合成⼀个类似URL的字符串,然后以这个字符串作为名称来创建临时节点。
临时节点不允许有孩⼦节点,只有永久节点才可以。
因此ZooKeeper提供的服务包括:
- 分布式消息同步和协调机制
- 服务器节点动态上下线
- 统一配置管理
- 负载均衡
- 集群管理
zookeeper的数据结构
数据结构,相比都非常的了解,链表,数据,树等等很多,那么zookeeper选择树作为自己的数据结构。
ZooKeeper提供基于类似于Linux文件系统的目录节点树方式的数据存储,即分层命名空间。
Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化,通过监控这些数据状态的变化,从而可以达到基于数据的集群管理,ZooKeeper节点的数据上限是1MB
我们可以认为Zookeeper=文件系统+通知机制,对于ZooKeeper的数据结构,每个子目录项如NameService 都被称作为 znode,这个 znode 是被它所在的路径唯一标识,如 Server1 这个 znode的标识为 /NameService/Server1
简单来说,这每一个节点都可以进行增删改查操作,那么zookeeper再次基础上,增加了新的信息:
- znode 是有版本的,每个 znode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据。
- znode 可以是临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除,Zookeeper 的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为 session,如果 znode 是临时节点,这个 session 失效,znode 也就删除了
- znode 的目录名可以自动编号,如 App1 已经存在,再创建的话,将会自动命名为 App2
- znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基于这个特性实现的,后面在典型的应用场景中会有实例介绍
zookeeper的设计目标
ZooKeeper致力于提供一个高性能、高可用,且具有严格的顺序访问控制能力(主要是写操作的严格顺序性)的分布式协调服务。高性能使得 ZooKeeper能够应用于那些对系统吞吐有明确要求的大型分布式系统中,高可用使得分布式的单点问题得到了很好的解决,而严格的顺序访问控制使得客户端能够基于 ZooKeeper实现一些复杂的同步原语。下面我们来具体看一下 ZooKeeper的四个设计目标。
- 简单的数据模型:zookeeper的数据都是一个个的znode节点类型,所有数据组织成一个类似linux的文件系统,zookeeper将全量的数据全部存储在内存当中,一次来提高服务的吞吐量,减少延迟。
- 可构建集群:组成 ZooKeeper集群的每台机器都会在内存中维护当前的服务器状态,并且每台机器之间都互相保持着通信。只要集群中存在超过一半的机器能够正常工作,那么整个集群就能够正常对外服务。
- 顺序访问:对于来自客户端的每个更新请求, ZooKeeper都会分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序,应用程序可以使用 ZooKeeper的这个特性来实现更高层次的同步原语。
- 高性能:zookeeper将数据存储在内存当中,所以非常适合以读为场景的应用中。
zookeeper的基本概念
集群
通常在分布式系统中,构成一个集群的每一台机器都有自己的角色,最典型的集群模式就是 Master/Slave模式(主备模式)。
在这种模式中,我们把能够处理所有写操作的机器称为 Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器称为Slave机器。
而在 ZooKeeper中,它没有沿用传统的 Master/Save概念,而是引入了 Leader、 Follower和 Observer三种角色。
ZooKeeper集群中的所有机器通过一个Leader选举过程来选定一台被称为“ Leader”的机器, Leader服务器为客户端提供读和写服务。除 Leader外,其他机器包括 Follower和 Observer。 Follower和 Observer都能够提供读服务,唯一的区别在于, Observer机器不参与 Leader选举过程,也不参与写操作的“过半写成功”策略,因此 Observer可以在不影响写性能的情况下提升集群的读性能。
会话
Session是指客户端会话,在讲解会话之前,我们首先来了解一下客户端连接。在ZooKeeper中,一个客户端连接是指客户端和服务器之间的一个TCP长连接。
ZooKeeper对外的服务端口默认是2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 ZooKeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watch事件通知。
Session的 session Timeout值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在 session Timeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。
数据节点(Znode)
在谈到分布式的时候,我们通常说的“节点”是指组成集群的每一台机器。然而,在ZooKeeper中,“节点”分为两类,
- 第一类同样是指构成集群的机器,我们称之为机器节点;
- 第二类则是指数据模型中的数据单元,我们称之为数据节点— ZNode。
ZooKeeper将所有数据存储在內存中,数据模型是一棵树( ZNode Tree),由斜杠(/)进行分割的路径,就是一个 Znode,例如/o/pathI
。每个 ZNode上都会保存自己的数据内容,同时还会保存一系列属性信息。在 ZooKeeper中, ZNode可以分为持久节点和临时节点两类。
- 所谓持久节点是一旦这个 ZNode被创建了,除非主动进行 ZNode的移除操作,否则这个 ZNode将一直保存在ZooKeeper上。
- 而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。
- 另外, ZooKeeper还允许用户为每个节点添加一个特殊的属性: SEQUENTIAL。一旦节点被标记上这个属性,那么在这个节点被创建的时候, ZooKeeper会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。
版本
在前面我们已经提到, ZooKeeper的每个 ZNode上都会存储数据,对应于每个 ZNode,ZooKeeper都会为其维护一个叫作Stat的数据结构,Stat中记录了这个 ZNode的三个数据版本,分别是 version(当前 ZNode的版本)、 cversion(当前 ZNode子节点的版本)和 aversion(当前 ZNode的ACL版本)。
Watcher
Watcher(事件监听器),是 ZooKeeper中的一个很重要的特性。 ZooKeeper允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候, ZooKeeper服务端会将事件通知到感兴趣的客户端上去。
该机制是 ZooKeeper实现分布式协调服务的重要特性。
ACL
ZooKeeper采用ACL( Access Control lists)策略来进行权限控制,类似于UNX文件系统的权限控制。 ZooKeeper定义了如下5种权限。
- CREATE:创建子节点的权限。
- READ:获取节点数据和子节点列表的权限。
- WRITE:更新节点数据的权限。
- DELETE:删除子节点的权限。
- ADMIIN:设置节点ACL的权限。
其中尤其需要注意的是, CREATE和 DELETE这两种权限都是针对子节点的权限控制。
zookeeper的特点
- Zookeeper:一个领导者(Leader),多个跟随者(Follower)组成的集群。
- 集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所以Zookeeper适合安装奇数台服务器。
- 全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
- 更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行。
- 数据更新原子性,一次数据更新要么成功,要么失败。
- 实时性,在一定时间范围内,Client能读到最新数据。
数据结构
ZooKeeper 数据模型的结构与 Unix 文件系统很类似,整体上可以看作是一棵树,每个节点称做一个 ZNode。每一个 ZNode 默认能够存储 1MB 的数据,每个 ZNode 都可以通过其路径唯一标识。
节点数据信息
- czxid:创建节点的事务 zxid,每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生。
- ctime:znode 被创建的毫秒数(从 1970 年开始)
- mzxid:znode 最后更新的事务 zxid
- mtime:znode 最后修改的毫秒数(从 1970 年开始)
- pZxid:znode 最后更新的子节点 zxid
- cversion:znode 子节点变化号,znode 子节点修改次数
- dataversion:znode 数据变化号
- aclVersion:znode 访问控制列表的变化号
- ephemeralOwner:如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0。
- dataLength:znode 的数据长度
- numChildren:znode 子节点数量
zookeeper提供了什么
- 文件系统 + 通知机制
zookeeper文件系统
Zookeeper 提供一个多层级的节点命名空间(节点称为 znode )。
与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。
Zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构, 这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为 1M。
分布式集群中为什么会有 Master?
在分布式环境中,有些业务逻辑只需要集群中的某一台机器进行执行,其他的机器可以共享这个结果,这样可以大大减少重复计算,提高性能,于是就需要进行 leader 选举,leader负责向集群中其他的服务器同步结果。
ZAB协议
也可以认为是zookeeper的工作原理
ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复的原子广播协议。
Zookeeper 的核⼼是原⼦⼴播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。
ZAB 协议包括两种基本的模式:崩溃恢复和消息广播。
当整个 zookeeper 集群刚刚启动或者 Leader 服务器宕机、重启或者网络故障导致不存在过半的服务器与 Leader 服务器保持正常通信时,所有进程(服务器)进入崩溃恢复模式,首先选举产生新的 Leader 服务器,然后集群中Follower服务器开始与新的 Leader 服务器进行数据同步。
当集群中超过半数机器与该 Leader服务器完成数据同步之后,退出恢复模式进入消息广播模式, Leader 服务器开始接收客户端的事务请求生成事物提案来进行事务请求处理。
四种类型的数据节点 Znode
PERSISTENT-持久节点
除非手动删除,否则节点一直存在于 Zookeeper 上。
EPHEMERAL - 临时节点
临时节点的生命周期与客户端会话绑定,一旦客户端会话失效(客户端与zookeeper 连接断开不一定会话失效),那么这个客户端创建的所有临时节点 都会被移除。
PERSISTENT_SEQUENTIAL-持久顺序节点
基本特性同持久节点,只是增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
EPHEMERAL_SEQUENTIAL - 临时顺序节点
基本特性同临时节点,增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字。
持久:客户端和服务器断开连接后,创建的节点不删除
临时:客户端和服务器断开链接后,创建的节点自己删除;
持久化目录节点:客户端与zookeeper服务器断开连接后,该节点依旧存在
持久化顺序编号目录节点:客户端与zookeeper断开连接后,该节点依旧存在,只是zookeeper给该节点名称进行顺序编号
临时目录节点:客户端与zookeeper服务器断开连接后,该节点呗删除
临时顺序编号目录节点:客户端与zookeeper断开连接后,该节点被删除,只是zookeeper给该节点名称进行顺序编号
请简述zookeeper的选举机制(第一次启动)
假设有五台服务器组成的zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么:
- 服务器1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING;
- 服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1)大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING
- 服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
- 服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;
- 服务器5启动,同4一样当小弟。
注意,如果按照5,4,3,2,1的顺序启动,那么5将成为Leader,因为在满足半数条件后,ZooKeeper集群启动,5的Id最大,被选举为Leader。
服务器5和服务器3,3相互选举会投ID最大的那一台服务器,所以服务器5会选举为leader。
- SID:服务器ID,用来唯一标识一台ZooKeeper集群中的机器,每台机器不能重复,和myid一致。
- ZXID:事务ID,ZXID是一个事务ID,用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID值不一定完全一致,这和ZooKeeper服务器对于客户端“更新请求”的处理逻辑有关。
- Epoch:每个Leader任期的代号,没有Leader时同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加
请简述zookeeper的选举机制(非第一次启动)
- 当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举:
- 服务器初始化启动。
- 服务器运行期间无法和Leader保持连接。
- 而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态:
- 集群中本来就已经存在一个Leader。对于第一种已经存在Leader的情况,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立连接,并进行状态同步即可。
- 集群中确实不存在Leader。
ZooKeeper的监听原理是什么?
在应用程序中,main()方法首先会创建zkClient,创建zkClient的同时就会产生两个进程,即Listener进程(监听进程)和connect进程(网络连接/传输进程),当zkClient调用getChildren()等方法注册监视器时,connect进程向ZooKeeper注册监听器,注册后的监听器位于ZooKeeper的监听器列表中,监听器列表中记录了zkClient的IP,端口号以及要监控的路径,一旦目标文件发生变化,ZooKeeper就会把这条消息发送给对应的zkClient的Listener()进程,Listener进程接收到后,就会执行process()方法,在process()方法中针对发生的事件进行处理.
原理图
- connect进程负责去zookeeper服务器上面注册监视器。
- Listener进程负责通知客户端进程。
Watcher机制原理
ZooKeeper提供了分布式数据的发布/订阅功能,一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。在ZooKeeper中,引入了Watcher机制来实现这种分布式的通知功能。
ZooKeeper 允许客户端向服务端注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。
ZooKeeper的Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper服务器三部分。
在具体工作流程上,客户端在向ZooKeeper 服务器注册Watcher的同时,会将Watcher 对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行process方法的回调逻辑。
Watcher特性
- 一次性
无论是服务端还是客户端,一旦一个Watcher被触发,ZooKeeper都会将其从相应的存储中移除。因此,开发人员在Watcher的使用上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。试想,如果注册一个Watcher之后一直有效,那么,针对那些更新非常频繁的节点,服务端会不断地向客户端发送事件通知,这无论对于网络还是服务端性能的影响都非常大。
- 客户端串行执行
客户端 Watcher回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要开发人员注意的一点是,千万不要因为一个Watcher的处理逻辑影响了整个客户端的Watcher回调。
- 轻量
WatchedEvent是ZooKeeper整个Watcher通知机制的最小通知单元,这个数据结构中只包含三部分内容:通知状态、事件类型和节点路径。也就是说,Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。
例如针对NodeDataChanged事件,ZooKeeper的Watcher只会通知客户端指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据一—这也是ZooKeeper的Watcher机制的一个非常重要的特性。
另外,客户端向服务端注册 Watcher的时候,并不会把客户端真实的Watcher对象传递到服务端,仅仅只是在客户端请求中使用boolean类型属性进行了标记,同时服务端也仅仅只是保存了当前连接的ServerCnxn对象。
如此轻量的Watcher机制设计,在网络开销和服务端内存开销上都是非常廉价的。
Zookeeper分布式锁(⽂件系统、通知机制)
有了zookeeper的⼀致性⽂件系统,锁的问题变得容易。锁服务可以分为两类:
- ⼀个是保持独占
- 另⼀个是控制时序。
对于第⼀类,我们将zookeeper上的⼀个znode看作是⼀把锁,通过createznode的⽅式来实现。所有客⼾端都去创建/distribute_lock 节点,最终成功创建的那个客⼾端也即拥有了这把锁。⽤完删除掉⾃⼰创建的distribute_lock 节点就释放出锁。
对于第⼆类, /distribute_lock 已经预先存在,所有客⼾端在它下⾯创建临时顺序编号⽬录节点,和选master⼀样,编号最⼩的获得锁,⽤完删除,依次⽅便。
获取分布式锁的流程
在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。客⼾端调⽤createNode⽅法在locker下创建临时顺序节点,然后调⽤getChildren(“locker”)来获取locker下⾯的所有⼦节点,注意此时不⽤设置任何Watcher。
客⼾端获取到所有的⼦节点path之后,如果发现⾃⼰创建的节点在所有创建的⼦节点序号最⼩,那么就认为该客⼾端获取到了锁。如果发现⾃⼰创建的节点并⾮locker所有⼦节点中最⼩的,说明⾃⼰还没有获取到锁,此时客⼾端需要找到⽐⾃⼰⼩的那个节点,然后对其调⽤exist()⽅法,同时对其注册事件监听器。
之后,让这个被关注的节点删除,则客⼾端的Watcher会收到相应通知,此时再次判断⾃⼰创建的节点是否是locker⼦节点中序号最⼩的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到⽐⾃⼰⼩的⼀个节点并注册监听。当前这个过程中还需要许多的逻辑判断。
代码的实现主要是基于互斥锁,获取分布式锁的重点逻辑在于BaseDistributedLock,实现了基于Zookeeper实现分布式锁的细节。
zookeeper中的数据复制
Zookeeper作为⼀个集群提供⼀致的数据服务,⾃然,它要在所有机器间做数据复制。数据复制的好处:
- 容错:⼀个节点出错,不致于让整个系统停⽌⼯作,别的节点可以接管它的⼯作;
- 提⾼系统的扩展能⼒ :把负载分布到多个节点上,或者增加节点来提⾼系统的负载能⼒;
- 提⾼性能:让客⼾端本地访问就近的节点,提⾼⽤⼾访问速度。
- 从客⼾端读写访问的透明度来看,数据复制集群系统分下⾯两种:
- 写主(WriteMaster) :对数据的修改提交给指定的节点。读⽆此限制,可以读取任何⼀个节点。这种情况下客⼾端需要对读与写进⾏区别,俗称读写分离;
- 写任意(Write Any):对数据的修改可提交给任意的节点,跟读⼀样。这种情况下,客⼾端对集群节点的⻆⾊与变化透明。
- 对zookeeper来说,它采⽤的⽅式是写任意。通过增加机器,它的读吞吐能⼒和响应能⼒扩展性⾮常好,⽽写,随着机器的增多吞吐能⼒肯定下降(这也是它建⽴observer的原因),⽽响应能⼒则取决于具体实现⽅式,是延迟复制保持最终⼀致性,还是⽴即复制快速响应。
zookeeper中任意节点收到写请求,如果是follower节点,则会把写请求转发给leader,如果是leader节点就直接进行下一步。
zookeeper是如何保证事务的顺序⼀致性的?
zookeeper采⽤了递增的事务Id来标识,所有的proposal(提议)都在被提出的时候加上了zxid,zxid实际上是⼀个64位的数字,⾼32位是epoch(时期; 纪元; 世; 新时代)⽤来标识leader是否发⽣改变,如果有新的leader产⽣出来,epoch会⾃增,低32位⽤来递增计数。
当新产⽣proposal的时候,会依据数据库的两阶段过程,⾸先会向其他的server发出事务执⾏请求,如果超过半数的机器都能执⾏并且能够成功,那么就会开始执⾏。
Zookeeper 下 Server⼯作状态
每个Server在⼯作过程中有三种状态:
- LOOKING:当前Server不知道leader是谁,正在搜寻
- LEADING:当前Server即为选举出来的leader
- FOLLOWING:leader已经选举出来,当前Server与之同步
简述ZAB协议
Zab 协议包括两种基本的模式:消息广播、崩溃恢复
- 消息广播是正常模式的zab协议
- 崩溃恢复是leader出现问题时候使用的zab协议模式。
消息广播
ZAB协议的消息广播过程使用的是一个原子广播协议,类似于一个二阶段提交过程。针对客户端的事务请求, Leader服务器会为其生成对应的事务 Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交。
此处ZAB协议中涉及的二阶段提交过程则与其略有不同。在ZAB协议的二阶段提交过程中,移除了中断逻辑,所有的 Follower服务器要么正常反馈 Leader提出的事务 Proposal,要么就抛弃Leader服务器。同时,ZAB协议将二阶段提交中的中断逻辑移除意味着我们可以在过半的 Follower服务器已经反馈Ack之后就开始提交事务 Proposal了,而不需要等待集群中所有的 Follower服务器都反馈响应。
当然,在这种简化了的二阶段提交模型下,是无法处理 Leader服务器崩溃退出而带来的数据不一致问题的,因此在ZAB协议中添加了另一个模式,即釆用崩溃恢复模式来解决这个问题。另外,整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,因此能够很容易地保证消息广播过程中消息接收与发送的顺序性。
在整个消息广播过程中, Leader服务器会为每个事务请求生成对应的 Proposa来进行厂播,并且在广播事务 Proposal之前, Leader服务器会首先为这个事务 Proposa分配一个全局单调递增的唯一ID,我们称之为事务ⅠD(即ZXID)。由于ZAB协议需要保证每个消息严格的因果关系,因此必须将每一个事务 Proposal按照其ZXID的先后顺序来进行排序与处理。
具体的,在消息广播过程中, Leader服务器会为每一个 Follower服务器都各自分配单独的队列,然后将需要广播的事务 Proposal依次放入这些队列中去,并且根据FIFO策略进行消息发送。每一个 Follower服务器在接收到这个事务 Proposal之后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写入后反馈给 Leader服务器个Ack响应。当 Leader服务器接收到超过半数 Follower的Ack响应后,就会广播一个Commit消息给所有的 Follower服务器以通知其进行事务提交,同时 Leader自身也会完成对事务的提交,而每一个 Follower服务器在接收到 Commit消息后,也会完成对事务的提交。
崩溃恢复
上面我们主要讲解了ZAB协议中的消息广播过程。ZAB协议的这个基于原子广播协议的消息广播过程,在正常情况下运行非常良好,但是一旦 Leader服务器出现崩溃,或者说由于网络原因导致 Leader服务器失去了与过半 Follower的联系,那么就会进入崩溃恢复模式。
在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的 Leader服务器。因此,ZAB协议需要一个高效且可靠的 Leader选举算法,从而确保能够快速地选举出新的 Leader。同时, Leader选举算法不仅仅需要让 Leader自己知道其自身已经被选举为 Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产生的新的 Leader服务器。
基本特性
根据上面的内容,我们了解到,ZAB协议规定了如果一个事务 Proposal在一台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。接下来我们看看在崩溃恢复过程中,可能会出现的两个数据不一致性的隐患及针对这些情况ZAB协议所需要保证的特性。
ZAB协议需要确保那些已经在 Leader服务器上提交的事务最终被所有服务器都提交:
假设一个事务在 Leader服务器上被提交了,并且已经得到过半 Follower服务器的Ack反馈,但是在它将 Commit消息发送给所有 Follower机器之前, Leader服务器挂了:
图中的消息C2就是一个典型的例子:在集群正常运行过程中的某一个时刻,Serverl是 Leader服务器,其先后广播了消息Pl、P2、Cl、P3和C2,其中,当Leader服务器将消息C2(C2是 Commit Of Proposal2的缩写,即提交事务 Proposal2)发出后就立即崩溃退出了。针对这种情况,ZAB协议就需要确保事务 Proposal2最终能够在所有的服务器上都被提交成功,否则将出现不一致
ZAB协议需要确保丢弃那些只在 Leader服务器上被提出的事务(可以理解为预提交的事务),相反,如果在崩溃恢复过程中出现一个需要被丢弃的提案,那么在崩溃恢复结束后需要跳过该事务 Proposal。
假设初始的 Leader服务器 Serverl在提出了一个事务Proposal3之后就崩溃退出了,从而导致集群中的其他服务器都没有收到这个事务Proposal,于是,当 Serverl恢复过来再次加入到集群中的时候,ZAB协议需要确保丟弃 Proposal3这个事务。
结合上面提到的这两个崩溃恢复过程中需要处理的特殊情况,就决定了ZAB协议必须设计这样一个 Leader选举算法:能够确保提交已经被 Leader提交的事务 Proposal,同时丢弃已经被跳过的事务 Proposal。针对这个要求,如果让 Leader选举算法能够保证新选举出来的 Leader服务器拥有集群中所有机器最高编号(即ZXID最大)的事务 Proposal,那么就可以保证这个新选举出来的 Leader一定具有所有已经提交的提案。更为重要的是,如果让具有最高编号事务 Proposal的机器来成为 Leader,就可以省去 Leader服务器检查 Proposal的提交和丢弃工作的这一步操作了。
选举处新的leader之后,还需要在leader和follower之间进行数据的同步过程。
数据同步
完成 Leader选举之后,在正式开始工作(即接收客户端的事务请求,然后提出新的提案)之前, Leader服务器会首先确认事务日志中的所有 Proposal是否都已经被集群中过半的机器提交了,即是否完成数据同步。
下面我们就来看看ZAB协议的数据同步过程。所有正常运行的服务器,要么成为 Leader,要么成为 Follower并和 Leader保持同步。Leader服务器需要确保所有的 Follower服务器能够接收到毎一条事务 Proposal,并且能够正确地将所有已经提交了的事务 Proposal应用到内存数据库中去。具体的, Leader服务器会为每一个 Follower服务器都准备一个队列,并将那些没有被各 Follower服务器同步的事务以 Proposal消息的形式逐个发送给 Follower服务器,并在每一个 Proposal消息后面紧接着再发送一个 Commit消息,以表示该事务已经被提交。等到 Follower服务器将所有其尚未同步的事务 Proposa都从 Leader服务器上同步过来并成功应用到本地数据库中后, Leader服务器就会将该 Follower服务器加入到真正的可用 Follower列表中,并开始之后的其他流程。
如何丢弃一个事务
上面讲到的是正常情况下的数据同步逻辑,下面来看ZAB协议是如何处理那些需要被丢弃的事务 Proposa的。在ZAB协议的事务编号ZXID设计中,ZXID是一个64位的数字,其中低32位可以看作是一个简单的单调递增的计数器,针对客户端的每一个事务请求, Leader服务器在产生一个新的事务 Proposal的时候,都会对该计数器进行加1操作;而高32位则代表了 Leader周期 epoch的编号,毎当选举产生一个新的 Leader服务器,就会从这个 Leader服务器上取出其本地日志中最大事务 Proposa的ZXID,并从该ZXID中解析出对应的 epoch值,然后再对其进行加l操作,之后就会以此编号作为新的 epoch,并将低32位置0来开始生成新的ZXID。
ZAB协议中的这一通过 epoch编号来区分 Leader周期变化的策略,能够有效地避免不同的 Leader服务器错误地使用相同的ZXID编号提出不一样的事务 Proposal的异常情况,这对于识别在 Leader崩溃恢复前后生成的 Proposa非常有帮助,大大简化和提升了数据恢复流程。
基于这样的策略,当一个包含了上一个 Leader周期中尚未提交过的事务 Proposal的服务器启动时,其肯定无法成为 Leader,原因很简单,因为当前集群中一定包含一个Quorum集合,该集合中的机器一定包含了更高 epoch的事务 Proposal,因此这台机器的事务 Proposal肯定不是最高,也就无法成为 Leader了。当这台机器加入到集群中,以Follower角色连接上 Leader服务器之后, Leader服务器会根据自己服务器上最后被提交的 Proposal来和 Follower服务器的 Proposa进行比对,比对的结果当然是 Leader会要求 Follower进行一个回退操作—回退到一个确实已经被集群中过半机器提交的最新的事务 Proposal。
raft算法中,leader负责处理读写请求,在ZAB中,Leader可以处理读写请求,follower可以处理读请求。在 raft中Leader的选举是放射状的,在ZAB中Leader的选举是网状的。
Paxos算法和AZB协议的区别和联系
ZAB协议并不是 Paxos算法的一个典型实现,在讲解ZAB和 Paxos之间的区别之前,我们首先来看下两者的联系。
- 两者都存在一个类似于Leader进程的角色,由其负责协调多个 Follower进程的运行。
- Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提案进行提交。
- 在ZAB协议中,每个 Proposal中都包含了一个 epoch值,用来代表当前的 Leader周期,在 Paxos算法中,同样存在这样的一个标识,只是名字变成了 Ballot,
在 Paxos算法中,一个新选举产生的主进程会进行两个阶段的工作。第一阶段被称为读阶段,在这个阶段中,这个新的主进程会通过和所有其他进程进行通信的方式来收集上个主进程提出的提案,并将它们提交。第二阶段被称为写阶段,在这个阶段,当前主进程开始提出它自己的提案。在 Paxos算法设计的基础上,ZAB协议额外添加了一个同步阶段。在同步阶段之前,ZAB协议也存在一个和 Paxos算法中的读阶段非常类似的过程,称为发现( Discovery)阶段。在同步阶段中,新的 Leader会确保存在过半的 Follower已经提交了之前 Leader周期中的所有事务 Proposal这一同步阶段的引入,能够有效地保证 Leader在新的周期中提出事务 Proposa之前,所有的进程都已经完成了对之前所有事务 Proposal的提交。一旦完成同步阶段后,那么ZAB就会执行和 Paxos算法类似的写阶段。
总的来讲,ZAB协议和 Paxos算法的本质区别在于,两者的设计目标不太一样。ZAB协议主要用于构建一个高可用的分布式数据主备系统,例如 ZooKeeper,而 Paxos算法则是用于构建一个分布式的一致性状态机系统。
ZAB 协议和我们之前看的 Raft 协议实际上是有相似之处的,比如都有一个 Leader,用来保证一致性(Paxos 并没有使用 Leader 机制保证一致性)。再有采取过半即成功的机制保证服务可用(实际上 Paxos 和 Raft 都是这么做的)。
ZAB 让整个 Zookeeper 集群在两个模式之间转换,消息广播和崩溃恢复,消息广播可以说是一个简化版本的 2PC,通过崩溃恢复解决了 2PC 的单点问题,通过队列解决了 2PC 的同步阻塞问题。
而支持崩溃恢复后数据准确性的就是数据同步了,数据同步基于事务的 ZXID 的唯一性来保证。通过 + 1 操作可以辨别事务的先后顺序。
zookeeper是如何选取主leader的?
当leader崩溃或者leader失去⼤多数的follower,这时zk进⼊恢复模式,恢复模式需要重新选举出⼀个新的leader,让所有的Server都恢复到⼀个正确的状态。Zk的选举算法有两种:
⼀种是基于basic paxos实现的,
另外⼀种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。
Zookeeper选主流程(basic paxos)
- 选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进⾏统计,并选出推荐的Server;
- 选举线程⾸先向所有Server发起⼀次询问(包括⾃⼰);
- 选举线程收到回复后,验证是否是⾃⼰发起的询问(验证zxid是否⼀致),然后获取对⽅的id(myid),并存储到当前询问对象列表中,最后获取对⽅提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;
- 收到所有Server回复以后,就计算出zxid最⼤的那个Server,并将这个Server相关信息设置成下⼀次要投票的Server;
- 线程将当前zxid最⼤的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数,设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置⾃⼰的状态,否则,继续这个过程,直到leader被选举出来。通过流程分析我们可以得出:要使Leader获得多数Server的⽀持,则Server总数必须是奇数2n+1,且存活的Server的数⽬不得少于n+1. 每个Server启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘
快照中恢复数据和会话信息,zk会记录事务⽇志并定期进⾏快照,⽅便在恢复时进⾏状态恢复。
Zookeeper选主流程(basic paxos)
fast paxos流程是在选举过程中,某Server⾸先向所有Server提议⾃⼰要成为leader,当其它Server收到提议以后,解决epoch和 zxid的冲突,并接受对⽅的提议,然后向对⽅发送接受提议完成的消息,重复这个流程,最后⼀定能选举出Leader。
Zookeeper同步流程
选完Leader以后,zk就进⼊状态同步过程。
- Leader等待server连接;
- Follower连接leader,将最⼤的zxid发送给leader;
- Leader根据follower的zxid确定同步点;
- 完成同步后通知follower 已经成为uptodate状态;
- Follower收到uptodate消息后,⼜可以重新接受client的请求进⾏服务了。
zk节点宕机如何处理?
Zookeeper本⾝也是集群,推荐配置不少于3个服务器。Zookeeper⾃⾝也要保证当⼀个节点宕机时,其他节点会继续提供服务。
如果是⼀个Follower宕机,还有2台服务器提供访问,因为Zookeeper上的数据是有多个副本的,数据并不会丢失;
如果是⼀个Leader宕机,Zookeeper会选举出新的Leader。
【ZK集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在ZK节点挂得太多,只剩⼀半或不到⼀半节点能⼯作,集群才失效。所以:
- 3个节点的cluster可以挂掉1个节点(leader可以得到2票>1.5)
- 2个节点的cluster就不能挂掉任何1个节点了(leader可以得到1票<=1)
zookeeper watch机制
Watch机制官⽅声明:⼀个Watch事件是⼀个⼀次性的触发器,当被设置了Watch的数据发⽣了改变的时候,则服务器将这个改变发送给设置了Watch的客⼾端,以便通知它们。
Zookeeper机制的特点:
- ⼀次性触发数据发⽣改变时,⼀个watcher event会被发送到client,但是client只会收到⼀次这样的信息。
- watcher event异步发送watcher的通知事件从server发送到client是异步的,这就存在⼀个问题,不同的客⼾端和服务器之间通过socket进⾏通信,由于⽹络延迟或其他因素导致客⼾端在不通的时刻监听到事件,由于Zookeeper本⾝提供了ordering guarantee,即客⼾端监听事件后,才会感知它所监视znode发⽣了变化。所以我们使⽤Zookeeper不能期望能够监控到节点每次的变化。Zookeeper只能保证最终的⼀致性,⽽⽆法保证强⼀致性。
- 数据监视Zookeeper有数据监视和⼦数据监视getdata() and exists()设置数据监视,getchildren()设置了⼦节点监视。
- 注册watcher getData、exists、getChildren
- 触发watcher create、delete、setData
- setData()会触发znode上设置的data watch(如果set成功的话)。⼀个成功的create() 操作会触发被创建的znode上的数据watch,以及其⽗节点上的child watch。⽽⼀个成功的delete()操作将会同时触发⼀个znode的data watch和childwatch(因为这样就没有⼦节点了),同时也会触发其⽗节点的child watch。
- 当⼀个客⼾端连接到⼀个新的服务器上时,watch将会被以任意会话事件触发。当与⼀个服务器失去连接的时候,是⽆法接收到watch的。⽽当client重新连接时,如果需要的话,所有先前注册过的watch,都会被重新注册。通常这是完全透明的。只有在⼀个特殊情况下,watch可能会丢失:对于⼀个未创建的znode的exist watch,如果在客⼾端断开连接期间被创建了,并且随后在客⼾端连接上之前⼜删除了,这种情况下,这个watch事件可能会被丢失。
- Watch是轻量级的,其实就是本地JVM的Callback,服务器端只是存了是否有设置了Watcher的布尔类型
zk的初始化选举和崩溃选举过程
zxid:事务的ID,leader会将客户端的命令封装为一个proposal对象,这个zxid也会封装在这个对象中,zxid是全局唯一的,表示客户端的一次请求以及请求内容。zxid有两部分内容:
- 第一部分是当前leader的任期
- 另外一部分是一个递增的序列,表示一个事务的id,这两部分决定了zxid全局唯一。
sid:节点的id,是我们人为指定的,在整个集群中,id也是惟一的。
初始化选举
先对比zxid,在对比sid,先投票给自己,投票内容为(zxid,sid),zxid是最后一条事务的id。投票过程中,先广播自己的票,获取到广播票的节点,把广播票和自己的票对比,首先对比zxid,谁的zxid大,谁就获胜,如果zxid相同,那么就对比sid,谁的sid大,谁就获胜。如果哪一方输掉,那一方就更改自己的选票,把zxid改为获胜一方的zxid,sid改为获胜一方的sid。
投票箱:每个节点在本地维护自己和其他节点的投票信息,改投票时候需要更新信息,并且广播出去。
节点状态:
- looking:竞选状态
- following:随从状态,同步leader状态,参与投票。
- observing:观察状态,同步leader状态,不参与投票。
- leading:领导者状态。
初始化:没有历史数据,5个节点为例
- 节点1启动,此时只有一台服务器启动,他发出去的请求没有任何响应,所以他的选举状态一直是looking状态。
- 节点2启动,他与节点1进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以serverid(sid)值较大的服务器2胜出,但是由于没有达到半数以上,所以服务器1,2还是继续保持looking状态。
- 节点3启动,与1,2节点通信交换数据,服务器3成为服务器 1,2,3中的leader,此时一共有三台服务器选举了3,所以3成为leader.
- 节点4启动,理论上服务器4应该是服务器1,2,3,4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以服务器4只能切换为follower。
- 节点5启动,与节点4一样。
崩溃选举:
- 变更状态,leader故障之后,follower进入looking状态
- 各个节点开始投票,首先投自己(zxid,sid),再广播投票
- 接收到投票,对比zxid和sid,如果本节点小,则将投票该我接受的投票信息,并记录投票信息,重新广播,否则本节点大,则可以不做处理。
- 统计本地投票信息,超过半数,则切换为leading状态并且广播。
简述zk的数据模型
可以把zk理解为一个文件系统,zk中每一个节点都是一个znode。
zk中的数据模型是一种树型结构,具有一个固定的根节点(/),可以在根节点下面创建子节点,并且在子节点下继续创建下一级节点,每一个层级使用/隔开,且只能使用绝对路径的方式查找zk的节点,而不可以使用相对路径。
zk中的节点分类:
- 持久类型:将节点创建为持久类型的节点,该数据节点会一直存储在zk的服务器上面,即使创建该节点的客户端与服务器的会话关闭了,该节点依然不会被删除,除非显示的调用delete函数进行删除操作。
- 临时节点:如果将节点创建为临时节点,那么该节点的数据不会一直存储在zk服务器上面,当创建该临时节点的客户端会话因为超市或发生异常关闭时,该节点也在相应的zk服务器上面被删除,也可以主动调用delete删除。
- 有序节点:有序节点并不算是一种单独的节点类型,而是在持久节点和临时节点的基础之上,增加一个节点有序的性质,创建有序节点的时候,zk服务器会自动的使用一个单调递增的数字作为后缀,追加到创建节点的后面,例如一个客户端创建了一个路径为/work、task的有序节点,那么zk服务器将会生成一个序号并且追加到该节点的路径后面,最后该节点的路径为/works/task-1。
节点的内容:一个二进制数组(byte data[]),用来存储节点的数据,ACL访问控制(文件访问权限),子节点数据(因为临时节点不允许有子节点,所以其子节点字段为null),记录自身状态信息的stat。
stat+节点路径可以查看状态信息
czxid:创建节点的事务id。
mzxid:最后一次被更新的事务id
pzxid:子节点最后一次被修改的事务id。
ctime:创建时间
mtime:最后更新时间
version:版本号,表示的是对节点的数据内容,子节点信息或者acl信息修改的次数,可以避免并发问题,使用之前获取的版本进行cas操作更新。
cversion:子节点版本号
aversion:acl版本号
ephemeralOwner:创建节点的sessionid,如果是持久节点,值为0。
dataLength:数据内容长度。
numChildren:子节点个数
zk的数据同步原理
根据这三个参数的大小对比结果,选择对应的数据同步方式。
- peerLastZxid:Learner服务器(Follower或Observer)最后处理的zxid。
- minCommittedLog:Leader服务器proposal缓存队列committedLog中最小的zxid。
- maxCommittedLog:leader服务器proposal缓存队列committedLog中最大的zxid。
zookeeper中数据同步一共四类,如下:
- DIFF:直接差异化同步:peerlastZxid介于minCommittedLog和maxCommittedLog之间。
- TRUNC+DIFF:先回滚然后在差异化同步:当leader服务器发现某一个learner包含了一条自己没有的事务记录,那么就需要让该learner进行事务回滚到Leader服务器上存在的记录,然后在进行同步,回滚到最接近peerLastZxid的地方。
- TRUNC:仅回滚同步,peerLastZxid大于maxCommittedLog,leader会要求learner回滚到zxid的值为maxCommittedLog对应的事务操作。
- SNAP:全量同步,peerLastZxid小于minCommittedLog。
在初始化阶段,leader服务器会优先初始化以全量同步方式来同步数据
learner先向leader注册,上报peerlastZxid
zk的应用场景
通过对zk中丰富的数据节点进行交叉使用,配合Watcher事件通知机制,可以非常方便的构建一系列分布式应用会涉及到的核心功能:
- 数据发布/订阅:配置中心
- 负载均衡,提供服务者列表
- 命名服务,提供服务名到服务地址的映射
- 分布式协调通知:watch机制和临时节点,获取各个节点的任务进度,通过修改节点发出通知。
- 集群管理:是否有机器退出或者加入,选举master。
- 分布式锁。
- 分布式队列。
第一类:在约定目录下面创建临时目录节点,监听节点数目是否是要求的数目
第二类:和分布式锁服务中的控制时序场景基本原理一致,入列有编号,在特定的目录下面创建PERSISTENT_SEQUENTIAL节点,创建成功时watch通知等待的队列,队列删除序号最小的节点泳衣消费,此场景下zookeeper的znode用于消息存储,znode存储的数据就是消息队列中的消息内容,SEQUENTIAL序列号就是消息的编号,按序取出即可,由于创建的节点是持久化的,所以不必担心队列消息的丢失问题。
zk中一个客户端修改了某一个节点的数据,其他客户端能够马上获取到这个最新数据么
是可以读取到的,但是需要额外操作,正常情况下,leader更新数据之后,如果还没有来得及同步follower节点,那么客户端去follower节点读取的数据是历史的数据,所以如果想读取最新的数据,需要我们执行sync操作,同步数据之后,就可以读取到最新的数据。
简述zk中的观察者机制
在zk中如果想把一个节点设置为observe节点的话,可以添加下面两个参数
peerType=observe
server.1:localhost:2181:3181:observe
observer节点和follower节点一样,同样可以处理读请求,还可以接受写请求,只不过会把写请求重新定向类leader节点。
观察者的设计是希望能够动态的扩展zookeeper集群又不会降低它的写性能。
如果扩展的节点是follower节点,则写入操作提交时候需要同步的节点数会增多,导致写入性能下降,而follower又是参与投票的,也会导致投票成本增加。为了解决这两个问题,引入observer节点。
observer是一种新的节点类型,解决扩展问题时的同时,只是获取投票结果,并不参与投票,同时也可以处理读写请求,写请求转发给leader,负责接受leader同步过来的提交数据,observer的节点故障也不会影响集群的可用性。observer同步的数据全部是commit的数据。zk容忍节点故障的个数是一半。超过半数就无法投票。
跨数据中心部署,把节点分散到多个数据中心可能因为网络的延迟会极大的拖慢系统,使用observer的话,更新操作都在一个单独的数据中心来处理,并发送到其他的数据中心,让其他数据中心的节点消费数据。
无法完成消除数据中心之间的网络延迟,因为observer需要把更新请求转发到另一个数据中心的leader,并处理同步消息,网络的速度极慢的话也会有影响,他的优势是为本地读请求提供快速响应。
不参与投票,宕机后并不影响可用性
ZAB协议详细流程
消息广播(正常模式)
ZAB协议针对事务请求的处理过程类似于一个两阶段提交过程
- 广播事务阶段
- 广播提交操作
- 客户端发起一个写操作请求。
- Leader服务器将客户端的请求转化为事务Proposal 提案,同时为每个Proposal 分配一个全局的ID,即zxid(事务id)。
- Leader服务器为每个Follower服务器分配一个单独的队列,然后将需要广播的 Proposal依次放到队列中去,并且根据FIFO策略进行消息发送。
- Follower接收到Proposal后,会首先将其以事务日志的方式写入本地磁盘中,写入成功后向Leader反馈一个Ack响应消息。
- Leader接收到超过半数以上Follower的Ack响应消息后,即认为消息发送成功,可以发送commit消息。
- Leader向所有Follower广播commit消息,同时自身也会完成事务提交。Follower 接收到commit消息后,会将上一条事务提交。
- Zookeeper采用Zab协议的核心,就是只要有一台服务器提交了Proposal,就要确保所有的服务器最终都能正确提交Proposal。
zab协议也可能出现问题:
这两阶段提交模型如下,有可能因为Leader宕机带来数据不一致,比如
- Leader 发 起 一 个 事 务Proposal1 后 就 宕 机 , Follower都没有Proposal1,预提交阶段宕机;
- Leader收到半数ACK宕机,没来得及向Follower发送Commit,Leader提交但是follower没有提交;
怎么解决呢?ZAB引入了崩溃恢复模式。
崩溃恢复模式
一旦Leader服务器出现崩溃或者由于网络原因导致Leader服务器失去了与过半 Follower的联系,那么就会进入崩溃恢复模式。
假设两种服务器异常情况:
- 假设一个事务在Leader提出之后,Leader挂了。也就是上面说的第一种情况,预提交。
- 一个事务在Leader上提交了,并且过半的Follower都响应Ack了,但是Leader在Commit消息发出之前挂了。上面说的第二种情况,Leader提交但是follower没有提交。
Zab协议崩溃恢复要求满足以下两个要求:
- 确保已经被Leader提交的提案Proposal,必须最终被所有的Follower服务器提交。 (已经产生的提案,Follower必须执行)
- 确保丢弃已经被Leader提出的,但是没有被提交的Proposal。(丢弃胎死腹中的提案)
崩溃恢复主要包括两部分:Leader选举和数据恢复。
leader选举
Leader选举:根据上述要求,Zab协议需要保证选举出来的Leader需要满足以下条件:
- 新选举出来的Leader不能包含未提交的Proposal。即新Leader必须都是已经提交了Proposal的Follower服务器节点。
- 新选举的Leader节点中含有最大的zxid。这样做的好处是可以避免Leader服务器检查Proposal的提交和丢弃工作。
数据恢复
Zab如何数据同步:
- 完成Leader选举后,在正式开始工作之前(接收事务请求,然后提出新的Proposal),Leader服务器会首先确认事务日志中的所有的Proposal 是否已经被集群中过半的服务器Commit。
- Leader服务器需要确保所有的Follower服务器能够接收到每一条事务的Proposal,并且能将所有已经提交的事务Proposal应用到内存数据中。等到Follower将所有尚未同步的事务Proposal都从Leader服务器上同步过,并且应用到内存数据中以后,Leader才会把该Follower加入到真正可用的Follower列表中
崩溃恢复:
- 初始化集群,刚刚启动的时候。
- Leader崩溃,因为故障宕机。
- Leader失去了半数的机器支持,与集群中超过一半的机器断联。
此时开启新一轮的Leader选举,选举产生的Leader会与过半的Follower进行同步,使数据一致,当参与过半的机器同步完成后,就退出恢复模式,然后进入消息广播模式。退出恢复模式的前提就是Leader与集群中半数以上的Follower达成一致。
整个zookeeper集群的一致性保证就是在上面两个状态之间进行切换,当Leader服务正常的时候,就是正常的消息广播模式,当Leader不可用的时候,则进入崩溃恢复模式,崩溃恢复阶段会进行数据的同步,完成之后,重新进入消息广播阶段。
zxid是ZAB协议的一个事务编号,zxid是一个64位的数字,其中低32位是一个简单的单调递增计数器,针对客户端的每一个事务请求,计数器累加1,而高32位则代表Leader周期年代的编号。两部分保证这个数字全局唯一。
Leader周期(epoch),可以理解为当前集群所处的年代或者周期,每当有一个新的Leader选举出现时候,就会从这个Leader服务器上取出其本地日志中最大事务的zxid,并且从中读取epoch值,然后累加1,以此作为新的周期ID,高32位代表每一代Leader的唯一性,低32位代表了每一代Leader中事务的唯一性。
zookeeper集群中的每一个节点,都处于一下三种状态之一:
- following:服从Leader的命令
- leadering:负责协调事务
- electionlocking:选举状态
ZooKeeper保证的是CP
- ZooKeeper不能保证每次服务请求的可用性。(注:在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果)。所以说,ZooKeeper不能保证服务可用性,但是可以保证数据的一致性;
- 进行Leader选举时集群都是不可用,因为此时没有leader,无法提供服务;