1、数据模型 znode
zk 数据存储结构与标准的 Unix 文件系统非常相似,都是在根节点下挂很多子节点。zk中没有引入传统文件系统中目录与文件的概念,而是使用了称为 znode 的数据节点概念。
znode 是 zk 中数据的最小单元,每个 znode 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。
1.1、节点类型
- 持久节点
- 持久顺序节点
- 临时节点:临时节点的生命周期与客户端的会话绑定在一起。临时节点不能有子节点,即临时节点只能是叶子节点
- 临时顺序节点
1.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.3、会话
会话是 zk 中最重要的概念之一,客户端与服务端之间的任何交互操作都与会话相关。
ZooKeeper 客户端启动时,首先会与 zk 服务器建立一个 TCP 长连接。连接一旦建立,客户端会话的生命周期也就开始了。
1.3.1、会话状态
- CONNECTING:连接中。客户端要创建连接,其首先会在客户端创建一个 zk 对象,代表服务器。其会采用轮询的方式对服务器列表逐个尝试连接,直到连接成功。不过,为了对 Server 进行负载均衡,其会首先对服务器列表进行打散操作,然后再轮询。
- CONNECTED:已经连接
- CLOSED:连接已经关闭
1.3.2、会话连接事件
客户端与服务端的长连接失效后,客户端将进行重连。在重连过程中客户端会产生三种会话连接事件:
- CONNECTION_LOSS:连接丢失。因为网络抖动等原因导致连接中断,在客户端会引发连接丢失事件。该事件会触发客户端逐个尝试重新连接服务器,直到连接成功或超时。
- SESSION_MOVED:会话转移。当连接丢失后,在 SessionTimeout 内重连成功,则 SessionId是不变的。若两次连接上的 Server 不是同一个,则会引发会话转移事件。该事件会引发客户端更新本地 zk 对象中的相关信息。
- SESSION_EXPIRED:会话失效。若客户端在 SessionTimeout 内没有连接成功,则服务器会将该会话进行清除,并向Client发送通知。但在Client收到通过之前,又连接上了Server,此时的这个会话是失效的,会引发会话失效事件。该事件会触发客户端重新生成新的SesssionId 重新连接 Server。
1.3.3、会话连接超时管理–分桶策略
分桶策略是指,将超时时间相近的会话放到同一个桶中来进行管理,以减少管理的复杂度。在检查超时时,只需要检查桶中剩下的会话即可,因为没有超时的会话已经被移出了桶,而桶中存在的会话就是超时的会话。
从以下描述可知,zk 对于会话的超时管理并非是精确的管理,即并非是一超时马上就执行相关的超时操作。
分桶依据
分桶的计算依据为:
ExpirationTime = CurrentTime + SessionTimeout
BucketTime = (ExpirationTime/ExpirationInterval + 1) * ExpirationInterval
从以上公式可知,一个桶的大小为 ExpirationInterval 时间。只要 ExpirationTime 落入到同一个桶中,系统就会对其中的会话超时进行统一管理
2、ACL
ACL 全称为 Access Control List(访问控制列表),是一种细粒度的权限管理策略,可以针对任意用户与组进行细粒度的权限控制。zk 利用 ACL 控制 znode 节点的访问权限,如节点数据读写、节点创建、节点删除、读取子节点列表、设置节点权限等。
UGO,粗粒度权限管理。
目前在 Linux/Unix 文件系统中使用,也是使用最广泛的权限控制方式。是一种粗粒度的文件系统权限控制模式。
2.1、zk 的 ACL 维度
Unix/Linux 系统的 ACL 分为两个维度:组与权限,且目录的子目录或文件能够继承父目录的 ACL 的。
而 Zookeeper 的 ACL 分为三个维度:授权策略 scheme、授权对象 id、用户权限 permission,子 znode 不会继承父 znode 的权限。
授权策略
授权策略用于确定权限验证过程中使用的检验策略(即通过什么来验证权限),在 zk 中最常用的有四种策略。
- IP:根据 IP 进行验证。
- digest:根据用户名与密码进行验证。
- world:对所有用户不做任何验证。
- super:超级用户对任意节点具有任意操作权限
授权对象 id
授权对象指的是权限赋予的用户。不同的授权策略具有不同类型的授权对象。下面是各个授权模式对应的授权对象 id:
- ip:将权限授予指定的 ip
- digest:将权限授予具有指定用户名与密码的用户。
- world:将权限授予一个用户 anyone
- Super:将权限授予具有指定用户名与密码的用户。
权限 Permission
权限指的是通过验证的用户可以对 znode 执行的操作。共有五种权限,不过 zk 支持自定义权限。
- c:Create,允许授权对象在当前节点下创建子节点
- d:Delete,允许授权对象删除当前节点
- r:Read,允许授权对象读取当前节点的数据内容及子节点列表
- w:Write,允许授权对象修改当前节点数据内容及子节点列表(可以为当前节点增/删 除子节点)
- a:Acl,允许授权对象对当前节点进行 ACL 设置
3、Watcher 机制
zk 通过 Watcher 机制实现了发布/订阅模式。
3.1、 watcher 工作原理
3.2、 watcher 事件
对于同一个事件类型,在不同的通知状态中代表的含义是不同的。
客户端所处状态 | 事件类型(常量值) | 触发条件 | 说明 |
---|---|---|---|
SyncConnected | None(-1) | 客户端与服务器成功建立会话 | 此时客户端与服务器处于连接状态 |
NodeCreated(1) | Watcher 监听的对应数据节点被创建 | ||
NodeDeleted(2) | Watcher 监听的对应数据节点被删除 | ||
NodeDataChanged(3) | Watcher 监听的对应数据节点的数据内容发生变化 | ||
NodeChildrenChanged(4) | Watcher 监听的节点的子节点列表发生变化 | ||
Disconnected(0) | None(-1) | 客户端与 zk 断开连接 | 此时客户端与服务器处于连接断开状态 |
Expired(-112) | None(-1) | 会话失效 | 此时客户端会话失效,通常会收到SessionExpiredException异常 |
AuthFailed | None(-1) | 使用错误的 scheme进行权限检查 | 通常会收到AuthFailedException 异 常 |
3.3、 watcher 特性
zk 的 watcher 机制具有以下几个特性。
- 一次性:watcher 机制不适合监听变化非常频繁的场景
- 串行性:只有当当前的 watcher 回调执行完毕了,才会向 server 注册新的 watcher(注 意,是对同一个节点相同事件类型的监听)。
- 轻量级:Client 向 Server 发送的 watcher 不是一个完整的,而是简易版的。另外,回调逻辑不是 Server 端的,而是 Client 的。
4、客户端
4.1、 客户端命令
//连接zk服务
zkCli.sh
//连接其他zk服务
zkCli.sh -server ip
//查看子节点
ls /
//创建永久节点
create /address add
//创建顺序节点
create -s /address /wuhan wh
//创建临时节点
create -e /address /shenzhen sz
//获取持久节点数据
get /address
//获取顺序节点信息
get /address/wuhan
//获取临时节点信息
get /address/shenzhen
//更新节点数据内容
set /address adds
//删除节点
delete /address/wuhan
ACL操作
//查看权限
getAcl /address
//设置权限
addauth digest zs:123//增加一个认证用户zs,密码123
setAcl /address auth:zs:123:cdrwa//为/adress节点指定只有zs用户才可以访问该节点,访问权限为所有权限
4.2、 ZKClient 客户端
ZkClient 是一个开源客户端,在 Zookeeper 原生 API 接口的基础上进行了包装,更便于开发人员使用。内部实现了 Session 超时重连,Watcher 反复注册等功能。像 dubbo 等框架对其也进行了集成使用。
4.2.1 API介绍
4.2.1.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 | 设置重试超时时间,单位毫秒 |
4.2.1.2 创建节点
参数的意义为:
参数名 | 意义 |
---|---|
path | 要创建的节点完整路径 |
data | 节点的初始数据内容,可以传入 Object 类型及 null。zk 原生 API中只允许向节点传入 byte[]数据作为数据内容,但 zkClient 中具有 |
自定义序列化器,所以可以传入各种类型对象。 | |
mode | 节点类型,CreateMode 枚举常量,常用的有四种类型。PERSISTENT:持久型;PERSISTENT_SEQUENTIAL:持久顺序型;EPHEMERAL:临时型;EPHEMERAL_SEQUENTIAL:临时顺序型 |
acl | 节点的 ACL 策略 |
callback | 回调接口 |
context | 执行回调时可以使用的上下文对象 |
createParents | 是否级递归创建节点。zk 原生 API 中要创建的节点路径必须存在,即要创建子节点,父节点必须存在。但 zkClient 解决了这个问题,可以做递归节点创建。没有父节点,可以先自动创建了父节点,然后再在其下创建子节点 |
4.2.1.3 删除节点
参数的意义为:
参数名 | 意义 |
---|---|
path | 要删除的节点的完整路径 |
version | 要删除的节点中包含的数据版本 |
4.2.1.4 更新节点
参数的意义为:
参数名 | 意义 |
---|---|
path | 要更新的节点的完整路径 |
data | 要采用的新的数据值 |
expectedVersion | 数据更新后要采用的数据版本号 |
4.2.1.5 检测节点是否存在
参数的意义为:
参数名 | 意义 |
---|---|
path | 要判断存在性节点的完整路径 |
watch | 要判断存在性节点及其子孙节点是否具有 watcher 监听 |
4.2.1.6 获取节点数据内容
参数的意义为:
参数名 | 意义 |
---|---|
path | 要读取数据内容的节点的完整路径 |
watch | 指定节点及其子孙节点是否具有 watcher 监听 |
returnNullIfPathNotExists | 这是个 boolean 值。默认情况下若指定的节点不存在,则会抛出 KeeperException$NoNodeException 异常。设置该值为 true,若指定节点不存在,则直接返回 null 而不再抛出异常。 |
watch | 指定当前节点的状态信息。不过,执行过后该 stat 值会被最新获取到的 stat 值给替换。 |
4.2.1.7 获取子节点列表
参数的意义为:
参数名 | 意义 |
---|---|
path | 要获取子节点列表的节点的完整路径 |
watch | 要获取子节点列表的节点及其子孙节点是否具有 watcher 监听 |
4.2.1.8 watcher 注册
ZkClient 采用 Listener 来实现 Watcher 监听。客户端可以通过注册相关监听器来实现对zk 服务端事件的订阅。
可以通过 subscribeXxx()方法实现 watcher 注册,即相关事件订阅;通过 unsubscribeXxx()方法取消相关事件的订阅。
参数的意义为:
参数名 | 意义 |
---|---|
path | 要操作节点的完整路径 |
IZkChildListener | 子节点数量变化监听器 |
IZkDataListener | 数据内容变化监听器 |
IZkStateListener | 客户端与zk的会话连接状态变化监听器,可以监听新会话的创建、会话创建出错、连接状态改变。连接状态是系统定义好的枚举类型 Event.KeeperState 的常量 |
4.2.2 代码
依赖
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
//创建会话
ZkClient zkClient = new ZkClient(ip);
//指定序列化器
zkClient.setZkSerializer(new SerializableSerializer());
//创建节点
String path = "/address/beijing";
String data = "bj";
CreateMode createMode = CreateMode.PERSISTENT;
zkClient.create(path, data, createMode);
//获取节点
Object readData = zkClient.readData(path);
//更新节点
zkClient.writeData(path, "bj1");
//注册watcher
zkClient.subscribeDataChanges(path, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
log.info("节点路径:{}", dataPath);
log.info("数据内容更新为:{}", data);
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
log.info(dataPath + "的数据内容被删除");
}
});
//判断节点是否存在
boolean exists = zkClient.exists(path);
4.3、 Curator 客户端
Curator 是 Netflix 公司开源的一套 zk 客户端框架,与 ZkClient 一样,其也封装了 zk 原生API。其目前已经成为 Apache 的顶级项目。同时,Curator 还提供了一套易用性、可读性更强的 Fluent 风格的客户端 API 框架
4.3.1、API 介绍
4.3.1.1、创建会话
加粗样式普通 API 创建 newClient()
在 CuratorFrameworkFactory 类中提供了两个静态方法用于完成会话的创建。
参数的意义为:
参数名 | 意义 |
---|---|
path | 要操作节点的完整路径 |
connectString | 指定 zk 服务器列表,由英文状态逗号分开的 host:port 字符串组成 |
sessionTimeoutMs | 设置会话超时时间,单位毫秒,默认 60 秒 |
connectionTimeoutMs | 设置连接超时时间,单位毫秒,默认 15 秒 |
retryPolicy | 重试策略,内置有四种策略,分别由以下四个类的实例指定:ExponentialBackoffRetry、RetryNTimes、RetryOneTime、 |
RetryUntilElapsed |
Fluent 风格创建
//创建客户端
CuratorFramework client= CuratorFrameworkFactory
.builder()
.connectString(ip)
.sessionTimeoutMs(5000)
.connectionTimeoutMs(3000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))//重试策略:第一秒重试一次,最多重试3次
.namespace("logs")
.build();
//开启客户端
client.start();
4.3.1.2、节点操作
String path = "/address";
String data = "hello, Timi";
String newData = "hello, Tomas";
//创建一个节点,初始内容为空,默认为持久化节点
client.create().forPath(path);
//创建一个节点,并初始化内容,Curator 在指定数据内容时,只能使用 byte[]作为方法参数
client.create().forPath(path, data.getBytes());
//创建一个临时节点,初始内容为空, CreateMode 为枚举类型
client.create().withMode(CreateMode.EPHEMERAL).forPath(path);
//创建一个临时节点,并自动递归创建父节点。若指定的节点多级父节点均不存在,则会自动创建
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL)
.forPath(path);
//删除一个节点,只能将叶子节点删除,其父节点不会被删除。
client.delete().forPath(path);
//删除一个节点,并递归删除其所有子节点
client.delete().deletingChildrenIfNeeded().forPath(path);
//设置一个节点的数据内容,该方法具有返回值,返回值为 Stat 状态对象
client.setData().forPath(path, newData.getBytes());
//检测节点是否存在,返回值为 Stat 状态对象。若 stat 为 null,说明该节点不存在,否则说明节点是存在的。
Stat stat = client.checkExists().forPath(path);
//获取节点数据内容
byte[] dataContent = client.getData().forPath(path);
//获取子节点列表
List<String> childrenNames = client.getChildren().forPath(path);
//watcher 注册 usingWatcher(), curator 中绑定 watcher 的操作有三个:checkExists()、getData()、getChildren()
//监听节点的存在性变化
Stat stat1 = client.checkExists().usingWatcher((CuratorWatcher) event -> {
log.info("节点存在性发生变化");
}).forPath(path);
//监听节点的内容变化
byte[] content = client.getData().usingWatcher((CuratorWatcher) event -> {
System.out.println("节点数据内容发生变化");
}).forPath(path);
//监听节点子节点列表变化
List<String> sons = client.getChildren().usingWatcher((CuratorWatcher) event -> {
System.out.println("节点的子节点列表发生变化");
}).forPath(path);
5、Zookeeper 典型应用场景
5.1 配置维护
5.1.1 介绍
分布式系统中,很多服务都是部署在集群中的,即多台服务器中部署着完全相同的应用,起着完全相同的作用。当然,集群中的这些服务器的配置文件是完全相同的。
若集群中服务器的配置文件需要进行修改,那么我们就需要逐台修改这些服务器中的配置文件,出错的概率大大提升。
5.1.2 实现原理
zk 可以通过“发布/订阅模型”实现对集群配置文件的管理与维护。“发布/订阅模型”分为推模式(Push)与拉模式(Pull)。zk 的“发布/订阅模型”采用的是推拉相结合的模式。
5.2 命名服务
5.2.1 介绍
命名服务是指可以为一定范围内的元素命名一个唯一标识,以与其它元素进行区分。在分布式系统中被命名的实体可以是集群中的主机、服务地址等。
5.2.2 实现原理
通过利用 zk 中顺序节点自动生成唯一编号的特点来实现命名服务。
首先创建一组业务相关的节点,然后再在这些节点下再创建顺序节点,此时的顺序节点的路径加名称即为生成的唯一标识。
5.3 DNS 服务
5.3.1 介绍
zk 的 DNS 服务是命名服务的一种特殊用法。其对外表现出的功能主要是防止提供者的单点问题,实现对提供者的负载均衡。
5.3.2 实现原理
5.3.3 什么是 DNS
DNS,Domain Name System,域名系统,即可以将一个名称与特定的主机 IP 加端口号进行绑定。zk 可以充当 DNS 的作用,完成域名到主机的映射。
5.3.4 基本 DNS 实现原理
假设应用程序 app1 与 app2 分别用于提供 service1 与 service2 两种服务,现要将其注册到 zk 中,具体的实现步骤如下图所示。
若某应用(例如 app1)具有多个服务名称,则可以在该应用节点下添加多个子节点
若某域名下的服务提供者增加,则可以首先读取该节点中的数据内容,然后再将该增加的提 供者主机信息与原来数据内容一起再写入到节点数据内容中。 若某域名需要修改,则则直接新增一个节点
5.3.5 具有状态收集功能的 DNS 实现原理
以上模型存在一个问题,如何获取各个提供者主机的健康状态、运行状态呢?可以为每一个域名节点再添加一个状态子节点,而该状态子节点的数据内容则为开发人员定义好的状态数据。这些状态数据是如何获取到的呢?是通过状态收集器(开发人员自行开发的)定期写入到 zk 的该节点中的。
阿里的 Dubbo 就是使用 Zookeeper 作为域名服务器的
5.4 Master 选举
集群是分布式系统中不可或缺的组成部分,是为了解决分布式系统中计算单元的单点问题,水平扩展计算单元的处理能力的一种解决方案。
一般情况下,会在群集中选举出一个 Master,用于协调集群中的其它 Slave 主机,对于Slave 主机的状态具有决定权。
例如,读写分离集群,Master 处理写请求,Slave 处理读请求(主从集群)再如,对于复杂处理逻辑的系统,Master 负责处理复杂逻辑计算,然后将计算结果写入到一个中间存储系统(DB,或 DFS 等),Slave 负责从中间存储系统中读取结果,并向读请求进行响应。
5.5 分布式同步
分布式同步,也称为分布式协调,是分布式系统中不可缺少的环节,是将不同的分布式组件有机结合起来的关键。对于一个在多台机器上运行的应用而言,通常需要一个协调者来控制整个系统的运行流程,例如执行的先后顺序,或执行与不执行等。
5.6 集群管理
能够随时获取到以下信息 当前集群中各个主机的运行时状态、 当前集群中主机的存活状况:
5.7 分布式锁
分布式锁是控制分布式系统同步访问共享资源的一种方式。Zookeeper 可以实现分布式锁功能。根据用户操作类型的不同,可以分为排他锁与共享锁。
5.7.1 分布式锁的实现
在 zk 上对于分布式锁的实现,使用的是类似于“/xs_lock/[hostname]-请求类型-序号”的临时顺序节点。
其具体实现过程如下:
- Step1:每一个客户端会对 xs_lock 节点注册子节点列表变更事件的 watcher 监听,随时监听子节点的变化情况
- Step2:若客户端需要向某资源发出操作请求时,其首先要从 zk 中获取一把分布式锁。
这个获取分布式锁的过程即是到 xs_lock 节点下创建一个读写操作的临时顺序节点。读写操作的顺序性就是通过这些子节点的顺序性体现的。 - Step3:在当前子节点创建完后,即 zk 将分布式锁分配给该客户端后,当前子节点会对 比其与其它子节点序号的大小关系,并根据读写操作的不同,执行不同的逻辑。
- 读请求:若没有比自己小的节点,或比自己小的节点都是读请求节点,则当前请求可以直接读取;若比自己小的节点中存在写请求节点,则当前请求等待。
- 写请求:若没有比自己小的节点,则直接进行写操作。若发现有比自己小的节 点,那些节点无论是读还是写节点,当前写操作都需要等待。
- Step4:客户端操作完毕后,与 zk 的连接断开,则 zk 中该会话对应的节点消失。
5.7.2 分布式锁的改进
前面的实现方式存在“羊群效应”,为了解决其所带来的性能下降,可以对前述分布式锁的实现进行改进。
由于一个操作而引发了大量的低效或无用的操作的执行,这种情况称为羊群效应。
当客户端请求发出后,在 zk 中创建相应的临时顺序节点后马上获取当前的/xs_lock 的所有子节点列表,但任何客户端都不向/xs_lock 注册用于监听子节点列表变化的 watcher。而是改为根据请求类型的不同向“对其有影响的”子节点注册 watcher。
5.8 分布式队列
常用的分布式消息队列中间件产品有:RabbitMQ、Kafka。zk 也可以实现简单的消息队列。
5.8.1 FIFO 队列
zk 实现 FIFO 队列的思路是:利用顺序节点的有序性,为每个数据在 zk 中都创建一个相应的节点。然后为每个节点都注册 watcher 监听。一个节点被消费,则会引发消费者消费下一个节点,直到消费完毕。
5.8.2 分布式屏障 Barrier 队列
Barrier,屏障、障碍物。Barrier 队列是分布式系统中的一种同步协调器,规定了一个队列中的元素必须全部聚齐后才能继续执行后面的任务,否则一直等待。其常见于大规模分布式并行计算的应用场景中:最终的合并计算需要基于很多并行计算的子结果来进行。
zk 对于 Barrier 的实现原理是,在 zk 中创建一个/barrier 节点,其数据内容设置为屏障打开的阈值,即当其下的子节点数量达到该阈值后,app 才可进行最终的计算,否则一直等待。每一个并行运算完成,都会在/barrier 下创建一个子节点,直到所有并行运算完成。