为了应对大流量,现代应用/中间件通常采用分布式部署,此时不得不考虑 CAP 问题。ZooKeeper(后文简称 ZK)是面向 CP 设计的一个开源的分布式协调框架,将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用,分布式应用程序可以基于它实现诸如 数据发布/订阅、负载均衡、命名服务、集群管理、Master 选举、分布式锁、分布式队列 等功能。ZK 之所以能够提供上述一套分布式数据一致性解决方案,核心在于其设计精妙的数据结构、watcher 机制、Zab 一致性协议等,下面将依次剖析。
ZK 在内存中维护了一个类似文件系统的树状数据结构实现命名空间(如下),树中的节点称为 znode。
然而,znode 要比文件系统的路径复杂,既可以通过路径访问,又可以存储数据。znode 具有四个属性 data、acl、stat、children,如下
public class DataNode implements Record {
byte data[];
Long acl;
public StatPersisted stat;
private Set<String> children = null;
}
注意:znode 的数据操作具有原子性,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。znode 可存储的最大数据量是 1MB ,但实际上我们在 znode 的数据量应该尽可能小,因为数据过大会导致 zk 的性能明显下降。每个 ZNode 都对应一个唯一的路径。
Zxid 由 Leader 节点生成。当有新写入事件时,Leader 节点生成新的 Zxid,并随提案一起广播。Zxid 的生成规则如下:
zxid 是递增的,所以谁的 zxid 越大,就表示谁的数据是最新的。每个节点都保存了当前最近一次事务的 Zxid。Zxid 对于 ZK 的数据一致性以及选主都有着重要意义,后边在介绍相关知识时会重点讲解其作用原理。
节点根据生命周期的不同可以将划分为持久节点和临时节点。持久节点的存活时间不依赖于客户端会话,只有客户端在显式执行删除节点操作时,节点才消失;临时节点的存活时间依赖于客户端会话,当会话结束,临时节点将会被自动删除(当然也可以手动删除临时节点)。注意:临时节点不能拥有子节点。
节点类型是在创建时进行制定,后续不能改变。如create /n1 node1
创建了一个数据为”node1”的持久节点/n1;在上述指令基础上加上参数-e:create -e /n1/n3 node3
,则创建了一个数据为”node3”的临时节点 /n1/n3。
create 命令还有一个可选参数-s 用于指定创建的节点是否具有顺序特性。创建顺序节点时,zk 会在路径后面自动追加一个 递增的序列号 ,这个序列号可以保证在同一个父节点下是唯一的,利用该特性我们可以实现分布式锁 等功能。
基于 znode 的上述两组特性,两两组合后可构建 4 种类型的节点:
Watcher 监听机制是 ZK 非常重要的一个特性。ZK 允许 Client 端在指定节点上注册 Watcher,监听节点数据变更、节点删除、子节点状态变更等事件,当特定事件发生时,ZK 服务端会异步通知注册了相应 Watcher 的客户端,通过该机制,我们可以利用 ZK 实现数据的发布和订阅等功能。
Watcher 监听机制由三部分协作完成:ZK 服务端、ZK 客户端、客户端的 WatchManager 对象。工作时,客户端首先将 Watcher 注册到服务端,同时将 Watcher 对象保存到客户端的 Watch 管理器中。当 ZK 服务端监听的数据状态发生变化时,服务端会主动通知客户端,接着客户端的 Watch 管理器会触发相关 Watcher 来回调相应处理逻辑。
注意:
为了确保服务的高可用性,ZK 采用集群化部署,如下:
ZK 集群服务器有三种角色:Leader、Follower 和 Observer
“早期的 ZooKeeper 集群服务运行过程中,只有 Leader 服务器和 Follow 服务器。随着集群规模扩大,follower 变多,ZK 在创建节点和选主等事务性请求时,需要一半以上节点 AC,所以导致性能下降写入操作越来越耗时,follower 之间通信越来越耗时。为了解决这个问题,就引入了观察者,可以处理读,但是不参与投票。既保证了集群的扩展性,又避免过多服务器参与投票导致的集群处理请求能力下降。”
ZK 集群中通常有很多服务器,那么如何区分不同的服务器的角色呢?可以通过服务器的状态进行区分
ZK 集群是一主多从的结构,所有的所有的写操作必须要通过 Leader 完成,Follower 可直接处理并返回客户端的读请求。那么如何保证从 Follower 服务器读取的数据与 Leader 写入的数据的一致性呢?Leader 万一由于某些原因崩溃了,如何选出新的 Leader,如何保证数据恢复?Leader 是怎么选出来的?
ZK 专门设计了 ZAB 协议(Zookeeper Atomic Broadcast)来保证主从节点数据的一致性。下面分别从 client 向 Leader 和 Follower 写数据场景展开陈述。
注意:
1.客户端向 Follower 发起写请求, Follower 将写请求转发给 Leader 处理;
注意:Observer 与 Follower 写流程相同
Zab 协议消息广播使用两阶段提交的方式,达到主从数据的最终一致性。为什么是最终一致性呢?从上文可知数据写入过程核心分成下面两阶段:
根据写入过程的两阶段的描述,可以知道 ZooKeeper 保证的是最终一致性,即 Leader 向客户端返回写入成功后,可能有部分 Follower 还没有写入最新的数据,所以是最终一致性。ZooKeeper 保证的最终一致性也叫顺序一致性,即每个结点的数据都是严格按事务的发起顺序生效的。ZooKeeper 集群的写入是由 Leader 结点协调的,真实场景下写入会有一定的并发量,那 Zab 协议的两阶段提交是如何保证事务严格按顺序生效的呢?ZK 事物的顺序性是借助上文中的 Zxid 实现的。Leader 在收到半数以上 ACK 后会将提案生效并广播给所有 Follower 结点,Leader 为了保证提案按 ZXID 顺序生效,使用了一个 ConcurrentHashMap,记录所有未提交的提案,命名为 outstandingProposals,key 为 ZXID,Value 为提案的信息。对 outstandingProposals 的访问逻辑如下:
Leader 是如何判断当前 ZXID 之前是否还有未提交提案的呢?由于前提是保证顺序提交的,所以 Leader 只需判断 outstandingProposals 里,当前 ZXID 的前一个 ZXID 是否存在。代码如下:
所以 ZooKeeper 是通过两阶段提交保证数据的最终一致性,并且通过严格按照 ZXID 的顺序生效提案保证其顺序一致性的。
ZK 中默认的并建议使用的 Leader 选举算法是:基于 TCP 的 FastLeaderElection。在分析选举原理前,先介绍几个重要的参数。
ZK 的 leader 选举存在两类,一个是服务器启动时 leader 选举,另一个是运行过程中服务器宕机时的 leader 选举,下面依次展开介绍。以下两节引自[从 0 到 1 详解 ZooKeeper 的应用场景及架构] 。
1、各自推选自己:ZooKeeper 集群刚启动时,所有服务器的 logicClock 都为 1,zxid 都为 0。各服务器初始化后,先把第一票投给自己并将它存入自己的票箱,同时广播给其他服务器。此时各自的票箱中只有自己投给自己的一票,如下图所示:
2、更新选票:第一步中各个服务器先投票给自己,并把投给自己的结果广播给集群中的其他服务器,这一步其他服务器接收到广播后开始更新选票操作,以 Server1 为例流程如下:
(1)Server1 收到 Server2 和 Server3 的广播选票后,由于 logicClock 和 zxid 都相等,此时就比较 myid;
(2)Server1 收到的两张选票中 Server3 的 myid 最大,此时 Server1 判断应该遵从 Server3 的投票决定,将自己的票改投给 Server3。接下来 Server1 先清空自己的票箱(票箱中有第一步中投给自己的选票),然后将自己的新投票(1->3)和接收到的 Server3 的(3->3)投票一起存入自己的票箱,再把自己的新投票决定(1->3)广播出去,此时 Server1 的票箱中有两票:(1->3),(3->3);
(3)同理,Server2 收到 Server3 的选票后也将自己的选票更新为(2->3)并存入票箱然后广播。此时 Server2 票箱内的选票为(2->3),(3->3);
(4)Server3 根据上述规则,无须更新选票,自身的票箱内选票仍为(3->3);
(5)Server1 与 Server2 重新投给 Server3 的选票广播出去后,由于三个服务器最新选票都相同,最后三者的票箱内都包含三张投给服务器 3 的选票。
3、根据选票确定角色:根据上述选票,三个服务器一致认为此时 Server3 应该是 Leader。因此 Server1 和 Server2 都进入 FOLLOWING 状态,而 Server3 进入 LEADING 状态。之后 Leader 发起并维护与 Follower 间的心跳。
本节讨论 Follower 节点发生故障重启或网络产生分区恢复后如何进行选举。
1、Follower 重启投票给自己:Follower 重启,或者发生网络分区后找不到 Leader,会进入 LOOKING 状态并发起新的一轮投票。
2、发现已有 Leader 后成为 Follower:Server3 收到 Server1 的投票后,将自己的状态 LEADING 以及选票返回给 Server1。Server2 收到 Server1 的投票后,将自己的状态 FOLLOWING 及选票返回给 Server1。此时 Server1 知道 Server3 是 Leader,并且通过 Server2 与 Server3 的选票可以确定 Server3 确实得到了超过半数的选票。因此服务器 1 进入 FOLLOWING 状态。
Follower 发起新投票:Leader(Server3)宕机后,Follower(Server1 和 2)发现 Leader 不工作了,因此进入 LOOKING 状态并发起新的一轮投票,并且都将票投给自己,同时将投票结果广播给对方。
2、更新选票:(1)Server1 和 2 根据外部投票确定是否要更新自身的选票,这里跟之前的选票 PK 流程一样,比较的优先级为:logicLock > zxid > myid,这里 Server1 的参数(L=3, M=1, Z=11)和 Server2 的参数(L=3, M=2, Z=10),logicLock 相等,zxid 服务器 1 大于服务器 2,因此服务器 2 就清空已有票箱,将(1->1)和(2->1)两票存入票箱,同时将自己的新投票广播出去 (2)服务器 1 收到 2 的投票后,也将自己的票箱更新。
3、重新选出 Leader:此时由于只剩两台服务器,服务器 1 投票给自己,服务器 2 投票给 1,所以 1 当选为新 Leader。
4、旧 Leader 恢复发起选举:之前宕机的旧 Leader 恢复正常后,进入 LOOKING 状态并发起新一轮领导选举,并将选票投给自己。此时服务器 1 会将自己的 LEADING 状态及选票返回给服务器 3,而服务器 2 将自己的 FOLLOWING 状态及选票返回给服务器 3。
5、旧 Leader 成为 Follower:服务器 3 了解到 Leader 为服务器 1,且根据选票了解到服务器 1 确实得到过半服务器的选票,因此自己进入 FOLLOWING 状态。
对于一主多从类的集群应用,通常要考虑脑裂问题,脑裂会导致数据不一致。那么,什么是脑裂?简单点来说,就是一个集群有两个 master。通常脑裂产生原因如下:
通常解决脑裂问题有 Quorums(法定人数)方式、Redundant communications(冗余通信)方式、仲裁、磁盘锁等方式。ZooKeeper 采用 Quorums 这种方式来防止“脑裂”现象,只有集群中超过半数节点投票才能选举出 Leader。
我们可基于 ZK 的 Watcher 监听机制实现数据的发布与订阅功能。ZK 的发布订阅模式采用的是推拉结合的方式实现的,实现原理如下:
注意:Watch 具有一次性,所以当获得服务器通知后要再次添加 Watch 事件。
利用 ZK 的临时节点、watcher 机制等特性可实现负载均衡,具体思路如下:
把 ZK 作为一个服务的注册中心,基本流程:
注意:服务发现可能存在延迟,因为服务提供者挂掉到缓存更新大约需要 3-5s 的时间(根据网络环境不同还需仔细测试)。为了保证服务的实时可用,client 请求 server 发生异常时,需要根据服务消费报错信息,进行重负载均衡重试等。
命名服务是指通过指定的名字来获取资源或者服务的地址、提供者等信息。以 znode 的路径为名字,znode 存储的数据为值,可以很容易构建出一个命名服务。例如 Dubbo 使用 ZK 来作为其命名服务,如下
/dubbo
的根节点下;com.foo.BarService
;providers
和 consumers
,表示该服务的提供者和消费者;providers
下的表示该服务的所有提供者,而在 consumers
下的表示该服务的所有消费者。举例说明, com.foo.BarService
的服务提供者在启动时将自己的 URL 信息注册到 /dubbo/com.foo.BarService/providers
下;同样的,服务消费者将自己的信息注册到相应的 consumers
下,同时,服务消费者会订阅其所对应的 providers
节点,以便能够感知到服务提供方地址列表的变化。基于 ZK 的临时节点和 watcher 监听机制可实现集群管理。集群管理通常指监控集群中各个主机的运行时状态、存活状况等信息。如下图所示,主机向 ZK 注册临时节点,监控系统注册监听集群下的临时节点,从而获取集群中服务的状态等信息。
ZK 中某节点同一层子节点,名称具有唯一性,所以,多个客户端创建同一节点时,只会有一个客户端成功。利用该特性,可以实现 maser 选举,具体如下:
基于 ZK 的临时顺序节点和 Watcher 机制可实现公平分布式锁。下面具体看下多客户端获取及释放 zk 分布式锁的整个流程及背后的原理。下面过程引自[七张图彻底讲清楚 ZooKeeper 分布式锁的实现原理【石杉的架构笔记】] 。
假如说客户端 A 先发起请求,就会搞出来一个顺序节点,大家看下面的图,Curator 框架大概会弄成如下的样子:
这一大坨长长的名字都是 Curator 框架自己生成出来的。然后,因为客户端 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 分布式队列和上节中的分布式锁本质是一样的,都是基于对上一个顺序节点进行监听实现的。具体原理如下:
ZK 在 Kafka 集群中扮演着极其重要的角色。Kafka 中很多信息都在 ZK 中维护,如 broker 集群信息、consumer 集群信息、 topic 相关信息、 partition 信息等。Kafka 的很多功能也是基于 ZK 实现的,如 partition 选主、broker 集群管理、consumer 负载均衡等,限于篇幅本文将不展开陈述,这里先附一张网上截图大家感受下,详情将在 Kafka 专题中细聊。
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
https://blog.csdn.net/Saintmm/article/details/124110149
https://www.wumingx.com/linux/zk-kafka.html
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8