1 重要理论
1.1 数据模型znode
zk 数据存储结构与标准的 Unix 文件系统非常相似,都是在根节点下挂很多子节点。zk 中没有引入传统文件系统中目录与文件的概念,而是使用了称为znode 的数据节点概念。znode 是 zk 中数据的最小单元,每个 znode 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。
(1)节点类型
- 持久节点:其会一直保存在 zk 中,直到将其删除为止。
- 持久顺序节点:
- 临时节点:其与创建它的会话是绑定的,会话消失,临时节点消失
- 临时顺序节点
(2)节点状态
- cZxid:Created Zxid,表示当前 znode 被创建时的事务 ID
- ctime:Created Time,表示当前 znode 被创建的时间
- mZxid:Modified Zxid,表示当前 znode 最后一次被修改时的事务 ID
- mtime:Modified Time,表示当前 znode 最后一次被修改时的时间
- pZxid:表示当前 znode 的子节点列表最后一次被修改时的事务 ID。注意,只能是其子节点列表变更了才会引起 pZxid 的变更,子节点内容的修改不会影响 pZxid。
- cversion:Children Version,表示子节点的版本号。该版本号用于充当乐观锁。
- dataVersion:表示当前 znode 数据的版本号。该版本号用于充当乐观锁。
- aclVersion:表示当前 znode 的权限 ACL 的版本号。该版本号用于充当乐观锁。
- ephemeralOwner:若当前 znode 是持久节点,则其值为 0;若为临时节点,则其值为创建该节点的会话的 SessionID。当会话消失后,会根据 SessionID 来查找与该会话相关的临时节点进行删除。
- dataLength:当前 znode 中存放的数据的长度。
- numChildren:当前 znode 所包含的子节点的个数。
1.2 ACL
(1)ACL 简介
ACL 全称为 Access Control List(访问控制列表),是一种细粒度的权限管理策略,可以针对任意用户与组进行细粒度的权限控制。zk 利用 ACL 控制znode 节点的访问权限,如节点数据读写、节点创建、节点删除、读取子节点列表、设置节点权限等。
扩展知识:UGO(User,Group,Other),是粗粒度权限管理策略。
(2)zk 的 ACL 维度
Unix/Linux 系统的 ACL 分为两个维度:组与权限。而 Zookeeper 的 ACL 分为三个维度: 授权策略 scheme、授权对象 id、用户权限 permission。
扩展知识:大多数 Unix 已经支持 ACL,Linux 从 2.6 版本开始也支持 ACL 了
- Unix/Linux 中的 ACL:子目录/子文件默认继承父目录的 ACL
- zk 中的 ACL:子 znode 不会继承父 znode 的 ACL
A、授权策略 scheme
授权策略用于确定权限验证过程中使用的检验策略(简单地说就是,通过什么来验证权限,即一个用户要访问某个 znode,如何验证其身份),在 zk 中最常用的有四种策略。
- IP
- digest:使用用户名与密码进行验证
- world:不验证
- super:
B、 授权对象 id
授权对象指的是权限赋予的用户。不同的授权策略具有不同类型的授权对象。下面是各个授权模式对应的授权对象 id。
- ip
- digest
- world:anyone
- Super
C、 权限 Permission
权限指的是通过验证的用户可以对znode 执行的操作。共有五种权限,不过 zk 支持自定义权限。
- c:create,允许授权对象在当前节点下创建子节点
- d:delete
- r:read
- w:write
- a:acl
1.3 Watcher 机制
zk 通过 Watcher 机制实现了发布/订阅模式。
(1)watcher 工作原理
(2)watcher 事件
对于同一个事件类型,在不同的通知状态中代表的含义是不同的。
(3)watcher 特性
zk 的watcher 机制具有以下几个特性。
- 一次性:zk 的watcher 机制不适合监听变化非常频繁的场景
- 串行性:
- 轻量级:
1.4 会话
会话是 zk 中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话相关。
ZooKeeper 客户端启动时,首先会与 zk 服务器建立一个 TCP 长连接。连接一旦建立,客户端会话的生命周期也就开始了。
(1)会话状态
常见的会话状态有三种:
A、CONNECTING(重要)
连接中。客户端要创建连接,首先会在客户端创建一个zk 对象。客户端会采用轮询方式逐个获取服务器列表中的 zk 的 IP 进行连接尝试,直到连接成功。注意,在轮询之前,首先会将服务器列表打散,然后再进行轮询。
B、 CONNECTED
已连接。
C、 CLOSED
已关闭。若出现会话超时、权限验证失败或客户端主动退出等情况,客户端状态就变为了 CLOSED,注意,此时客户端的 zk 对象就消失了。
(2)会话连接超时管理
当客户端向 zk 发出连接请求后,是如何知道是否连接成功的呢?当 zk 接收到某客户端会话连接后,其会向该客户端发送连接成功 ACK。当客户端接收到 ACK 后,就知道自己已经与 zk 建立了连接。
若 zk 没有收到连接请求,或客户端没有收到 zk 发送的 ACK 怎么办呢?客户端就需要进行等待,直到发生会话连接超时。然后再进行下一次连接尝试。当然,尝试一直连接不上怎么办?这就依赖于连接时设置的超时重试策略了。
会话连接超时是由客户端维护的。
(3)会话空闲超时管理(重要)
zk 为每一个客户端都维护着空闲超时管理。一旦空闲超时,服务端就会认为该客户端已丢失,其会将该会话的 SessionId 从服务端清除。这也就是为什么客户端在空闲时需要定时向服务端发送心跳,就是为了维护这个会话长连接的。服务器是通过空闲超时管理来判断会话是否发生中断的。
会话空闲超时管理是由服务端维护的。其采用了一种特殊的方式——分桶策略。
A、基本概念
分桶策略是指,将空闲超时时间相近的会话放到同一个桶中来进行管理,以减少管理的复杂度。在检查超时时,只需要检查桶中剩下的会话即可,因为在该桶的时间范围内没有超时的会话已经被移出了桶,而桶中存在的会话就是超时的会话。
zk 对于会话空闲的超时管理并非是精确的管理,即并非是一超时马上就执行相关的超时操作。
B、 分桶依据
现要在计算当前的会话需要存放到哪个会话桶中进行管理。分桶的计算依据为:
ExpirationTime= CurrentTime + SessionTimeout
BucketTime = (ExpirationTime/ExpirationInterval + 1) * ExpirationInterval
从以上公式可知,一个桶的大小为 ExpirationInterval 时间。只要 ExpirationTime 落入到同一个桶中,系统就会对其中的会话超时进行统一管理。
(4)会话连接事件
客户端与服务端的长连接失效后,客户端将进行重连。在重连过程中客户端会产生三种会话连接事件:
- 连接丢失
- 会话转移
- 会话失效
2 客户端命令
2.1 启动客户端
(1)连接本机 zk 服务器
(2)连接其它 zk 服务器
2.2 查看子节点-ls
查看根节点及/brokers 节点下所包含的所有子节点列表。
2.3 创建节点-create
(1)创建永久节点
创建一个名称为 china 的znode,其值为 999。
(2)创建顺序节点
在/china 节点下创建了顺序子节点 beijing、shanghai、guangzhou,它们的数据内容分别为 bj、sh、gz。
(3)创建临时节点
临时节点与持久节点的区别,在后面 get 命令中可以看到。
(4)创建临时顺序节点
2.4 获取节点信息-get
(1)获取持久节点数据
(2)获取顺序节点信息
(3)获取临时节点信息
2.5 更新节点数据内容-set
更新前:
更新:
2.6 删除节点-delete
若要删除具有子节点的节点,会报错。
2.7 ACL 操作
(1)查看权限-getAcl
(2)设置权限
下面的命令是,首先增加了一个认证用户 zs,密码为 123,然后为/china 节点指定只有
zs 用户才可访问该节点,而访问权限为所有权限。
3 可视化客户端
zk 常见的可视化客户端有两个:ZooView 与 ZooInspector。
3.1 ZooView
解压后直接双击运行 startup.bat 即可。
3.2 ZooInspector
在解压目录的 build 目录下进入 cmd 窗口,然后通过 jar 命令运行下面的 jar 包。
4 ZKClient 客户端
4.1 简介
ZkClient 是一个开源客户端,在 Zookeeper 原生API 接口的基础上进行了包装,更便于开发人员使用。内部实现了 Session 超时重连,Watcher 反复注册等功能。像 dubbo 等框架对其也进行了集成使用。
4.2 API 介绍
以下 API 方法均是 ZkClient 类中的方法。
(1)创建会话
ZkClient 中提供了九个构造器用于创建会话。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
zkServers | 指定zk 服务器列表,由英文状态逗号分开的 host:port 字符串组成 |
connectionTimeout | 设置连接创建超时时间,单位毫秒。在此时间内无法创建与zk 的连接,则直接放弃连接,并抛出异常 |
sessionTimeout | 设置会话超时时间,单位毫秒 |
zkSerializer | 为会话指定序列化器。zk 节点内容仅支持字节数组(byte[])类型, 且 zk 不负责序列化。在创建 zkClient 时需要指定所要使用的序列化器,例如 Hessian 或Kryo。默认使用 Java 自带的序列化方式进行对象的序列化。当为会话指定了序列化器后,客户端在进行读写操作时就会自动进行序列化与反序列化。 |
connection | IZkConnection 接口对象,是对zk 原生 API 的最直接包装,是和zk 最直接的交互层,包含了增删改查等一系列方法。该接口最常用的实现类是 zkClient 默认的实现类 ZkConnection,其可以完成绝大部分的业务需求。 |
operationRetryTimeout | 设置重试超时时间,单位毫秒 |
(2)创建节点
ZkClient 中提供了 15 个方法用于创建节点。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
path | 要创建的节点完整路径 |
data | 节点的初始数据内容,可以传入 Object 类型及 null。zk 原生API中只允许向节点传入 byte[]数据作为数据内容,但 zkClient 中具有自定义序列化器,所以可以传入各种类型对象。 |
mode | 节点类型,CreateMode 枚举常量,常用的有四种类型。 PERSISTENT:持久型 PERSISTENT_SEQUENTIAL:持久顺序型 EPHEMERAL:临时型 EPHEMERAL_SEQUENTIAL:临时顺序型 |
acl | 节点的 ACL 策略 |
callback | 回调接口 |
context | 执行回调时可以使用的上下文对象 |
createParents | 是否级递归创建节点。zk 原生 API 中要创建的节点路径必须存在, 即要创建子节点,父节点必须存在。但zkClient 解决了这个问题, 可以做递归节点创建。没有父节点,可以先自动创建了父节点,然后再在其下创建子节点。 |
(3)删除节点
ZkClient 中提供了 3 个方法用于创建节点。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
path | 要删除的节点的完整路径 |
version | 要删除的节点中包含的数据版本 |
(4)更新数据
ZkClient 中提供了 3 个方法用于修改节点数据内容。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
path | 要更新的节点的完整路径 |
data | 要采用的新的数据值 |
expectedVersion | 数据更新后要采用的数据版本号 |
(5)检测节点是否存在
ZkClient 中提供了 2 个方法用于判断指定节点的存在性,但 public 方法就一个:只有一个参数的exists()方法。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
path | 要判断存在性节点的完整路径 |
watch | 要判断存在性节点及其子孙节点是否具有 watcher 监听 |
(6)获取节点数据内容
ZkClient 中提供了 4 个方法用于获取节点数据内容,但 public 方法就三个。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
path | 要读取数据内容的节点的完整路径 |
watch | 指定节点及其子孙节点是否具有 watcher 监听 |
returnNullIfPathNotExists | 这是个 boolean 值。默认情况下若指定的节点不存在,则会抛出 KeeperException$NoNodeException 异常。设置该值为 true,若指定节点不存在,则直接返回 null 而不再抛出异常。 |
stat | 指定当前节点的状态信息。不过,执行过后该 stat 值会被最新获取到的 stat 值给替换。 |
(7)获取子节点列表
ZkClient 中提供了 2 个方法用于获取节点的子节点列表,但 public 方法就一个:只有一个参数的 getChildren()方法。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
path | 要获取子节点列表的节点的完整路径 |
watch | 要获取子节点列表的节点及其子孙节点是否具有 watcher 监听 |
(8)watcher 注册
ZkClient 采用 Listener 来实现 Watcher 监听。客户端可以通过注册相关监听器来实现对zk 服务端事件的订阅。
可以通过 subscribeXxx()方法实现 watcher 注册,即相关事件订阅;通过 unsubscribeXxx()方法取消相关事件的订阅。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
path | 要操作节点的完整路径 |
watch | 要判断存在性节点及其子孙节点是否具有 watcher 监听 |
IZkChildListener | 子节点数量变化监听器 |
IZkDataListener | 数据内容变化监听器 |
IZkStateListener | 客户端与zk 的会话连接状态变化监听器,可以监听新会话的创建、会话创建出错、连接状态改变。连接状态是系统定义好的枚举类型 Event.KeeperState 的常量 |
4.3 代码演示
(1)创建工程
创建一个 Maven 的 Java 工程,并导入以下依赖。
这里仅创建一个 ZkClient 的测试类即可。本例不适合使用 JUnit 测试。
(2)代码
public class ZKClientTest {
// 指定 zk 集群
private static final String CLUSTER = "zkOS:2181";
// 指定节点名称
private static final String PATH = "/mylog";
public static void main(String[] args) {
// ---------------- 创建会话 -----------
// 创建 zkClient
ZkClient zkClient = new ZkClient(CLUSTER);
// 为 zkClient 指定序列化器
zkClient.setZkSerializer(new SerializableSerializer());
// ---------------- 创建节点 -----------
// 指定创建持久节点
CreateMode mode = CreateMode.PERSISTENT;
// 指定节点数据内容
String data = "first log";
// 创建节点
String nodeName = zkClient.create(PATH, data, mode);
System.out.println("新创建的节点名称为:" + nodeName);
// ---------------- 获取数据内容 -----------
Object readData = zkClient.readData(PATH);
System.out.println("节点的数据内容为:" + readData);
// ---------------- 注册 watcher -----------
zkClient.subscribeDataChanges(PATH, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception{
System.out.print(" 节 点 " + dataPath);
System.out.println("的数据已经更新为了" + data);
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
System.out.println(dataPath + "的数据内容被删除");
}
});
// ---------------- 更新数据内容 -----------
zkClient.writeData(PATH, "second log");
String updatedData = zkClient.readData(PATH);
System.out.println("更新过的数据内容为:" + updatedData);
// ---------------- 删除节点 -----------
zkClient.delete(PATH);
// ---------------- 判断节点存在性 -----------
boolean isExists = zkClient.exists(PATH);
System.out.println(PATH + "节点仍存在吗?" + isExists);
}
}
5 Curator 客户端
5.1 简介
Curator 是Netflix 公司开源的一套 zk 客户端框架,与 ZkClient 一样,其也封装了 zk 原生API。其目前已经成为Apache 的顶级项目。同时,Curator 还提供了一套易用性、可读性更强的 Fluent 风格的客户端 API 框架。
5.2 API 介绍
这里主要以 Fluent 风格客户端 API 为主进行介绍。
(1)创建会话
A、普通 API 创建 newClient()
在 CuratorFrameworkFactory 类中提供了两个静态方法用于完成会话的创建。
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
参数名 | 意义 |
---|---|
connectString | 指定zk 服务器列表,由英文状态逗号分开的 host:port 字符串组成 |
sessionTimeoutMs | 设置会话超时时间,单位毫秒,默认 60 秒 |
connectionTimeoutMs | 设置连接超时时间,单位毫秒,默认 15 秒 |
retryPolicy | 重试策略,内置有四种策略,分别由以下四个类的实例指定:ExponentialBackoffRetry、RetryNTimes、RetryOneTime、RetryUntilElapsed |
B、 Fluent 风格创建
(2)创建节点 create()
下面以满足各种需求的举例方式分别讲解节点创建的方法。
说明:下面所使用的 client 为前面所创建的Curator 客户端实例。
- 创建一个节点,初始内容为空
语句:client.create().forPath(path);
说明:默认创建的是持久节点,数据内容为空。 - 创建一个节点,附带初始内容
语句:client.create().forPath(path, “mydata”.getBytes());
说明:Curator 在指定数据内容时,只能使用 byte[]作为方法参数。 - 创建一个临时节点,初始内容为空
语句:client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
说明:CreateMode 为枚举类型。 - 创建一个临时节点,并自动递归创建父节点
语句:client.create().createingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
说明:若指定的节点多级父节点均不存在,则会自动创建。
(3)删除节点delete()
- 删除一个节点
语句:client.delete().forPath(path);
说明:只能将叶子节点删除,其父节点不会被删除。 - 删除一个节点,并递归删除其所有子节点
语句:client.delete().deletingChildrenIfNeeded().forPath(path);
说明:该方法在使用时需谨慎。
(4)更新数据setData()
- 设置一个节点的数据内容
语句:client.setData().forPath(path, newData);
说明:该方法具有返回值,返回值为 Stat 状态对象。
(5)检测节点是否存在 checkExits()
- 设置一个节点的数据内容
语句:Stat stat = client.checkExists().forPath(path);
说明:该方法具有返回值,返回值为 Stat 状态对象。若 stat 为 null,说明该节点不存在,否则说明节点是存在的。
(6)获取节点数据内容getData()
- 读取一个节点的数据内容
语句:byte[] data = client.getDate().forPath(path);
说明:其返回值为byte[]数组。
(7)获取子节点列表 getChildren()
- 读取一个节点的所有子节点列表
语句:List<String> childrenNames = client.getChildren().forPath(path);
说明:其返回值为byte[]数组。
(8)watcher 注册 usingWatcher()
curator 中绑定 watcher 的操作有三个:checkExists()、getData()、getChildren()。这三个方法的共性是,它们都是用于获取的。这三个操作用于 watcher 注册的方法是相同的,都是usingWatcher()方法。
这两个方法中的参数 CuratorWatcher 与 Watcher 都为接口。这两个接口中均包含一个process()方法,它们的区别是,CuratorWatcher 中的 process()方法能够抛出异常,这样的话, 该异常就可以被记录到日志中。
- 监听节点的存在性变化
Stat stat = client.checkExists().usingWatcher((CuratorWatcher) event -> { System.out.println("节点存在性发生变化");}).forPath(path);
- 监听节点的内容变化
byte[] data = client.getData().usingWatcher((CuratorWatcher) event -> { System.out.println("节点数据内容发生变化");}).forPath(path);
- 监听节点子节点列表变化
List<String> sons = client.getChildren().usingWatcher((CuratorWatcher) event -> { System.out.println("节点的子节点列表发生变化");}).forPath(path);
5.3 代码演示
(1)创建工程
创建一个 Maven 的 Java 工程,并导入以下依赖。
(2)代码
public class FluentTest {
public static void main(String[] args) throws Exception {
// ---------------- 创建会话 -----------
// 创建重试策略对象:第 1 秒重试 1 次,最多重试 3 次
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
// 创建客户端
CuratorFramework client = CuratorFrameworkFactory
.builder()
.connectString("zkOS:2181")
.sessionTimeoutMs(15000)
.connectionTimeoutMs(13000)
.retryPolicy(retryPolicy)
.namespace("logs")
.build();
// 开启客户端
client.start();
// 指定要创建和操作的节点,注意,其是相对于/logs 节点的
String nodePath = "/host";
// ---------------- 创建节点 -----------
String nodeName = client.create().forPath(nodePath, "myhost".getBytes());
System.out.println("新创建的节点名称为:" + nodeName);
// ---------------- 获取数据内容并注册 watcher -----------
byte[] data = client.getData().usingWatcher(
(CuratorWatcher) event ->
{System.out.println(event.getPath() + "数据内容发生变化");}
).forPath(nodePath);
System.out.println("节点的数据内容为:" + new String(data));
// ---------------- 更新数据内容 -----------
client.setData().forPath(nodePath, "newhost".getBytes());
// 获取更新过的数据内容
byte[] newData = client.getData().forPath(nodePath);
System.out.println("更新过的数据内容为:" + new String(newData));
// ---------------- 删除节点 -----------
client.delete().forPath(nodePath);
// ---------------- 判断节点存在性 -----------
Stat stat = client.checkExists().forPath(nodePath);
boolean isExists = true;
if(stat == null) {
isExists = false;
}
System.out.println(nodePath + "节点仍存在吗?" + isExists);
}
}