前言
最近加入了部门的技术兴趣小组, 被分配了 Zookeeper 的研究任务. 在研究过程当中, 发现 Zookeeper 由于其开源的特性和其卓越的性能特点, 在业界使用广泛, 有很多的应用场景, 而这些不同的应用场景实际上底层的原理都是差不多的, 只要你真正理解了 Zookeeper 的一些基础概念和机制, 就能够触类旁通.
于是乎, 在第一次和项目小组内成员分享过 Zookeeper 作为服务注册中心的原理和客户端 demo 演示之后, 我萌生出了整理一个专题的想法, 以此为起点, 慢慢捡起自己的博客分享之路.
本篇的内容主要介绍以下几点:
What is Zookeeper
Zookeeper 数据模型
Zookeeper 服务基本操作
Sessions
Watcher
总结
一, What is Zookeeper
我最早接触 Zookeeper 是因为我们项目使用的微服务治理架构是 Dubbo,Dubbo 推荐使用的服务注册中心就是 Zookeeper. 从本质上来说, Zookeeper 就是一种分布式协调服务, 在分布式环境中协调和管理服务是一个复杂的过程. ZooKeeper 通过其简单的架构和 API 解决了这个问题. ZooKeeper 允许开发人员专注于核心应用程序逻辑, 而不必担心应用程序的分布式特性. Zookeeper 最早的应用是在 Hadoop 生态中, Apache HBase 使用 ZooKeeper 跟踪分布式数据的状态.
实际上从它的名字上就很好理解, Zoo - 动物园, Keeper - 管理员, 动物园中有很多种动物, 这里的动物就可以比作分布式环境下多种多样的服务, 而 Zookeeper 做的就是管理这些服务.
ZooKeeper 的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来, 构成一个高效可靠的原语集, 并以一系列简单易用的接口提供给用户使用.
原语: 操作系统或计算机网络用语范畴. 是由若干条指令组成的, 用于完成一定功能的一个过程. 具有不可分割性. 即原语的执行必须是连续的, 在执行过程中不允许被中断.
Zookeeper 提供服务主要就是通过: 数据结构 + 原语集 + watcher 机制达到的.
分布式应用程序结合 Zookeeper 可以实现诸如数据发布 / 订阅, 负载均衡, 命名服务, 分布式协调 / 通知, 集群管理, Master 选举, 分布式锁和分布式队列等功能.
二, Zookeeper 数据模型
ZNode
从上图可以看到, Zookeeper 的数据模型和 Unix 的文件系统目录树很类似, 拥有一个层次的命名空间. 这里面的每一个节点都被称为 - ZNode, 节点可以拥有子节点, 同时也允许少量数据节点存储在该节点之下.(可以理解成一个允许一个文件也可以是一个目录的文件系统)
(1)节点引用方式
ZNode 通过路径引用, 如同 Unix 中的文件路径. 路径必须是绝对的, 因此他们必须有斜杠字符 / 来开头, 除此之外, 路径名必须是唯一的, 且不能更改.
这个特性在 Dubbo 的服务注册上也有体现, Dubbo 源码中有个贯穿全局的类 URL,dubbo 是以总线模式来时刻传递和保存配置信息的, 也就是配置信息都被放在 URL 上进行传递, 随时可以取得相关配置信息. Dubbo 在向注册中心注册时写下的节点名就是由 URL 中的 URI 和配置信息编码后组成的. 如下图.
这属于这部分知识的扩展内容, 在之后服务注册中心的章节会更具体的说明.
(2)ZNode 结构
前面提到过, ZNode 兼具文件和目录两种特点, 既像文件一样维护着数据, 元信息, 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 节点
下面有几个需要注意的知识点着重讲一下:
A. 状态信息 / 节点属性
下图是我在服务器上使用 zkClient, 用 get 命令获取到的某个 Dubbo 微服务接口节点的状态信息, 来作为示例,
- [zk: localhost:2181(CONNECTED) 0] get /dubbo/com.***.microservice.ucs.API.UniqueControlApi
- 127.0.0.1 // 节点数据 Data 域
- cZxid = 0xdd59 //Created ZXID, 表示该 ZNode 被创建时的事务 ID
- ctime = Thu Apr 18 15:17:11 CST 2019 //Created Time, 表示该 ZNode 被创建的时间
- mZxid = 0xdd59 //Modified ZXID, 表示该 ZNode 最后一次被更新时的事务 ID
- mtime = Thu Apr 18 15:17:11 CST 2019 //Modified Time, 表示该节点最后一次被更新的时间
- pZxid = 0xdd62 // 表示该节点的子节点列表最后一次被修改时的事务 ID. 注意, 只有子节点列表变更了才会变更 pZxid, 子节点内容变更不会影响 pZxid.
- cversion = 4 // 子节点的版本号
- dataVersion = 0 // 数据节点版本号
- aclVersion = 0 //ACL 版本号
- ephemeralOwner = 0x0 // 创建该节点的会话的 sessionID. 如果该节点是持久节点, 那么这个属性值为 0.
- dataLength = 9 // Data 域内容长度
- numChildren = 4 // 子节点个数 众所周知, Dubbo 接口子节点分为 providers/configurators/routers/consumers
B. Data 域
关于 Data 域, Zookeeper 中每个节点存储的数据要被原子性的操作, 也就是说读操作将获取与节点相关的所有数据, 写操作也将替换掉节点的所有数据.
值得注意的是, Zookeeper 虽然可以存储数据, 但是从设计目的上, 并不是为了做数据库或者大数据存储, 相反, 它是用来管理调度数据, 比如分布式应用中的配置文件信息, 状态信息, 汇集位置等, 这些数据通常是很小的数据, KB 为大小单位. ZNode 对数据大小也有限制, 至多 1M. 实际上从这里, 就可以推导出 Zookeeper 用于分布式配置中心的可行性.
C. Zxid
在 ZooKeeper 中, 能改变 ZooKeeper 服务器状态的操作称为事务操作. 一般包括数据节点创建与删除, 数据内容更新和客户端会话创建与失效等操作. 对应每一个事务请求, ZooKeeper 都会为其分配一个全局唯一的事务 ID, 用 Zxid 表示.
由上图的示例可以看出, Zxid 是一个 64 位的数字. 前 32 位叫做 epoch, 用来标识 Zookeeper 集群中的 Leader 节点, 当 Leader 节点更换时, 就会有一个新的 epoch. 后 32 位则为递增序列. 从这些 Zxid 中可以间接地识别出 ZooKeeper 处理这些事务操作请求的全局顺序.
(3)节点类型
ZNode 节点类型严格来说有四种: 持久节点, 临时节点, 持久顺序节点, 临时顺序节点
PERSISTENT 持久节点 - 该节点的生命周期不依赖于 session, 创建之后客户端断开连接, 节点依旧存在, 只用客户端执行删除操作, 节点才能被删除;
EPHEMERAL 临时节点 - 该节点的声明周期依赖于 session, 客户端断开连接, 临时节点就会自动删除. 另外, 临时节点不允许有子节点.
SEQUENTIAL 顺序节点 - 当选择创建顺序节点时, ZooKeeper 通过将 10 位的序列号附加到原始名称来设置 znode 的路径. 例如, 如果将具有路径 /myapp 的 znode 创建为顺序节点, 则 ZooKeeper 会将路径更改为 /myapp0000000001 , 并将下一个序列号设置为 0000000002. 如果两个顺序节点是同时创建的, 那么 ZooKeeper 不会对每个 znode 使用相同的数字. 顺序节点在锁定和同步中起重要作用.
三, Zookeeper 服务基本操作
如上图, 标明了 Zookeeper 服务的九种基本操作, 进入 ZkClient.sh, 使用 help, 可以看到这几种操作.
- [zk: localhost:2181(CONNECTED) 1] help
- ZooKeeper -server host:port cmd args
- stat path [watch] // 获取指定节点的状态信息
- set path data [version] // setData 操作
- ls path [watch] // 查看某个节点下的所有子节点信息
- delquota [-n|-b] path // 删除节点配额
- ls2 path [watch] // ls + stat 两个命令结合
- setAcl path acl // 设置 ACL
- setquota -n|-b val path // 设置节点配额,-n 是限制子节点个数 -b 是限制节点数据长度
- history // 历史命令
- redo cmdno // 执行历史命令
- printwatches on|off
- delete path [version] // 删除指定路径节点, 有子节点需要先删除子节点
- sync path // 同步视图
- listquota path // 查看节点配额信息
- rmr path // 删除节点及其子节点
- get path [watch] // 获取当前节点数据内容
- create [-s] [-e] path data acl // 创建节点
- addauth scheme auth
- quit
- getAcl path // 获取 ACL
- close
- connect host:port
从命令中可以看到, 更新 ZooKeeper 操作是有限制的. delete 或 setData 必须明确要更新的 Znode 的版本号, 我们可以调用 exists 找到. 如果版本号不匹配, 更新将会失败.
更新 ZooKeeper 操作是非阻塞式的. 因此客户端如果失去了一个更新(由于另一个进程在同时更新这个 Znode), 他可以在不阻塞其他进程执行的情况下, 选择重新尝试或进行其他操作.
四, 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 是可以当做分布式配置中心来使用的, 只不过你需要自己扩展他异步通知节点数据变化之后的逻辑, 更新你的配置. 在后面的章节会更新相关 demo.
关于 Watches 详细介绍可以参考官网的介绍:
ZooKeeper Watches
六, 总结
本章内容算是 Zookeeper 系列的开篇, 介绍了 Zookeeper 的几个基础概念, 并且给出了相关实例, 助于理解.
现在我们再回过头来看看 Zookeeper 的特性:
1 顺序一致性
从同一个客户端发起的事务请求, 最终将会严格按照其发起顺序被应用到 ZooKeeper 中.
2 原子性
所有事务请求的结果在集群中所有机器上的应用情况是一致的, 也就是说要么整个集群所有集群都成功应用了某一个事务, 要么都没有应用, 一定不会出现集群中部分机器应用了该事务, 而另外一部分没有应用的情况.
3 单一视图
无论客户端连接的是哪个 ZooKeeper 服务器, 其看到的服务端数据模型都是一致的.
4 可靠性
一旦服务端成功地应用了一个事务, 并完成对客户端的响应, 那么该事务所引起的服务端状态变更将会被一直保留下来, 除非有另一个事务又对其进行了变更.
5 实时性
通常人们看到实时性的第一反应是, 一旦一个事务被成功应用, 那么客户端能够立即从服务端上读取到这个事务变更后的最新数据状态. 这里需要注意的是, ZooKeeper 仅仅保证在一定的时间段内, 客户端最终一定能够从服务端上读取到最新的数据状态.
今天的内容中, 顺序一致性是通过 ZXid 来实现的, 全局唯一, 顺序递增, 同一个 session 中请求是 FIFO 的; 可靠性的描述也可以通过今天的知识进行理解, 一次事务的应用, 服务端状态的变更会以 Zxid,Znode 数据版本, 数据, 节点路径的形式保存下来. 剩下的几种特性是怎么实现的, 在学习完 Zookeeper 集群相关的内容之后应该就能理解.
本篇文章中借鉴了网上几篇优秀的文章, 并且结合了我本人一些思考和实践. 希望能对你学习了解 Zookeeper 起到一些帮助.
下一章, 我会介绍 Zookeeper 集群方面的知识, CAP 理论在 Zookeeper 中的实践, 以及如何搭建 Zookeeper 的集群.
参考
[1] https://zookeeper.apache.org/doc/r3.4.14/ 官方文档(强烈推荐)
[2] https://www.cnblogs.com/sunddenly/p/4033574.html 作者应该是对官方文档有比较深的了解, 我发现他的文章的脉络和官网有很相似的地方. 写的非常好
[3] https://www.jianshu.com/p/a1721bbf61ca 作者对 Zookeeper 做了一个易懂的总体介绍
[4] https://www.w3cschool.cn/zookeeper/ w3cSchool tutorial 加粗文字
来源: https://segmentfault.com/a/1190000018927058