zookeeper_overview
概述
zk 是一个开源的,分布式协调服务,它的目的就是为了服务于分布式应用。zk 允许分布式应用通过 zk 的节点进行相互协调,常见的有配置同步、分布式锁、微服务注册与发现等等。
zk 本身和它所要协调的分布式应用一样,也是也是在集群中相互复制,以保证 zk 的高可用性。每台服务器都需要相互了解,数据保持一致。只要大多数服务器是可用的,那么整个 zk 集群就是可用的。
zk 具有以下特性
-
有序的
-
高可用
-
高吞吐
-
低延迟
其中,zk 使用一个 zxid(事务id)来保证每个更新的先后顺序,客户端可以用这个特性来实现同步原语,也就是分布式锁,zk 的高可用是通过 zk 集群的数据一致性来保证的。高吞吐以及低延迟是因为 zk 在内存中维护一套数据映射,通过内存进行读取更新。当然 zk 还会将日志文件以及数据快照持久化,这也同样为高可用提供了一定的支持。
zk 数据模型
znode
zk 中的每个节点都由路径标识,和标准文件系统类似,都是采用 "/" 反斜杠进行进行路径分割。
如下图所示
znode 是我们访问的主要实体,我们需要对它有一个清晰的认识。
zk 中的每个节点都可以拥有子节点以及相关联的数据。这里的数据一般是存储分布式服务的协调数据如:状态信息、配置信息、位置信息等,因此存储在节点上的数据通常很小,在字节到千字节的范围内。
watch(监听)
zk 支持可以在指定的节点上设置监听,当节点更改后,监听会被触发以及删除,客户端会受到一个回调包,告知节点被更改了。
3.6.0 版本支持持久化监听,监听触发后可以不被删除。
数据权限(ACL)
zk 的每个节点都会有一个 acl 列表(访问控制列表)来限制谁可以做什么
临时节点
ZooKeeper 也有临时节点的概念。只要创建znode的会话是存活的,这些 znode 就会存在。当会话结束时,删除znode。由于这种行为,临时节点不允许有子节点。会话的临时列表可以使用 getEphemerals() api 检索。
序列节点--唯一命名
当创建znode时,你也可以请求ZooKeeper在路径的末尾添加一个单调递增的计数器。这个计数器对于父节点是唯一的。如创建了一个节点 /A,在 A 节点使用唯一命名,那么后续创建子节点就会用数字自动递增,如:/A/1, /A/2。这个计数器是靠父节点维护的,这里是 A 节点。
zk 中的时间
-
zxid:zk 事务 id,每次对 zk状态信息的一个更改都会收到一个事务 id,该 id 是唯一的,zk 会保证事务 id 小的在事务 id 大的之前进行更新,防止出现丢失更新的情况。
-
版本号:对节点的每次修改都会使得版本号增加,客户端操作一个节点时会带上版本号,如果版本号不一致,那么就会操作失败。这相当于加锁了。
- version:修改 znode 数据的次数
- cversion:对 znode 子节点修改的次数
- aversion:对 znode 的 acl 列表修改的次数
-
Ticks:配置文件中的一个时间,单位是毫米,zk 中的大部分时间都是以该时间为基本单位。比如会话超时,就是 2 Ticks。
-
Real time:zk 除了在创建和修改 znode 时会将时间戳放入节点的 stat 结构中,其他任何地方都不会使用现实时间。
stat 结构
zk 节点的信息由一个 stat 结构维护
包含了以下信息:
-
czxid:该节点创建时候的 zxid
-
mzxid:该节点最后变更时候的 zxid
-
pzxid:该节点的子节点最后变更时候的 zxid
-
ctime :节点创建时候的时间戳
-
mtime:节点修改时候的时间戳
-
version :znode 数据变更的次数
-
cversion :znode 子节点变更的次数
-
aversion : znode 的 acl 列表变更的次数
-
ephemeralOwner:如果该节点是一个临时节点,则为创建该节点的会话id。如果它不是一个临时节点,它将为0。
-
dataLength :该节点存储的数据的长度
-
numChildren:该节点的子节点数
会话(session)
当我们使用客户端连接 zk 服务端的时候就创建了一个会话,在建立连接的过程中,会话状态时 connecting,当通过验证,连接成功的时候,状态进入 connected,如果因为身份验证失败或者会话超时,那么,就进入一个 close 状态。
当我们使用以下代码连接 zk 服务器成功的时候就是建立了一个会话。
ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 4000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
if (Event.KeeperState.SyncConnected == watchedEvent.getState()) {
System.out.println("watch");
}
}
});
zk 服务器会给客户端分配一个 64 位的 session id,同时为了安全考虑,也会配套的创建一套加密密码,当客户端因某种原因。连接到其他的 zk 服务器的时候,需要将 session id 和密码一起发送给 zk 服务器,重新建立连接。
zk 有一个 session 过期时间,默认为 2 Ticks time,当超过这个时间,zk 服务器没有收到客户端的信息(包括心跳),那么就会断开连接,session 就会进入到一个 close 状态。这时候在该会话中建立的所有临时节点都会被删除,同时通知给所有监听了这些节点的客户端。
如果因为连接的 zk 服务器宕机了,或者 session 在 zk 集群中重新分区时,这时候需要与其他 zk 服务器建立 session 连接,如果在超时时间内连接上了,那么状态重新回归 connected,否则,连接过期,这时候 zk 客户端会自动处理重新连接,无需重新创建新的会话对象(new ZooKeeper() )。
会话通过客户端发送的请求保持活动。如果会话在一段时间内处于空闲状态,该会话将超时,那么客户机将发送一个PING请求以保持会话处于活动状态。这个PING请求不仅允许ZooKeeper服务器知道客户端仍然是活动的,而且它还允许客户端验证它到ZooKeeper服务器的连接仍然是活动的。PING的时间足够保守,以确保有合理的时间检测死连接并重新连接到新服务器。
监听(watch)
定义:在 zk 中监听是一次性的,当对某个节点设置了监听,那么当该节点进行了变更后,客户端就会受到一个回调通知。
所有对节点的读操作都可以设置对该节点的监听:getData(), getChildren(), 以及 exists()
zooKeeper.getData("/", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println(event.getState());
}
}, new Stat());
zooKeeper.getChildren("/", new Watcher() {
@Override
public void process(WatchedEvent event) {
}
});
zooKeeper.exists("/", new Watcher() {
@Override
public void process(WatchedEvent event) {
}
});
在 zk 3.6.0 版本中,客户端还可以在znode上设置永久的、递归的监听,这些监听在被触发时不会被删除,并递归地触发注册znode以及任何子znode上的更改。
如下,分别是创建持久监听已经持久递归监听
zooKeeper.addWatch("/",AddWatchMode.PERSISTENT);
zooKeeper.addWatch("/",AddWatchMode.PERSISTENT_RECURSIVE);
监听的一些顺序性问题:
- 客户端在获取到节点的新数据之前,会先拿到对于该节点的监听时间。
- 监听的顺序和 zk 更新节点的顺序是一致的
一些要注意的点:
- 标准的监听只触发一次,触发后如果想要对对应的节点继续监听数据,需要再次对该节点添加监听机制
- 由于标准的监听是一次性的,在获取数据和发送新的监听请求这中间可能可能有多次节点变更,这样会丢失掉一些关于该节点的更新监听
访问控制列表(ACL)
acl 支持以下几种权限
-
CREATE:可以创建子节点
-
READ:可以从节点中获取数据和子节点列表
-
WRITE:可以设置节点的数据
-
DELETE:可以删除子节点
-
ADMIN:可以设置权限
保证
zk 为了能够构建更加复杂的服务,提供了以下保证
-
顺序一致性:来自客户端的更新将按照发送的顺序执行
-
原子性:更新要么成功,要么失败,没有部分成功部分失败
-
单一系统映像:客户端在不同的 zk 服务器中看到的都是相同的视图。即使因为故障转移到其他服务器,也不会看到历史视图。
-
可靠性:一旦节点被创建或更新,那么它将一直存在,除非它被删除或者更改了。
-
及时性:保证客户端在一定时间内看到到最近视图
简单的 api
zk 立志于提供一套简单编程接口,因此只支持以下几种 api
-
create :创建节点
-
delete :删除节点
-
exists :判断某个节点是否存在
-
get data:读取某个节点的数据
-
set data:为某个节点写入数据
-
get children:检索节点的子节点列表
-
sync :等待数据被同步
实现
每台 zk 服务器都会复制将自己的数据复制一份存为副本。复制的信息包含整个 zk 的内存数据,更新数据被记录到磁盘以实现可恢复性,写入数据在写入内存前会先被序列化到磁盘。
zk 集群中的服务器分为 1 台 leader 和多台 follower。zk 集群中的每台服务器都为客户端提供服务,不同的是 follower 提供读服务,leader 提供读服务和写服务,当 follower 接收到写请求时会转发到 leader 服务器处理,leader 服务器写入数据完后会广播给所有的 follower 进行同步写数据。假如 leader 出现故障,那么会从 follower 中选举一个新 leader 出来。
文章为本人学习过程中的一些个人见解,漏洞是必不可少的,希望各位大佬多多指教,帮忙修复修复漏洞!!!
参考资料
zk 官网