ZooKeeper技术内幕和使用篇(一)重要理论(数据模型、会话管理、分桶策略、ACL、Watcher机制)、客户端命令、ZKClient 客户端、Curator 客户端

重要理论

1. 数据模型 znode

在这里插入图片描述
zk 数据存储结构与标准的 Unix 文件系统非常相似,都是在根节点下挂很多子节点。zk中没有引入传统文件系统中目录与文件的概念,而是使用了称为 znode 的数据节点概念。znode 是 zk 中数据的最小单元,每个 znode 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。

  • (1) 节点类型
    • 持久节点:zk 中最常见的节点,节点一旦被创建,只要不删除,其就会一直存在于 zk中。
    • 持久顺序节点:一个父节点可以为其直接子节点维护一份顺序,用于记录子节点创建的先后顺序。在创建子节点时,会自动在指定的节点名称后添加数字后辍,用为该子节点的完整名称。序号由 10 位数字组成,从 0 开始计数。
    • 临时节点:临时节点的生命周期与客户端的会话绑定在一起,会话消失则该节点也就会消失。临时节点只能做叶子节点,不能创建子节点。
    • 临时顺序节点:添加了创建序号的临时节点
  • (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 所包含的子节点的个数。

2. 会话

会话是 zk 中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话相关。

ZooKeeper 客户端启动时,首先会与 zk 服务器建立一个 TCP 长连接。连接一旦建立,客户端会话的生命周期也就开始了。

(1) 会话状态

  • CONNECTING:连接中。Client 要创建一个连接,其首先会在本地创建一个 zk 对象,用于表示其所连接上的Server。从zk对象被创建开始,会话状态就进入到了CONNECTING,同时 Client 会从 Server 服务列表中通过轮询方式逐个尝试连接,直到连接成功。注意,在轮询之前,其首先会将列表进行随机打散,然后再在打散的列表基础上进行轮询
  • CONNECTED:已连接。连接成功后,该连接的各种临时性数据会被初始化到 zk 对象中。
  • CLOSED:已关闭。连接关闭后,这个代表 Server 的 zk 对象会被删除。

(2) 会话连接超时管理—客户端维护

我们这里的会话连接超时管理指的是,客户端所发起的服务端连接时间记录,是从客户端当前会话第一次发起服务端连接的时间开始计时。

(3) 会话空闲超时管理—服务端维护

服务器为每一个客户端的会话都记录着上一次交互后空闲的时长,及从上一次交互结束开始会话空闲超时的时间点。一旦空闲时长超时,服务端就会将该会话的 SessionId 从服务端清除。这也就是为什么客户端在空闲时需要定时向服务端发送心跳,就是为了维护这个会话长连接的。服务器是通过空闲超时管理来判断会话是否发生中断的。

服务端对于会话空闲超时管理,采用了一种特殊的方式——分桶策略。
A、分桶策略

  • 分桶策略是指,将空闲超时时间相近的会话放到同一个桶中来进行管理,以减少管理的复杂度。在检查超时时,只需要检查桶中剩下的会话即可,因为没有超时的会话已经被移出了桶,而桶中存在的会话就是超时的会话。
  • zk 对于会话空闲的超时管理并非是精确的管理,即并非是一超时马上就执行相关的超时操作。

B、 分桶依据

  • 分桶的计算依据为:
    在这里插入图片描述
  • 从以上公式可知,一个桶的大小为 ExpirationInterval 时间。只要 ExpirationTime 落入到同一个桶中,系统就会对其中的会话超时进行统一管理。

(4) 会话连接事件

客户端与服务端的长连接失效后,客户端将进行重连。在重连过程中客户端会产生三种会话连接事件:

  • CONNECTION_LOSS:连接丢失。因为网络抖动等原因导致客户端长时间收不到服务端的心跳回复,客户端就会引发“连接丢失事件”。该事件会触发当前客户端重连服务端,直到重连成功,或重连超时。
  • SESSION_MOVED:会话转移。当发生“连接丢失事件”后,若客户端在连接超时时限内重连服务端成功,此时当前会话的 id 是没有发生变化的。若服务器检测到同一个SessionId 的会话,两次连接到的不是同一个 zk 主机,那么服务端就会引发“会话转移异常”,客户端会引发“会话转移事件”。该事件会触发当前客户端使用第二次连接上的主机的 IP 来与 Server 进行交互。
  • SESSION_EXPIRED:会话失效。若服务端发现某客户端的会话空闲时间超时,那么服务器就会将该客户端会话进行清除。对于客户端来说,其长时间没有收到服务端的心跳回复,则会引发“连接丢失事件”,然后进行重连,直到连接成功或超时。但在会话已经从服务端清除完毕,而重连又未超时的一个很短暂的时间缝隙中,客户端与服务连接成功了。此时客户端就会发“会话失效事件”。该事件会触发客户端取消该连接,并使客户端重新实例化 zk 对象,即重新使用新的 SessionId 进行重连。

3. ACL

(1) ACL 简介

ACL 全称为 Access Control List(访问控制列表),是一种细粒度的权限管理策略,可以针对任意用户与组进行细粒度的权限控制。zk 利用 ACL 控制 znode 节点的访问权限,如节点数据读写、节点创建、节点删除、读取子节点列表、设置节点权限等。

UGO,User、Group、Others,是一种粗粒度的权限管理策略。

(2) zk 的 ACL 维度

Unix/Linux 系统的 ACL 分为两个维度:组与权限,且目录的子目录或文件能够继承父目录的 ACL 的。而 Zookeeper 的 ACL 分为三个维度:授权策略 scheme、授权对象 id、用户权限 permission,子 znode 不会继承父 znode 的权限。

A、授权策略 scheme
授权策略用于确定权限验证过程中使用的检验策略(简单来说就是,通过什么来验证权限,或一个用户要访问某个节点,系统如何验证其身份),在 zk 中最常用的有四种策略。

  • IP:根据 IP 地址进行权限验证。
  • digest:根据用户名与密码进行验证。密码可以用明文也可以用密文,但是使用密文加密需要zookeeper官方提供的加密算法进行加密
  • world:对所有用户不做任何验证。
  • super:超级用户可以对任意节点进行任意操作。这种模式打开客户端的方式都与正常方式的不同。需要在打开客户端时添加一个系统属性。-Dxxxxx

B、 授权对象 id
授权对象指的是权限赋予的用户。不同的授权策略具有不同类型的授权对象。下面是各个授权模式对应的授权对象 id。

  • ip:授权对象是 IP 地址。
  • digest:授权对象是“用户名 + 密码”。
  • world:其授权对象只有一个,即 anyone。
  • Super:与 digest 相同,授权对象为“用户名 + 密码”。

C、 权限 Permission
权限指的是通过验证的用户可以对 znode 执行的操作。共有五种权限,不过 zk 支持自定义权限。

  • c:Create,允许授权对象在当前节点下创建子节点。
  • d:Delete,允许授权对象删除当前节点。
  • r:Read,允许授权对象读取当前节点的数据内容,及子节点列表。
  • w:Write,允许授权对象修改当前节点的数据内容,及子节点列表。
    • 允许修改子节点列表意味着允许创建子节点、删除子节点

  • a:Acl,允许授权对象对当前节点进行 ACL 相关的设置。

4. Watcher 机制

zk 通过 Watcher 机制实现了发布/订阅模式。

(1) watcher 工作原理

在这里插入图片描述

  • WatchManager是客户端本地的对象,是一个集合,包含了很多Watcher对象
  • 第二步在向Server注册watcher,在向Server发送watcher并不是把整个对象原封不动发送过去,发送的只是Server需要的,比如监听的谁,监听的路径,监听它的什么变化,类型等等。可以理解为发送过去的是一个简易版的watcher
  • 如果被监听的对象发生了相应的watcher事件,服务端就会把事件通知发送给客户端

(2) watcher 事件

对于同一个事件类型,在不同的通知状态中代表的含义是不同的。
在这里插入图片描述
在这里插入图片描述

(3) watcher 特性

zk 的 watcher 机制具有以下几个特性。

  • 一次性:一旦一个 watcher 被触发,zk 就会将其从客户端的 WatcherManager 中删除,服务端中也会删除该watcher。zk 的 watcher 机制不适合监听变化非常频繁的场景。会出现消息丢失的情况。
  • 串行性:对同一个节点的相同事件类型的 watcher 回调方法的执行是串行的。
    • (实际上是因为只有watcher被删除了才允许再新建相同节点相同事件类型的watcher)如果回调是耗时操作,也不适合这种场景,因为耗时长会导致watcher没法创建,导致Server发生很多变化都感知不到。
    • 实际业务场景:Dubbo利用zookeeper做配置中心,一旦配置文件发生变化,让其他所有主机同时都变
    • Kafka在0.8前版本都是用zookeeper监听消息变化的,但是kafka消息变化很频繁,导致zookeeper压力很大,所以0.8以后改了
  • 轻量级:真正传递给 Server 的是一个简易版的 watcher。回调逻辑存放在客户端,没有在服务端。

客户端命令

1. 启动客户端

(1) 连接本机 zk 服务器

在这里插入图片描述

(2) 连接其它 zk 服务器

在这里插入图片描述

2. 查看子节点-ls

查看根节点及/brokers 节点下所包含的所有子节点列表。
在这里插入图片描述

3. 创建节点-create

(1) 创建永久节点

创建一个名称为 china 的 znode,其值为 999。
在这里插入图片描述

(2) 创建顺序节点

在/china 节点下创建了顺序子节点 beijing、shanghai、guangzhou,它们的数据内容分别为 bj、sh、gz。
在这里插入图片描述

(3) 创建临时节点

临时节点与持久节点的区别,在后面 get 命令中可以看到。
在这里插入图片描述

4. 获取节点信息-get

(1) 获取持久节点数据

在这里插入图片描述

在这里插入图片描述

(2) 获取顺序节点信息

在这里插入图片描述

(3) 获取临时节点信息

在这里插入图片描述

5. 更新节点数据内容-set

更新前:
在这里插入图片描述
更新:
在这里插入图片描述
在这里插入图片描述

6. 删除节点-delete

在这里插入图片描述
若要删除具有子节点的节点,会报错。
在这里插入图片描述

7. ACL 操作

(1) 查看权限-getAcl

在这里插入图片描述

(2) 设置权限

下面的命令是,首先增加了一个认证用户 zs,密码为 123,然后为/china 节点指定只有zs 用户才可访问该节点,而访问权限为所有权限。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

ZKClient 客户端

1. 简介

ZkClient 是一个开源客户端,在 Zookeeper 原生 API 接口的基础上进行了包装,更便于开发人员使用。内部实现了 Session 超时重连,Watcher 反复注册(用完了马上帮你注册)等功能。像 dubbo 等框架对其也进行了集成使用。

2. API 介绍

以下 API 方法均是 ZkClient 类中的方法。

(1) 创建会话

ZkClient 中提供了九个构造器用于创建会话。
在这里插入图片描述
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
在这里插入图片描述

(2) 创建节点

ZkClient 中提供了 15 个方法用于创建节点。
在这里插入图片描述
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
在这里插入图片描述

(3) 删除节点

ZkClient 中提供了 3 个方法用于删除节点。
在这里插入图片描述
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
在这里插入图片描述

(4) 更新数据

ZkClient 中提供了 3 个方法用于修改节点数据内容。
在这里插入图片描述
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
在这里插入图片描述

(5) 检测节点是否存在

ZkClient 中提供了 2 个方法用于判断指定节点的存在性,但 public 方法就一个:只有一个参数的 exists()方法。
在这里插入图片描述
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
在这里插入图片描述

(6) 获取节点数据内容

ZkClient 中提供了 4 个方法用于获取节点数据内容,但 public 方法就三个。
在这里插入图片描述
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
在这里插入图片描述

(7) 获取子节点列表

ZkClient 中提供了 2 个方法用于获取节点的子节点列表,但 public 方法就一个:只有一个参数的 getChildren()方法。
在这里插入图片描述
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
在这里插入图片描述

(8) watcher 注册

ZkClient 采用 Listener 来实现 Watcher 监听。客户端可以通过注册相关监听器来实现对zk 服务端事件的订阅。

可以通过 subscribeXxx()方法实现 watcher 注册,即相关事件订阅;通过 unsubscribeXxx()方法取消相关事件的订阅。
在这里插入图片描述
查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
在这里插入图片描述

3. 代码演示

(1) 创建工程

创建一个 Maven 的 Java 工程,并导入以下依赖。

<!--zkClient 依赖-->
<dependency>
	<groupId>com.101tec</groupId>
	<artifactId>zkclient</artifactId>
	<version>0.10</version>
</dependency>

这里仅创建一个 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);
	}
}

