ZooKeeper学习
ZooKeeper核心
本文主要是阅读其他文章后的理解和删减,十分简明扼要介绍了zk的原理
为了应对大流量,现代应用/中间件通常采用分布式部署,此时不得不考虑CAP问题。ZooKeeper(后文简称ZK)是面向CP设计的一个开源的分布式协调框架,将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用,分布式应用程序可以基于它实现诸如 数据发布/订阅、负载均衡、命名服务、集群管理、Master 选举、分布式锁、分布式队列 等功能。ZK之所以能够提供上述一套分布式数据一致性解决方案,核心在于其设计精妙的数据结构、watcher机制、Zab一致性协议等,下面将依次剖析。
数据结构
ZK在内存中维护了一个类似文件系统的树状数据结构实现命名空间(如下),树中的节点称为znode。
然而,znode要比文件系统的路径复杂,既可以通过路径访问,又可以存储数据。znode具有四个属性data、acl、stat、children,如下
1 | public class DataNode implements Record { |
data: znode相关的业务数据均存储在这里,但是,父节点不可存储数据;
children: 存储当前节点的子节点引用信息,因为内存限制,所以 znode 的子节点数不是无限的;
stat: 包含znode节点的状态信息,比如: 事务id、版本号、时间戳等,其中事务id和ZK的数据一直性、选主相关,下面将重点介绍;
acl: 记录客户端对znode节点的访问权限;
注意: znode的数据操作具有原子性,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。znode可存储的最大数据量是1MB ,但实际上我们在znode的数据量应该尽可能小,因为数据过大会导致zk的性能明显下降。每个ZNode都对应一个唯一的路径。
事物ID:Zxid
Zxid由Leader节点生成。当有新写入事件时,Leader节点生成新的Zxid,并随提案一起广播。Zxid的生成规则如下
epoch:任期/纪元,Zxid的高32位, ZAB协议通过epoch编号来区分Leader 周期变化,每次一个 leader 被选出来,它都会有一个新的 epoch=(原来的epoch+1),标识当前属于那个leader的 统治时期;可以假设leader 就像皇帝,epoch则相当于年号,每个皇帝都有自己的年号;
事务计数器:Zxid的低32位,每次数据变更,计数器都会加一;
zxid是递增的,所以谁的zxid越大,就表示谁的数据是最新的。每个节点都保存了当前最近一次事务的Zxid。Zxid对于ZK的数据一致性以及选主都有着重要意义,后边在介绍相关知识时会重点讲解其作用原理。
znode类型
节点根据生命周期的不同可以将划分为持久节点和临时节点。持久节点的存活时间不依赖于客户端会话,只有客户端在显式执行删除节点操作时,节点才消失;临时节点的存活时间依赖于客户端会话,当会话结束,临时节点将会被自动删除(当然也可以手动删除临时节点)。注意:临时节点不能拥有子节点。
节点类型是在创建时进行制定,后续不能改变。如 create /n1 node1创建了一个数据为”node1”的持久节点/n1;在上述指令基础上加上参数-e: create -e /n1/n3 node3,则创建了一个数据为”node3”的临时节点 /n1/n3。
create命令还有一个可选参数-s 用于指定创建的节点是否具有顺序特性。创建顺序节点时,zk会在路径后面自动追加一个 递增的序列号 ,这个序列号可以保证在同一个父节点下是唯一的,利用该特性我们可以实现分布式锁 等功能。
基于znode的上述两组特性,两两组合后可构建4种类型的节点:
PERSISTENT:永久节点
EPHEMERAL:临时节点
PERSISTENT_SEQUENTIAL:永久顺序节点
EPHEMERAL_SEQUENTIAL:临时顺序节点
Watcher监听机制
Watcher监听机制是ZK非常重要的一个特性。ZK允许Client端在指定节点上注册Watcher,监听节点数据变更、节点删除、子节点状态变更等事件,当特定事件发生时,ZK服务端会异步通知注册了相应Watcher的客户端,通过该机制,我们可以利用ZK实现数据的发布和订阅等功能。
Watcher监听机制由三部分协作完成:ZK服务端、ZK客户端、客户端的WatchManager对象。工作时,客户端首先将Watcher 注册到服务端,同时将Watcher对象保存到客户端的Watch管理器中。当ZK服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的Watch管理器会触发相关Watcher来回调相应处理逻辑。

注意:
watcher变更通知是一次性的:当数据发生变化的时候, ZK会产生一个watcher事件,并且会发送到客户端。但是客户端只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置 Watcher的客户端不会再次收到消息。可以通过循环监听去达到永久监听效果。
客户端watcher顺序回调:watcher回调是顺序串行化执行的,只有回调后客户端才能看到节点最新的状态。watcher回调逻辑不应太复杂,否则可能影响watcher执行。
不会告诉节点变化前后的具体内容:watchEvent是最小的通信单元,结构上包含通知状态、事件类型和节点路径,但是,不会告诉节点变化前后的具体内容。
时效性:watcher只有在当前session彻底失效时才会无效,若在session有效期内快速重连成功,则watcher依然存在,仍可收到事件通知。
ZK集群
和raft大同小异,可以跳过不看
为了确保服务的高可用性,ZK采用集群化部署,如下

ZK集群服务器有三种角色:Leader、Follower和Observer
Leader:一个 ZK 集群同一时间只会有一个实际工作的 Leader,它会发起并维护与各 Follwer 及 Observer 间的心跳。所有的写操作必须要通过 Leader 完成再由 Leader 将写操作广播给其它服务器。
Follower: 一个 ZK 集群可同时存在多个 Follower,它会响应 Leader 的心跳。Follower 可直接处理并返回客户端的读请求,同时会将写请求转发给 Leader 处理,参与事务请求Proposal的投票及Leader选举投票。
Observer:Observer是3.3.0 版本开始引入的一个服务器角色,一个 ZK 集群可同时存在多个 Observer, 功能与 Follower 类似,但是,不参与投票。
“早期的 ZooKeeper 集群服务运行过程中,只有Leader服务器和Follow服务器。随着集群规模扩大,follower变多,ZK在创建节点和选主等事务性请求时,需要一半以上节点AC,所以导致性能下降写入操作越来越耗时,follower之间通信越来越耗时。为了解决这个问题,就引入了观察者,可以处理读,但是不参与投票。既保证了集群的扩展性,又避免过多服务器参与投票导致的集群处理请求能力下降。”
ZK集群中通常有很多服务器,那么如何区分不同的服务器的角色呢?可以通过服务器的状态进行区分
LOOKING:寻找Leader状态。当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。
LEADING:领导者状态。表明当前服务器角色是Leader。
FOLLOWING:跟随者状态,同步 leader 状态,参与投票。表明当前服务器角色是Follower。
OBSERVING:观察者状态,同步 leader 状态,不参与投票。表明当前服务器角色是Observer。
ZK集群是一主多从的结构,所有的所有的写操作必须要通过 Leader 完成,Follower 可直接处理并返回客户端的读请求。那么如何保证从Follower服务器读取的数据与Leader写入的数据的一致性呢?Leader万一由于某些原因崩溃了,如何选出新的Leader,如何保证数据恢复? Leader 是怎么选出来的?
Zab一致性协议
ZK专门设计了ZAB协议(Zookeeper Atomic Broadcast)来保证主从节点数据的一致性。下面分别从client向Leader和Follower写数据场景展开陈述。
写 Leader场景数据一致性

客户端向 Leader 发起写请求
Leader 将写请求以 Proposal 的形式发给所有 Follower 并等待 ACK
Follower 收到 Leader 的 Proposal 后返回 ACK
Leader 得到过半数的 ACK(Leader 对自己默认有一个 ACK)后向所有的 Follower 和 Observer 发送 Commmit
Leader 将处理结果返回给客户端
注意:
Leader不需要得到所有Follower的ACK,只要收到过半的ACK即可,同时Leader本身对自己有一个ACK。上图中有2个Follower,只需其中两个返回ACK即可,因为(1+1) / (2+1) > 1/2
Observer虽然无投票权,但仍须同步Leader的数据从而在处理读请求时可以返回尽可能新的数据
最终一致性
Zab 协议消息广播使用两阶段提交的方式,达到主从数据的最终一致性。为什么是最终一致性呢?从上文可知数据写入过程核心分成下面两阶段:
第一阶段:Leader数据写入事件作为提案广播给所有 Follower 结点;可以写入的Follower结点返回确认信息 ACK。
第二阶段:Leader 收到一半以上的 ACK 信息后确认写入可以生效,向所有结点广播 COMMIT 将提案生效。
根据写入过程的两阶段的描述,可以知道 ZooKeeper 保证的是最终一致性,即 Leader 向客户端返回写入成功后,可能有部分 Follower 还没有写入最新的数据,所以是最终一致性。ZooKeeper 保证的最终一致性也叫顺序一致性,即每个结点的数据都是严格按事务的发起顺序生效的。ZooKeeper 集群的写入是由 Leader 结点协调的,真实场景下写入会有一定的并发量,那 Zab 协议的两阶段提交是如何保证事务严格按顺序生效的呢?ZK事物的顺序性是借助上文中的Zxid实现的。Leader 在收到半数以上 ACK 后会将提案生效并广播给所有 Follower 结点,Leader 为了保证提案按 ZXID 顺序生效,使用了一个 ConcurrentHashMap,记录所有未提交的提案,命名为 outstandingProposals,key 为 ZXID,Value 为提案的信息。对 outstandingProposals 的访问逻辑如下:
Leader 每发起一个提案,会将提案的 ZXID 和内容放到 outstandingProposals 中,作为待提交的提案;
Leader收到 Follower 的 ACK 信息后,根据 ACK 中的 ZXID 从 outstandingProposals 中找到对应的提案,对 ACK 计数;
执行 tryToCommit 尝试将提案提交:判断流程是,先判断当前 ZXID 之前是否还有未提交提案,如果有,当前提案暂时不能提交;再判断提案是否收到半数以上 ACK,如果达到半数则可以提交;如果可以提交,将当前 ZXID 从 outstandingProposals 中清除并向 Followers 广播提交当前提案;
Leader 是如何判断当前 ZXID 之前是否还有未提交提案的呢?由于前提是保证顺序提交的,所以 Leader 只需判断 outstandingProposals 里,当前 ZXID 的前一个 ZXID 是否存在。
所以 ZooKeeper 是通过两阶段提交保证数据的最终一致性,并且通过严格按照 ZXID 的顺序生效提案保证其顺序一致性的。
选主原理
和raft基本一模一样
典型应用场景
数据发布/订阅
我们可基于ZK的Watcher监听机制实现数据的发布与订阅功能。ZK的发布订阅模式采用的是推拉结合的方式实现的,实现原理如下:

当集群中的服务启动时,客户端向ZK注册watcher监听特定节点,并从节点拉取数据获取配置信息;
当发布者变更配置时,节点数据发生变化,ZK会发送watcher事件给各个客户端;客户端在接收到watcher事件后,会从该节点重新拉取数据获取最新配置信息。
注意: Watch 具有一次性,所以当获得服务器通知后要再次添加 Watch 事件。
disconf便是这么做的,不过节点里不是存配置数据,而是存的mysql用的元数据,原理是一样的
负载均衡
利用ZK的临时节点、watcher机制等特性可实现负载均衡,具体思路如下:

把ZK作为一个服务的注册中心,基本流程:
服务提供者server启动时在ZK进行服务注册(创建临时文件);
服务消费者client启动时,请求ZK获取最新的服务存活列表并注册watcher,然后将获得服务列表保存到本地缓存中;
client请求server时,根据自己的负载均衡算法,从服务器列表选取一个进行通信。
若在运行过程中,服务提供者出现异常或人工关闭不能提供服务,临时节点失效,ZK探测到变化更新本地服务列表并异步通知到服务消费者,服务消费者监听到服务列表的变化,更新本地缓存
注意:服务发现可能存在延迟,因为服务提供者挂掉到缓存更新大约需要3-5s的时间(根据网络环境不同还需仔细测试)。为了保证服务的实时可用,client请求server发生异常时,需要根据服务消费报错信息,进行重负载均衡重试等。
命名服务
命名服务是指通过指定的名字来获取资源或者服务的地址、提供者等信息。以znode的路径为名字,znode存储的数据为值,可以很容易构建出一个命名服务。例如Dubbo使用ZK来作为其命名服务,如下

- 所有 Dubbo 相关的数据都组织在
/dubbo的根节点下;
二级目录是服务名,如 com.foo.BarService ;
三级目录有两个子节点,分别是 providers 和 consumers ,表示该服务的提供者和消费者;
四级目录记录了与该服务相关的每一个应用实例的 URL 信息,在 providers 下的表示该服务的所有提供者,而在 consumers 下的表示该服务的所有消费者。举例说明, com.foo.BarService 的服务提供者在启动时将自己的 URL 信息注册到 /dubbo/com.foo.BarService/providers 下;同样的,服务消费者将自己的信息注册到相应的 consumers 下,同时,服务消费者会订阅其所对应的 providers 节点,以便能够感知到服务提供方地址列表的变化。
集群管理
基于ZK的临时节点和watcher监听机制可实现集群管理。集群管理通常指监控集群中各个主机的运行时状态、存活状况等信息。如下图所示,主机向ZK注册临时节点,监控系统注册监听集群下的临时节点,从而获取集群中服务的状态等信息。

Master 选举
ZK中某节点同一层子节点,名称具有唯一性,所以,多个客户端创建同一节点时,只会有一个客户端成功。利用该特性,可以实现maser选举,具体如下:

多个客户端同时竞争创建同一临时节点/master-election/master,最终只能有一个客户端成功。这个成功的客户端成为Master,其它客户端置为Slave。
Slave客户端都向这个临时节点的父节点/master-election 注册一个子节点列表的watcher 监听。
一旦原Master宕机,临时节点就会消失,zk 服务器就会向所有 Slave 发送子节点变更事件,Slave 在接收到事件后会竞争创建新的master临时子节点。谁创建成功,谁就是新的 Master。
分布式锁
基于ZK的临时顺序节点和Watcher 机制可实现公平分布式锁。下面具体看下多客户端获取及释放zk分布式锁的整个流程及背后的原理。
假如说客户端A先发起请求,就会搞出来一个顺序节点,因为客户端A是第一个发起请求的,所以给他搞出来的顺序节点的序号是”1”。接着客户端A会查一下”my_lock”这个锁节点下的所有子节点,并且这些子节点是按照序号排序的,这个时候大概会拿到一个集合.接着客户端A会走一个关键性的判断:唉!兄弟,这个集合里,我创建的那个顺序节点,是不是排在第一个啊?如果是的话,那我就可以加锁了啊!因为明明我就是第一个来创建顺序节点的人,所以我就是第一个尝试加分布式锁的人啊!bingo!加锁成功!大家看下面的图,再来直观的感受一下整个过程。
假如说客户端A加完锁完后,客户端B过来想要加锁,这个时候它会干一样的事儿:先是在”my_lock”这个锁节点下创建一个临时顺序节点,因为是第二个来创建顺序节点的,所以zk内部会维护序号为”2”。接着客户端B会走加锁判断逻辑,查询”my_lock”锁节点下的所有子节点,按序号顺序排列,此时看到的类似于:

同时检查自己创建的顺序节点,是不是集合中的第一个?明显不是,此时第一个是客户端A创建的那个顺序节点,序号为”01”的那个。所以加锁失败!加锁失败了以后,客户端B就会通过ZK的API对他的顺序节点的上一个顺序节点加一个监听器, 即对客户端A创建的那个顺序节加监听器!如下

接着,客户端A加锁之后,可能处理了一些代码逻辑,然后就会释放锁。那么,释放锁是个什么过程呢?
其实很简单,就是把自己在zk里创建的那个顺序节点删除
删除了那个节点之后,zk会负责通知监听这个节点的监听器,也就是客户端B之前加的那个监听器,说:兄弟,你监听的那个节点被删除了,有人释放了锁。

此时客户端B的监听器感知到了上一个顺序节点被删除,也就是排在他之前的某个客户端释放了锁。
此时,就会通知客户端B重新尝试去获取锁,也就是获取”my_lock”节点下的子节点集合,此时集合里此时只有客户端B创建的唯一的一个顺序节点了!
然后呢,客户端B判断自己居然是集合中的第一个顺序节点,bingo!可以加锁了!直接完成加锁,运行后续的业务代码即可,运行完了之后再次释放锁。
注意:利用ZK实现分布式锁时要避免出现惊群效应。上述策略中,客户端B通过监听比其节点顺序小的那个临时节点,解决了惊群效应问题
分布式队列
基于ZK的临时顺序节点和Watcher 机制可实现简单的FIFO分布式队列。ZK分布式队列和上节中的分布式锁本质是一样的,都是基于对上一个顺序节点进行监听实现的。具体原理如下:

利用顺序节点的有序性,为每个数据在/FIFO下创建一个相应的临时子节点;且每个消费者均在/FIFO注册一个watcher;
消费者从分布式队列获取数据时,首先尝试获取分布式锁,获取锁后从/FIFO获取序号最小的数据,消费成功后,删除相应节点;
由于消费者均监听了父节点/FIFO,所以均会收到数据变化的异步通知,然后重复2的过程,尝试消费队列数据。依此循环,直到消费完毕。
中间件落地案例
kafka
ZK在Kafka 集群中扮演着极其重要的角色。Kafka中很多信息都在ZK中维护,如 broker集群信息、consumer集群信息、 topic相关信息、 partition信息等。Kafka的很多功能也是基于ZK实现的,如partition选主、broker集群管理、consumer负载均衡等,限于篇幅本文将不展开陈述,这里先附一张网上截图大家感受下,详情将在Kafka专题中细聊。
Dubbo
Dubbo 使用 Zookeeper 用于服务的注册发现和配置管理,详情见上文“命名服务”。
参考文献
https://mp.weixin.qq.com/s/tiAQQXbh7Tj45_1IQmQqZg
https://www.jianshu.com/p/68b45694026c
https://time.geekbang.org/column/article/239261
https://blog.csdn.net/lihao21/article/details/51810395
https://zhuanlan.zhihu.com/p/378018463
https://juejin.cn/post/6974737393324654628
https://blog.csdn.net/liuao107329/article/details/78936160
https://blog.csdn.net/en_joker/article/details/78799737
https://blog.51cto.com/u_15077535/4199740
https://juejin.cn/post/6844903729406148622