Curator 客户端

1. 简介

Curator 是 Netflix 公司开源的一套 zk 客户端框架,与 ZkClient 一样,其也封装了 zk 原生API。其目前已经成为 Apache 的顶级项目。同时,Curator 还提供了一套易用性、可读性更强的 Fluent 风格的客户端 API 框架。

Dubbo从2.6.4以后就开始用Curator 客户端,之前是ZKClient
Fluent 风格:链式调用风格

2. API 介绍

这里主要以 Fluent 风格客户端 API 为主进行介绍。

(1) 创建会话

  • A、普通 API 创建 newClient()
    • 在 CuratorFrameworkFactory 类中提供了两个静态方法用于完成会话的创建。
      在这里插入图片描述
    • 查看这些方法的源码可以看到具体的参数名称,这些参数的意义为:
      在这里插入图片描述
  • 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);

3. 代码演示

(1) 创建工程

创建一个 Maven 的 Java 工程,并导入以下依赖。

<!--curator 依赖-->
<dependency>
	<groupId>org.apache.curator</groupId>
	<artifactId>curator-framework</artifactId>
	<version>2.12.0</version>
</dependency>

(2) 代码

public class FluentTest {
	public static void main(String[] args) throws Exception {
		// ---------------- 创建会话 -----------
		// 创建重试策略对象:重试间隔时间是1秒,最多重试 3 次
		ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3);
		// 创建客户端
		CuratorFramework client = CuratorFrameworkFactory
									.builder()
									.connectString("zkOS:2181")
									.sessionTimeoutMs(15000)
									.connectionTimeoutMs(13000)
									.retryPolicy(retryPolicy)
									//namespace:根路径,所有操作都是基于该路径之上
									.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);
	}
}

猜你喜欢

转载自blog.csdn.net/weixin_41947378/article/details/106948298