分布式专题——15 ZooKeeper特性与节点数据类型详解
1 简介
-
ZooKeeper 是 Apache Hadoop 的子项目,作为开源分布式协调框架,旨在解决分布式集群应用系统的一致性问题。它把复杂易错的分布式一致性服务封装成高效可靠的原语集,通过简单易用接口供用户使用;
-
官网:Apache ZooKeeper;
-
从本质看,ZooKeeper 是文件系统 + 监听机制的分布式小文件存储系统,以类似文件系统目录树的方式存储数据,能有效管理树中节点,维护和监控数据状态变化,进而实现基于数据的集群管理、统一命名服务、分布式配置管理、分布式消息队列、分布式锁、分布式协调等功能;
-
从设计模式角度,ZooKeeper 是一个基于观察者模式设计的分布式服务管理框架,存储管理大家关心的数据。ZooKeeper 接受观察者的注册,当数据状态变化时,会通知已注册的观察者做出反应;
-
在实际应用流程中,ZooKeeper 集群的作用:
- 服务启动时(如 Server1、Server2、Server3),会注册信息(创建临时节点)到集群;
- Client(如 Client1、Client2、Client3)获取当前在线 server 列表并注册监听;
- 当服务节点下线(比如某一 server 出现故障等情况),ZooKeeper 集群会向相关 Client 发送 server 节点下线通知;
- 之后 Client 会重新获取 server 列表并注册监听,以保证能获取最新的服务节点信息;
2 快速入门
2.1 ZooKeeper 安装
-
下载地址:Apache ZooKeeper;
-
解压安装包后进入
conf
目录,复制zoo_sample.cfg
,修改文件名为zoo.cfg
;cp zoo_sample.cfg zoo.cfg
-
修改
zoo.cfg
配置文件,将dataDir=/tmp/zookeeper
修改为指定的 data 目录:# zookeeper时间配置中的基本单位(毫秒) tickTime=2000 # 允许follower初始化连接到leader最大时长,它表示tickTime时间倍数 即:initLimit*tickTime initLimit=10 # 允许follower与leader数据同步最大时长,它表示tickTime时间倍数 syncLimit=5 # zookeeper 数据存储目录及日志保存目录(如果没有指明dataLogDir,则日志也保存在这个文件中) dataDir=/tmp/zookeeper # 对客户端提供的端口号 clientPort=2181 # 单个客户端与zookeeper最大并发连接数 maxClientCnxns=60 # 保存的快照数量,之外的将会被清除 autopurge.snapRetainCount=3 # 自动触发清除任务时间间隔,小时为单位。默认为0,表示不自动清除。 autopurge.purgeInterval=1
-
启动 zookeeper server:
# 可以通过 bin/zkServer.sh 来查看都支持哪些参数 # 默认加载配置路径conf/zoo.cfg bin/zkServer.sh start bin/zkServer.sh start conf/my_zoo.cfg# 查看zookeeper状态 bin/zkServer.sh status
-
启动 zookeeper client 连接 zookeeper server:
bin/zkCli.sh # 连接远程的zookeeper server bin/zkCli.sh -server ip:port
2.2 客户命令行操作
-
通过命令
help
查看 zookeeper 支持的所有命令: -
常见 cli 命令:ZooKeeper: Because Coordinating Distributed Systems is a Zoo;
命令基本语法 功能描述 help 显示所有操作命令 ls [-s] [-w] [-R] path 使用 ls 命令来查看当前 znode 的子节点[可监听]
-w:监听子节点变化
-s:节点状态信息(时间戳、版本号、数据大小等)
-R:表示递归的获取create [-s] [-e] [-c] [-t ttl] path [data] [acl] 创建节点
-s:创建有序节点
-e:创建临时节点
-c:创建一个容器节点
ttl:创建一个 TTL 节点,-t 时间(单位毫秒)
data:节点的数据,可选,如果不使用时,节点数据就为 null
acl:访问控制get [-s] [-w] path 获取节点数据信息
-s:节点状态信息(时间戳、版本号、数据大小等)
-w:监听节点变化set [-s] [-v version] path data 设置节点数据
-s:表示节点为顺序节点
-v:指定版本号getAcl [-s] path 获取节点的访问控制信息
-s:节点状态信息(时间戳、版本号、数据大小等)setAcl [-s] [-v version] [-R] path acl 设置节点的访问控制列表
-s:节点状态信息(时间戳、版本号、数据大小等)
-v:指定版本号
-R:递归的设置stat [-w] path 查看节点状态信息 delete [-v version] path 删除某一节点,只能删除无子节点的节点
-v:表示节点版本号deleteall path 递归的删除某一节点及其子节点 setquota -n|-b val path 对节点增加限制
n:表示子节点的最大个数
b:数据值的最大长度,-1表示无限制
2.3 GUI 工具
- https://issues.apache.org/jira/secure/attachment/12436620/ZooInspector.zip;
- Zo开源的 prettyZoo:https://github.com/vran-dev/PrettyZoo/releases;
- 收费的 ZooKeeperAssistant:ZooKeeper Assistant - ZooKeeper可视化管理与监控工具。
3 ZooKeeper 数据结构
3.1 整体概述
-
ZooKeeper 数据模型结构类似 Unix 文件系统,整体可看作一棵树,每个节点称为 ZNode;
-
其数据模型是层次模型,层次模型常见于文件系统,层次模型和 key-value 模型是两种主流数据模型。ZooKeeper 采用文件系统模型主要基于两点考虑:
- 一是文件系统的树形结构便于表达数据之间的层次关系;
- 二是便于为不同应用分配独立的命名空间(namespace);
-
ZooKeeper 的层次模型称作 Data Tree,Data Tree 的每个节点叫 ZNode。和文件系统不同,每个 ZNode 都能保存数据,默认每个 ZNode 可存储 1MB 数据,且每个 ZNode 能通过自身路径唯一标识,每个节点还有版本(version),版本从 0 开始计数;
-
从代码层面看:
DataTree
类中用ConcurrentHashMap<String, DataNode>
来存储节点,还有dataWatches
和childWatches
用于管理监听;DataNode
类实现了Record
接口,包含存储数据的byte
数组、访问控制相关的Long
类型acl
、持久化状态StatPersisted
以及子节点集合children
等成员;
public class DataTree {private final ConcurrentHashMap<String, DataNode> nodes =new ConcurrentHashMap<String, DataNode>();private final WatchManager dataWatches = new WatchManager();private final WatchManager childWatches = new WatchManager(); }public class DataNode implements Record {byte data[];Long acl;public StatPersisted stat;private Set<String> children = null;}
3.2 ZNode
3.2.1 介绍
-
ZooKeeper 存在多种节点(ZNode)类型,各有不同生命周期:
类型 生命周期 创建示例 持久节点(persistent node) 一直存在,一直存储在 ZooKeeper 服务器上,即使创建该节点的客户端与服务端的会话关闭了,该节点依然不会被删除 create /locks 临时节点(ephemeral node) 当创建该临时节点的客户端会话因超时或发生异常而关闭时,该节点也相应在 ZooKeeper 服务器上被删除 create -e /locks/DBLock 有序节点(sequential node) 并不算是一种单独种类的节点,而是在持久节点和临时节点特性的基础上,增加了一个节点有序的性质。在我们创建有序节点的时候会自动使用一个单调递增的数字作为后缀 create -e -s /jobs/job(临时有序节点) 容器节点(container node) 当一个容器节点的最后一个子节点被删除后,容器节点也会被删除 create -c /work TTL节点(ttl node) 当一个TTL节点在 TTL 内没有被修改并且没有子节点,会被删除。注意:默认此功能不开启,需要修改配置文件 extendedTypesEnabled=true
create -t 3000 /ttl_node -
此外,还有几种组合类型节点:
-
持久节点(PERSISTENT):这样的节点在创建后,即使 ZooKeeper 集群或客户端宕机也不会丢失;
-
临时节点(EPHEMERAL):客户端宕机或在指定超时时间内未给 ZooKeeper 集群发消息,节点就会消失;
-
持久顺序节点(PERSISTENT_SEQUENTIAL):除了具备持久节点的特点,节点名还具备顺序性;
-
临时顺序节点(EPHEMERAL_SEQUENTIAL):除了具备临时节点的特点,节点名也具备顺序性;
-
-
ZooKeeper 主要用到上述 4 种节点,另外 3.5.3 版本新增 Container 容器节点:
- 当容器节点无子节点时,会被 ZooKeeper 定期(定时任务默认 60s 检查一次)删除;
- 与持久节点的区别是:ZooKeeper 服务端启动后,有有一个单独的线程去扫描所有容器节点,当子节点数为 0 时会自动删除该容器节点,可用于 Leader 选举或锁场景(例如,当锁释放或领导者退出时,相关节点被删除后,容器节点会自动清理);
- 注意:容器节点不能有子容器节点(即不能嵌套容器节点);
-
TTL 节点:带过期时间节点,默认禁用,需要在
zoo.cfg
中添加extendedTypesEnabled=true
开启。 注意:TTL 不能用于临时节点; -
不同节点的创建命令示例:
# 创建持久节点 create /servers xxx # 创建临时节点 create -e /servers/host xxx # 创建临时有序节点 create -e -s /servers/host xxx # 创建容器节点 create -c /container xxx # 创建TTL节点 create -t 10 /ttl
3.2.2 示例:实现分布式锁
-
分布式锁需要在锁的持有者出现异常(宕机)时能释放锁,而 ZooKeeper 的临时节点(
ephemeral
节点)就具备类似于分布式锁这样的特性:当创建临时节点的客户端会话结束(比如客户端宕机,会话关闭),临时节点会被自动删除;-
终端 1:
# 启动 ZooKeeper 命令行客户端 zkCli.sh # 创建一个临时节点 /lock,此时终端 1 持有这个“锁”(对应临时节点) create –e /lock # 退出客户端,会话结束,临时节点 /lock 会被自动删除,锁释放 quit
-
终端 2:
# 同样先启动客户端 zkCli.sh # 因为终端 1 已创建了 /lock 节点,所以下面的创建会失败 create –e /lock # 对 /lock 节点设置监听,当节点状态变化(比如终端 1 退出后节点被删除)时,会收到通知 stat –w /lock # 当终端 1 退出,/lock 节点被删除后,终端 2 就能成功创建临时节点 /lock,获取到锁 create –e /lock
-
-
节点状态信息:ZooKeeper 的节点类似树状结构,可存储信息和属性,通过
stat
命令查看节点状态:-
cZxid
:节点创建时的事务 ID,用于标识节点创建这个操作的唯一性; -
ctime
:节点创建的时间戳,记录节点创建的时间; -
mZxid
:节点最后一次被修改时的事务 ID,每次对节点的修改(包括数据修改等)都会更新该值;对于 ZooKeeper 来说,每次的变化都会产生一个唯一的事务 id,即 Zxid(ZooKeeper Transaction Id);
- 通过 Zxid,可以确定更新操作的先后顺序。例如,如果 Zxid1 小于 Zxid2 ,说明 Zxid1 操作先于 Zxid2 发生;
- Zxid 对于整个 ZooKeeper 都是唯一的,即使操作的是不同的 ZNode;
-
mtime
:节点最后一次被修改的时间戳; -
pZxid
:节点的子节点列表最后一次被修改的事务 ID;- 添加或删除子节点操作才会变更
pZxid
,对于子节点内容的修改不影响; - 换句话说:只有子节点列表变更了才会变更
pzxid
,子节点内容变更不会影响pzxid
;
- 添加或删除子节点操作才会变更
-
cversion
:子节点的版本号,子节点有变化(添加、删除)时,版本号加 1; -
dataVersion
:节点数据的版本号,每次对节点数据进行set
操作(即使数据相同),版本号加 1,可避免数据更新的先后顺序问题; -
aclVersion
:节点访问控制列表(ACL)的版本号; -
ephemeralOwner
:若该节点为临时节点,该值是与节点绑定的会话 ID;若为持久节点,值为 0(持久节点);客户端和 ZooKeeper 服务端通信前要建立会话(
session
),会话因连接超时、授权失败或显式关闭等情况结束时,临时节点会被删除; -
dataLength
:节点存储数据的长度; -
numChildren
:节点直接子节点的数量。
-
3.2.3 示例:ZooKeeper 乐观锁删除
悲观锁:假定会冲突,因此先加锁再访问,独占资源;
乐观锁:假定不冲突,因此直接访问,只在提交更新时检测是否发生冲突;
-
先删除再创建节点
- 执行
delete /test2
命令删除/test2
节点; - 再用
create /test2
重新创建该节点; - 这一步是为后续基于版本号的操作做准备,重新初始化节点;
- 执行
-
尝试按指定版本删除节点(失败)
-
执行
delete -v 1 /test2
,试图删除版本号(cversion
)为 1 的/test2
节点。但操作失败,提示“version No is not valid : /test2”;在 ZooKeeper 的
delete -v
命令中,版本号指的是dataVersion
; -
接着用
get -s /test2
获取节点详细信息,发现此时节点的cversion
为 0,这就解释了为何按版本 1 删除会失败——当前节点版本并非 1;
-
-
修改节点数据
-
执行
set /test2 abc
命令,修改/test2
节点的数据为abc
。然后再次用get -s /test2
查询节点信息; -
此时可以看到,节点的
dataVersion
变为 1(因为数据被修改,版本号递增),而我们关注的cversion
仍为 0(cversion
主要和子节点相关,这里没有子节点操作,所以未变化);
-
-
按正确版本删除节点(成功)
- 执行
delete -v 1 /test2
,这次删除成功; - 因为经过前面修改节点数据等操作后,节点相关版本变化,此时按版本 1 删除能匹配上节点的版本状态,所以删除操作生效;
- 执行
-
ZooKeeper 中基于版本号的操作(如这里的
delete -v
),是乐观锁的一种体现。客户端在操作节点时,会携带期望的版本号,服务端会检查节点当前版本是否与期望版本一致:-
若一致,操作(删除、修改等)成功,且版本号递增;
-
若不一致,操作失败,以此来避免分布式环境下的并发冲突问题。
-
3.3 watch 机制
3.3.1 介绍
-
ZooKeeper 的 watch 机制是一种监听机制,需要客户端先向服务端注册监听,当对应的事件发生时,服务端会通知客户端;
-
监听的对象是事件,支持的事件类型如下:
-
None:连接建立事件;
-
NodeCreated:节点创建事件;
-
NodeDeleted:节点删除事件;
-
NodeDataChanged:节点数据变化事件;
-
NodeChildrenChanged:子节点列表变化事件;
-
DataWatchRemoved:节点监听被移除事件;
-
ChildWatchRemoved:子节点监听被移除事件;
-
-
相关命令:
# 监听节点数据的变化 get -w path # 获取节点数据并设置监听 stat -w path # 获取节点状态并设置监听 # 监听子节点增减的变化 ls -w path # 列出节点子节点并设置监听
-
监听特性:
-
一次性触发:watch 是一次性的,触发后就会被移除,若要再次使用需要重新注册;
-
客户端顺序回调:watch 回调是顺序串行执行的,只有回调完成后客户端才能看到最新的数据状态,且一个 watcher 的回调逻辑不宜过多,否则会影响其他 watch 执行;
-
轻量级:WatchEvent 是最小的通信单位,仅包含通知状态、事件类型和节点路径,不会告知数据节点变化前后的具体内容;
-
时效性:watcher 只有在当前 session 彻底失效时才会无效,如果在 session 有效期内快速重连成功,watcher 依然存在,还能接收通知;
-
-
永久性 Watch:在 ZooKeeper 3.6.0 版本新增了永久性 Watch 功能,被触发后仍然保留,可继续监听 ZNode 上的变更。通过
addWatch [-m mode] path
命令为指定节点添加事件监听,支持两种模式:-
PERSISTENT:持久化订阅,针对当前节点的修改和删除事件,以及当前节点的子节点的删除和新增事件;
-
PERSISTENT_RECURSIVE:持久化递归订阅(默认),在 PERSISTENT 的基础上,增加了子节点修改的事件触发,子节点的子节点的数据变化也会触发相关事件(满足递归订阅特性)。
-
3.3.2 示例:协同服务
-
设计一个 master-worker 的组成员管理系统,要求:
- 保证只有一个 master
- master 监控 worker 状态
-
**保证只有一个 master **。要确保系统中只有一个 master,可以利用 ZooKeeper 临时节点(ephemeral 节点)的特性:临时节点会在创建它的客户端会话结束(如客户端宕机)时自动删除
# master1 创建了临时节点 /master,此时 master1 成为系统中的 master create -e /master "m1:2223"# 由于 /master 节点已存在,所以创建失败,master2 无法成为 master create -e /master "m2:2223" Node already exists: /master# master2 对 /master 节点设置监听,这样当 /master 节点状态变化(比如 master1 宕机,节点被删除)时,master2 能收到通知 stat -w /master# 当 master2 收到 /master 节点删除的通知后,再次执行 create -e /master "m2:2223" # 此时若成功创建,master2 就成为新的 master。 create -e /master "m2:2223"
- 这种方式也可用于 master-slave 选举:
-
master 实时获取 worker 的情况,借助 ZooKeeper 的节点监听机制以及临时节点特性;
# master服务 create /workers # 创建/workers节点作为所有worker节点的父节点,用于统一管理worker# 让master服务监控/workers下的子节点 ls -w /workers # 对/workers节点设置子节点变化监听,当子节点增删时会收到通知# worker1 create -e /workers/w1 "w1:2224" # worker1创建临时子节点,数据为自身标识和端口;由于master设置了监听,会收到NodeChildrenChanged通知,得知有新的 worker 加入# master服务 ls -w /workers # 监听是一次性的,收到通知后需重新注册监听,以继续监控后续子节点变化# worker2 create -e /workers/w2 "w2:2224" # worker2创建临时子节点;master因重新注册了监听,会再次收到子节点变化通知,得知有新的 worker 加入# master服务 ls -w /workers # 再次重新注册监听,确保持续监控# worker2 quit # worker2退出导致会话结束,其创建的临时节点/w2被自动删除;master收到子节点删除的通知,感知到worker2下线
3.3.3 示例:条件更新
-
用 ZooKeeper 的 ZNode(下图中的
/c
)实现一个计数器(counter
),通过set
命令完成自增操作。在分布式环境下,多个客户端可能同时操作这个计数器,若不加以控制,会出现基于过期数据更新的问题,而条件更新能解决此问题; -
过程:
-
初始状态:ZooKeeper 中的
/c
节点数据版本为 0; -
客户端 1 首次操作:客户端 1 执行
get -s -w /c
。获取/c
节点数据(此时为 0)和版本号(version=0
),同时设置监听(-w
); -
客户端 2 操作:
- 客户端 2 执行
set -s -v 0 /c 1
,基于版本号 0,将/c
节点数据更新为 1,此时节点版本号变为 1; - 客户端 1 因之前设置了监听,会收到节点数据变化的通知;
- 客户端 2 执行
-
客户端 1 再次获取并更新:
- 客户端 1 收到通知后,再次执行
get -s -w /c
,获取到/c
节点数据为 1,版本号为 1; - 然后执行
set -s -v 1 /c 2
,基于版本号 1,将/c
节点数据更新为 2,此时节点版本号变为 2(这一步下图中未体现);
- 客户端 1 收到通知后,再次执行
-
错误情况演示(若用无条件更新):
- 如果客户端 1 不知道
/c
已被客户端 2 更新(即没有收到通知),还用过时的版本 1 去更新,若采用无条件更新,会错误地将/c
数据更新为 2; - 但其实此时正确的后续更新应基于版本 1 之后的状态,条件更新通过版本号校验,能避免这种错误(当版本不匹配时,更新失败);
- 如果客户端 1 不知道
-
-
ZooKeeper 的条件更新(
set
命令带-v
指定版本号),要求更新操作时的版本号与节点当前版本号一致,才会执行更新。这样就保证了客户端是基于最新的节点状态进行操作,避免了分布式环境下多个客户端因并发操作,基于过期数据更新而导致的数据不一致问题。
3.4 节点特性总结
-
同一级节点 key 名称是唯一的:
- 在 ZooKeeper 中,同一父节点下的子节点,其名称(key)是唯一的;
- 例:先创建了
/lock
节点(create /lock
),然后再次创建/lock
节点(create /lock
)时,会提示“Node already exists: /lock”,即节点已存在,创建失败;
-
创建 ZooKeeper 节点时,需要指定从根节点(
/
)开始的完整路径,不能只写相对路径; -
session 关闭时,临时节点清除:
- 临时节点(通过
create -e
创建的节点)与客户端的 session 相关联; - 当客户端的 session 关闭(比如客户端断开连接)时,临时节点会被自动清除;
- 临时节点(通过
-
自动创建顺序节点:
- 可以创建带顺序的节点(通过
create -s
或create -e -s
,后者是临时且顺序的节点)。ZooKeeper 会为顺序节点自动生成唯一的顺序编号,保证节点创建的顺序性; - 例:创建
/queue/host1
、/queue/host2
等临时顺序节点时,节点名称后自动添加了如10000000000
、20000000001
等顺序编号,通过ls -R /queue
可以看到这些带顺序编号的节点;
- 可以创建带顺序的节点(通过
-
watch 机制,监听节点变化
- ZooKeeper 的 watch 机制类似于观察者模式。客户端向服务端的某个节点路径注册一个 watcher,同时客户端存储特定的 watcher;
- 当节点数据或子节点发生变化时,服务端通知客户端,客户端进行回调处理;
- 需要注意的是,监听事件是单次触发的,事件触发后,该 watcher 就失效了,如果还需要监听,需要重新注册。
-
delete 命令只能一层一层删除。使用
delete
命令删除节点时,只能删除没有子节点的节点,需要一层一层地删除。不过新版本的 ZooKeeper 可以通过deleteall
命令递归删除节点及其所有子节点。
3.5 应用场景详解
-
ZooKeeper 适用于存储和协同相关的关键数据,但不适合大数据量存储,因为其设计更侧重于分布式协同而非大规模数据承载;
-
基于 ZooKeeper 具备的诸多节点特性(如节点唯一性、临时节点机制、watch 监听机制、顺序节点等),能支持多种经典应用场景:
-
注册中心:服务可在 ZooKeeper 注册自身信息,其他服务能方便地发现和调用这些已注册的服务;
-
数据发布/订阅(常用于实现配置中心):可将配置数据发布到 ZooKeeper 节点,订阅的应用能实时获取配置变更,实现配置的集中管理与动态更新;
-
负载均衡:结合服务注册等,可根据服务节点的状态等信息,将请求合理分发到不同服务节点,平衡各节点负载;
-
命名服务:为分布式系统中的资源(如服务、节点等)提供统一的命名规则,方便识别和管理;
-
分布式协调/通知:在分布式环境下,各节点可通过 ZooKeeper 进行协调操作,或接收节点状态变化等通知;
-
集群管理:能监控集群中节点的状态(如在线、离线等),实现集群的统一管理;
-
Master 选举:在分布式系统中,通过 ZooKeeper 选出一个主节点(Master),负责协调或管理其他节点,保证系统中只有一个有效主节点;
-
分布式锁:利用 ZooKeeper 的节点特性,实现分布式环境下的锁机制,保证多个节点对共享资源的互斥访问;
-
分布式队列:基于 ZooKeeper 的顺序节点等,可实现分布式队列,支持多节点间的有序任务调度等。
-
3.5.1 统一命名服务
-
在分布式环境中,为了方便识别应用服务,常需要对服务进行统一命名。比如 IP 地址难以记忆,而域名更容易记住;
-
通过 ZooKeeper 可以实现这种统一命名服务。如图所示:
- ZooKeeper 以树状结构组织节点,根节点(
/
)下有services
节点,services
下又有www.baidu.com
节点,www.baidu.com
节点下关联了多个 IP 地址(183.232.231.172
、180.97.34.94
、39.156.66.18
等); - 客户端(
Client1
、Client2
)可以通过访问www.baidu.com
这个易记的域名节点,来获取对应的 IP 地址等服务信息,无需直接记忆复杂的 IP;
- ZooKeeper 以树状结构组织节点,根节点(
-
利用 ZooKeeper 顺序节点的特性,能够制作分布式的序列号生成器(也叫 ID 生成器);
-
在分布式环境下,需要为数据生成唯一 ID,UUID 虽然能保证唯一性,但没有规律,不利于理解和管理。而 ZooKeeper 生成的顺序节点可以产生有顺序、易理解且支持分布式环境的编号;
-
例:
/order
节点下有多个顺序节点,如/order - date1 - 00000000000001
、/order - date2 - 00000000000002
等,这些节点带有顺序编号,可作为分布式环境下数据的唯一 ID,既保证了唯一性,又有顺序性,方便使用和管理;/ └── /order├── /order-date1-000000000000001├── /order-date2-000000000000002├── /order-date3-000000000000003├── /order-date4-000000000000004└── /order-date5-000000000000005
-
3.5.2 数据发布/订阅
-
数据发布/订阅的常见场景是配置中心:发布者将数据发布到 ZooKeeper 的一个或一系列节点上,订阅者对这些节点进行数据订阅,从而实现动态获取数据的目的;
-
配置信息通常具有以下特点:
-
数据量小的 KV:配置数据一般是键值对(Key-Value)形式,且数据量不大;
-
数据内容在运行时会发生动态变化:比如系统运行过程中,配置可能需要调整,需要能动态更新;
-
集群机器共享,配置一致:集群中的多台机器需要共享相同的配置,保证配置的一致性;
-
-
ZooKeeper 采用“推 + 拉”结合的方式来实现数据发布/订阅:用推及时通知客户端数据变化,通过拉让客户端获取到最新的完整数据
-
推:服务端(ZooKeeper 服务)会向注册了监控节点的客户端推送 Watcher 事件通知。当 ZooKeeper 中存储配置数据的节点发生变化时,服务端会主动通知订阅了该节点的客户端;
-
拉:客户端获得通知后,会主动到服务端拉取最新的数据;
-
下图中:
- ZooKeeper 中有
/configuration
节点存储配置数据,Client1
、Client2
、Client3
等客户端通过watch
(监听)机制关注该节点; - 当
/configuration
节点数据变化时,ZooKeeper 服务端会推送通知给客户端,客户端再主动拉取最新配置数据;
- ZooKeeper 中有
-
3.5.3 统一集群管理
-
在分布式环境里,实时掌握每个节点的状态十分必要,这样能依据节点的实时状态做出相应调整。ZooKeeper 可实现对节点状态变化的实时监控,具体方式为:
-
把节点信息写入 ZooKeeper 上的一个 ZNode(ZooKeeper 的数据节点);
-
监听这个 ZNode,从而获取它的实时状态变化;
-
-
如下图:
- ZooKeeper 中有
/GroupManager
节点,其下有/client1
、/client2
、/client3
等子节点,分别对应集群中的Client1
、Client2
、Client3
; - 客户端(
Client1
、Client2
、Client3
)会向对应的 ZNode 进行注册并设置监听; - 当节点状态发生变化时,ZooKeeper 能及时将变化通知给监听的客户端,使得集群管理者或其他相关组件可以实时了解各节点的状态,进而进行如负载均衡调整、故障节点处理等操作;
- ZooKeeper 中有
3.5.4 负载均衡
-
在 ZooKeeper 中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求,以此实现负载均衡;
-
如下图:
- ZooKeeper 以树状结构组织节点,根节点(
/
)下有services
节点,services
下又有orderService
节点,orderService
节点下关联了多个服务器(以ip:port
标识),并且记录了每台服务器的访问数(如访问数 100、50、80 等); - 当
Client1
、Client2
等客户端发起访问请求时,系统会参考 ZooKeeper 中记录的各服务器访问数,将请求导向当前访问数最少的服务器,从而平衡各服务器的负载,避免部分服务器因请求过多而压力过大,部分服务器又过于空闲的情况;
- ZooKeeper 以树状结构组织节点,根节点(
3.5.5 Master-Worker 架构
-
Master-Worker 是一种广泛应用的分布式架构,在该架构中,有一个
master
负责监控worker
的状态,并为worker
分配任务;-
单一 active master:
- 在任何时刻,系统中最多只能有一个处于
active
(活跃)状态的master
; - 如果出现多个
master
共存的情况,会导致“脑裂”问题(即多个master
各自为战,对系统状态和任务分配产生冲突,破坏系统一致性);
- 在任何时刻,系统中最多只能有一个处于
-
备份 master:
- 系统中除了处于
active
状态的master
,还会有一个backup master
(备份主节点); - 当
active master
失败时,backup master
可以快速进入active
状态,保证系统服务的连续性;
- 系统中除了处于
-
worker 状态监控与任务重分配:
master
会实时监控worker
的状态,能够及时收到worker
成员变化(如worker
上线、下线、故障等)的通知;- 当
master
收到worker
成员变化的通知时,通常会重新进行任务的分配,以适应worker
资源的变化,确保任务能高效执行;
-
-
如下图:
- 左侧的
cluster
(集群)中包含master
以及worker1
、worker2
、worker3
; - 右侧的树状结构展示了 ZooKeeper 中的节点组织,
/
为根节点,下有master
和workers
节点,workers
下又有w1
、w2
、w3
等子节点,分别对应各个worker
; master
通过 ZooKeeper 的节点机制来监控worker
的状态变化,从而实现对worker
的管理和任务分配;
- 左侧的
3.6 ACL 权限控制
- ZooKeeper 的 ACL(Access Control List,访问控制列表)权限在生产环境至关重要,可对节点设置读写等权限,保障数据安全。
3.6.1 ACL 构成
-
ZooKeeper 的 ACL 通过
[scheme:id:permissions]
构成权限列表:-
scheme:授权模式,有
world
(授权所有客户端)、auth
(用添加认证的用户)、digest
(用户:密码方式)、ip
(IP 地址认证)、super
(超级用户)几种; -
id:授权对象,若为
IP
模式,是 IP 地址或网段;Digest
/Super
模式对应用户名;World
模式则是所有用户; -
permissions:授权权限,由
cdrwa
组成,分别代表创建(create
)、删除(delete
)、读(read
)、写(write
)、管理(admin
)权限;
-
-
授权模式说明
模式 描述 world
授权对象为 anyone
,所有登录服务器的客户端都能对节点执行对应权限操作auth
使用添加认证的用户进行认证 digest
使用 用户:密码
方式验证ip
对连接的客户端用 IP 地址认证 -
权限类型说明
权限类型 ACL 简写 描述 read
r
读取节点及显示子节点列表的权限 write
w
设置节点数据的权限 create
c
创建子节点的权限 delete
d
删除子节点的权限 admin
a
设置该节点 ACL 权限的权限 -
授权命令说明
授权命令 用法 描述 getAcl
getAcl path
读取节点的 ACL setAcl
setAcl path acl
设置节点的 ACL create
create path data acl
创建节点时设置 ACL addAuth
addAuth scheme auth
添加认证用户,类似登录操作 -
ACL 权限控制测试:
-
取消节点读权限
-
先创建
/name
节点,用getAcl /name
查看,默认是world:anyone:cdrwa
(所有权限); -
执行
setAcl /name world:anyone:cdwa
,取消读(r
)权限; -
再用
get /name
读取节点,提示Insufficient permission : /name
,即因无读权限,读取操作失败;
-
-
取消节点删除子节点权限
-
创建
/name/fox
子节点; -
执行
setAcl /name world:anyone:cwa
,取消删除(d
)权限; -
尝试
delete /name/fox
删除子节点,提示Insufficient permission : /name/fox
,因无删除子节点权限,删除操作失败;
-
-
3.6.2 auth授权模式
-
创建用户:
addauth digest shisan:123456
- 添加一个采用
digest
认证模式的用户,用户名为shisan
,密码为123456
;
- 添加一个采用
-
设置权限:
-
为
/name
节点设置 ACL 权限,指定只有用户shisan
(密码123456
)拥有创建(c
)、删除(d
)、读(r
)、写(w
)、管理(a
)的权限;setAcl /name auth:shisan:123456:cdrwa
-
对
shisan:123456
进行 SHA - 1 加密并做 Base64 编码,会返回一串加密后的字符串;echo -n shisan:123456 | openssl dgst -binary -sha1 | openssl base64
-
用加密后的信息(
ZSwmgmtnTnxIusRfIvoHFJAYGQU
)来设置节点权限,这样更安全:setAcl /name auth:shisan:ZSwmgmtnTnxIusRfIvoHFJAYGQU=:cdrwa
-
-
权限验证:
-
退出客户端后重新连接,执行
get /name
命令尝试读取/name
节点,此时提示Insufficient permission : /name
,即没有足够权限,因为重新连接后,客户端未携带之前添加的认证用户信息; -
接着执行
addauth digest shisan:123456
命令,重新添加认证用户shisan
,之后再执行get /name
命令,就能够成功读取节点内容。这验证了只有通过认证的用户才能访问被设置了对应权限的节点。
-
3.6.3 digest授权模式
-
digest
授权模式是一种基于用户名:密码的认证方式,密码会经过加密处理;-
设置权限:为
/lost/shisan
节点设置digest
模式的 ACL 权限,指定用户shisan
(密码经加密后为ZSwmgmtnTnxIusRF1voHFJAYGQU=
)拥有创建(c
)、删除(d
)、读(r
)、写(w
)、管理(a
)的权限;setAcl /lost/shisan digest:shisan:ZSwmgmtnTnxIusRF1voHFJAYGQU=:cdrwa
-
操作示例:
-
先创建
/lost
和/lost/shisan
节点,初始时/lost/shisan
节点的 ACL 是world:anyone:cdrwa
(所有客户端都有全部权限,通过getAcl /lost/shisan
可以获取);create /lost create /lost/shisan
-
执行上方设置权限的
setAcl
命令修改权限后,直接执行getAcl /lost/shisan
会提示Insufficient permission : /lost/shisan
(权限不足); -
执行
addauth digest shisan:123456
添加认证用户后,再执行getAcl /lost/shisan
,就能成功获取到该节点的 ACL 信息,显示为digest,'shisan:ZSwmgmtnTnxIusRF1voHFJAYGQU=:cdrwa
,这就验证了只有通过认证的用户才能访问设置了digest
权限的节点。
-
-
3.6.4 IP 授权模式
-
IP
授权模式是基于客户端 IP 地址进行权限控制;-
设置权限:
# 为 /node-ip 节点设置 ACL 权限,指定 IP 为 192.168.109.128 的客户端拥有创建、删除、读、写、管理的权限 setAcl /node-ip ip:192.168.109.128:cdrwa # 创建 /node-ip 节点时就设置好 IP 授权的 ACL create /node-ip data ip:192.168.109.128:cdrwa
-
多 IP 支持:多个指定 IP 可以通过逗号分隔,例如
setAcl /node-ip ip:IP1:rw,ip:IP2:a
,这样 IP 为IP1
的客户端有读和写权限,IP 为IP2
的客户端有管理权限。
-
3.6.5 Super 超级管理员模式
-
Super
是一种特殊的Digest
模式,在该模式下,超级管理员用户可以对 ZooKeeper 上的所有节点进行任何操作; -
开启方式:需要在 ZooKeeper 的启动脚本中添加 JVM 参数来开启,参数为:
-Dzookeeper.DigestAuthenticationProvider.superDigest=admin:<base64encoded(SHA1(123456))
admin
是用户名,123456
是密码,base64encoded(SHA1(123456))
是对admin:123456
进行 SHA - 1 加密后再做 Base64 编码的结果。
3.6.6 可插拔身份验证接口
-
ZooKeeper 提供了权限扩展机制,允许用户实现自定义的权限控制方式;
-
实现方式:要实现自定义的权限控制机制,需要继承
AuthenticationProvider
接口,用户通过该接口可以实现自定义的权限控制逻辑;public interface AuthenticationProvider {// 返回标识插件的字符串String getScheme();// 将用户和验证信息关联起来KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);// 验证 ID 格式是否有效boolean isValid(String id);// 将认证信息与 ACL 进行匹配,判断是否命中权限规则boolean matches(String id, String aclExpr);// 判断是否已授权boolean isAuthenticated(); }
4 ZooKeeper 集群架构
4.1 集群角色
-
Leader(领导者):
- 是事务请求(写操作,如
create
、setData
、delete
等)的唯一调度者和处理者,保证集群事务处理的顺序性; - 同时也是集群内部各个服务器的调度者,对于写操作请求,会统一转发给 Leader 处理,Leader 需要决定编号、执行操作,这个过程称为事务;
- 是事务请求(写操作,如
-
Follower(跟随者):
- 处理客户端的非事务(读操作)请求,这类请求可以直接响应;
- 对于事务请求,会转发给 Leader;
- 参与集群 Leader 的选举投票;
-
Observer(观察者):
-
对于非事务请求可以独立处理(读操作),事务性请求会转发给 Leader 处理;
-
Observer 节点接收来自 Leader 的
inform
信息,更新自己的本地存储,但不参与提交和选举投票; -
作用是在不影响集群事务处理能力的前提下,提升集群的非事务处理能力;
-
配置示例:配置一个 ID 为 3 的观察者节点,其中
:observer
标识该节点为 Observer 角色server.3=192.168.0.3:2888:3888:observer
-
应用场景:
- 提升集群读性能:因为 Observer 不参与提交和选举的投票过程,所以往集群里添加 Observer 节点可以提高整个集群的读性能;
- 跨数据中心部署:例如需要部署一个北京和香港两地都可使用的 ZooKeeper 集群服务,且要求北京和香港客户的读请求延迟都很低。解决方案是把香港的节点都设置为 Observer,这样香港的读请求可由本地 Observer 节点处理,降低延迟。
-
4.2 集群架构
-
ZooKeeper 集群由 Leader 节点和 Follower 节点组成:
-
Leader 节点(以 Server2 为例):可以处理读写请求,是集群中事务(写操作)的唯一调度者和处理者,保证集群事务处理的顺序性;
-
Follower 节点(以 Server1、Server3 为例):只能处理读请求,当接收到写请求时,会把写请求转发给 Leader 来处理;
-
客户端与节点交互:
- 客户端可以向 Leader 节点发起读或写请求,也可以向 Follower 节点发起读请求;
- 若向 Follower 节点发起写请求,Follower 会将其转发给 Leader 处理;
-
-
ZooKeeper 通过以下方式保证数据一致性:
-
全局可线性化(Linearizable)写入:先到达 Leader 的写请求会被先处理,Leader 负责决定写请求的执行顺序,确保所有客户端对写操作的顺序感知是一致的;
-
客户端 FIFO 顺序:来自给定客户端的请求按照发送顺序执行,保证了单个客户端请求的有序性。
-
4.3 三节点 ZooKeeper 集群搭建
4.3.1 环境准备
-
需要三台虚拟机,IP 分别为:
192.168.65.163 192.168.65.184 192.168.65.186
-
如果条件有限,也可以在一台虚拟机上搭建 ZooKeeper 伪集群。
4.3.2 集群搭建步骤
-
修改
zoo.cfg
配置,添加 server 节点配置-
首先修改数据存储目录,设置
dataDir=/data/zookeeper
; -
然后在三台虚拟机的
zoo.cfg
文件末尾添加如下配置:server.1=192.168.65.163:2888:3888
server.2=192.168.65.184:2888:3888
server.3=192.168.65.186:2888:3888
server.A=B:C:D
中:A
是服务器编号,集群模式下需在dataDir
目录下创建myid
文件(就是下一步所做的工作),文件内容为A
的值,ZooKeeper 启动时读取该文件,与zoo.cfg
中的配置比较来判断是哪个服务器;B
是服务器的地址;C
是该服务器(Follower)与集群中 Leader 服务器交换信息的端口;D
是当集群中的 Leader 服务器宕机时,用于重新选举新 Leader 的通信端口;
-
-
创建
myid
文件,配置服务器编号-
在
dataDir
对应的目录(即/data/zookeeper
)下创建myid
文件,文件内容为对应 IP 的 ZooKeeper 服务器编号(注意文件中不要有空格和空行);- 对于
zoo.cfg
中配置的server.1=192.168.65.163:2888:3888
,这台服务器(IP 为192.168.65.163
)的myid
文件里就填写 1; - 对于
server.2=192.168.65.184:2888:3888
,对应的服务器(IP 为192.168.65.184
)的myid
文件里填写 2; - 对于
server.3=192.168.65.186:2888:3888
,对应的服务器(IP 为192.168.65.186
)的myid
文件里填写 3;
- 对于
-
注意:
myid
文件一定要在 Linux 系统中创建,在 Notepad++ 等 Windows 编辑器中创建很可能出现乱码问题;
-
-
启动 ZooKeeper server 集群
-
启动前需要关闭防火墙(生产环境需要打开对应端口);
-
分别在三个节点执行
bin/zkServer.sh start
命令启动 ZooKeeper server; -
执行
bin/zkServer.sh status
命令查看集群状态,若显示某节点的模式为leader
,说明该节点成为了集群的 Leader 节点。
-
4.3.3 常见问题及解决
- 如果服务启动出现
java.net.NoRouteToHostException: 没有到主机的路由 (Host unreachable)
的异常; - 原因:
zoo.cfg
配置错误;- 防火墙未关闭;
- 解决方法:
- 检查
zoo.cfg
配置是否正确; - 关闭防火墙,在 CentOS 7 系统中,可通过以下命令:
systemctl status firewalld
:检查防火墙状态;systemctl stop firewalld
:关闭防火墙;systemctl disable firewalld
:禁止防火墙开机启动。
- 检查
4.4 四字命令
-
用户可以使用 ZooKeeper 四字命令获取 ZooKeeper 服务的当前状态及相关信息,在客户端通过
nc
(netcat)向 ZooKeeper 提交相应命令。首先需要安装nc
命令,在 CentOS 系统中,可通过yum install nc
进行安装; -
四字命令格式为:
echo [command] | nc [ip] [port]
- 即通过
echo
输出四字命令,再通过管道(|
)传递给nc
命令,由nc
连接到指定 IP 和端口的 ZooKeeper 服务来执行命令;
- 即通过
-
常用四字命令及功能
-
conf
:3.3.0 版本引入,打印出服务相关配置的详细信息; -
cons
:3.3.0 版本引入,列出所有连接到这台服务器的客户端全部连接/会话详细信息,包括“接受/发送”的包数量、会话 ID、操作延迟、最后的操作执行等信息; -
crst
:3.3.0 版本引入,重置所有连接的连接和会话统计信息; -
dump
:列出那些比较重要的会话和临时节点,这个命令只能在 leader 节点上有用; -
envi
:打印出服务环境的详细信息; -
reqs
:列出未经处理的请求; -
ruok
:测试服务是否处于正确状态,如果确实如此,服务返回“imok”,否则不做任何相应; -
stat
:输出关于性能和连接的客户端的列表; -
srst
:重置服务器的统计; -
srvr
:3.3.0 版本引入,列出连接服务器的详细信息; -
wchs
:3.3.0 版本引入,列出服务 watch 的详细信息; -
wchc
:3.3.0 版本引入,通过 session 列出服务器 watch 的详细信息,输出是一个与 watch 相关的会话的列表; -
wchp
:3.3.0 版本引入,通过路径列出服务器 watch 的详细信息,输出一个与 session 相关的路径; -
mntr
:3.4.0 版本引入,输出可用于检测集群健康状态的变量列表;
-
-
开启四字命令的方法
-
方法 1:在
zoo.cfg
文件里加入配置项4lw.commands.whitelist=*
,让这些指令放行; -
方法 2:在 ZK 的启动脚本
zkServer.sh
中新增放行指令# 添加 JVM 环境变量 -Dzookeeper.4lw.commands.whitelist=* ZOOMAIN="-Dzookeeper.4lw.commands.whitelist=* ${ZOOMAIN}"
-
-
具体命令示例
-
stat 命令:用于查看 ZK 的状态信息,例:
echo stat | nc 192.168.65.186 2181
-
ruok 命令:用于查看当前 ZK server 是否启动,若返回
imok
表示正常,例:echo ruok | nc 192.168.65.186 2181
-
4.5 ZooKeeper选举原理
-
ZooKeeper 的 Leader 选举过程基于投票和对比规则,确保集群中选出具有最高优先级的服务器作为 Leader 处理客户端请求。以服务启动期间的选举为例,选票格式为
vote=(myid,ZXID)
,通过多轮投票,依据特定规则确定 Leader; -
选举时遵循以下优先级规则来对比选票:
-
首先比较
epoch
,选取具有最大epoch
的服务器。epoch
用于区分不同的选举轮次,每次重新选举时epoch
会增加;epoch:选举周期
-
如果
epoch
相同,则比较zxid
(事务 ID),选取事务 ID 最大的服务器。zxid
表示最后一次提交的事务 ID,zxid
大的服务器包含的数据更新; -
如果
zxid
也相同,则比较myid
(服务器 ID),选取服务器 ID 最大的服务器;
-
-
源码:
-
totalOrderPredicate
方法用于判断新的候选(newId
、newZxid
、newEpoch
)是否比当前投票(curId
、curZxid
、curEpoch
)更优,返回true
表示新候选更优,否则为false
; -
判断逻辑与上述投票对比规则一致:
-
若
newEpoch > curEpoch
,新候选更优; -
若
newEpoch == curEpoch
,再看newZxid
和curZxid
:- 若
newZxid > curZxid
,新候选更优; - 若
newZxid == curZxid
,则比较newId
和curId
,newId > curId
时新候选更优;
- 若
-
/*** Check if a pair (server id, zxid) succeeds our* current vote.**/ protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {LOG.debug("id: {}, proposed id: {}, zxid: 0x{}, proposed zxid: 0x{}",newId,curId,Long.toHexString(newZxid),Long.toHexString(curZxid));if (self.getQuorumVerifier().getWeight(newId) == 0) {return false;}/** We return true if one of the following three cases hold:* 1- New epoch is higher* 2- New epoch is the same as current epoch, but new zxid is higher* 3- New epoch is the same as current epoch, new zxid is the same* as current zxid, but server id is higher.*/return ((newEpoch > curEpoch)|| ((newEpoch == curEpoch)&& ((newZxid > curZxid)|| ((newZxid == curZxid)&& (newId > curId))))); }
-
-
zxid
是一个 64 位的整数,由高 32 位的epoch
和低 32 位的counter
组成:-
epoch
:表示 ZooKeeper 服务器的逻辑时期(logical epoch),用于区分不同的 Leader 选举周期,每次新的 Leader 选举周期会更新epoch
; -
counter
:是一个在每个epoch
内递增的计数器,用于标识事务的顺序,保证同一epoch
内事务的有序性; -
工具类
ZxidUtils
提供了从zxid
中获取epoch
和counter
、根据epoch
和counter
生成zxid
以及将zxid
转换为字符串等方法,方便对zxid
进行操作和解析;
public class ZxidUtils {public static long getEpochFromZxid(long zxid) {return zxid >> 32L;}public static long getCounterFromZxid(long zxid) {return zxid & 0xffffffffL;}public static long makeZxid(long epoch, long counter) {return (epoch << 32L) | (counter & 0xffffffffL);}public static String zxidToString(long zxid) {return Long.toHexString(zxid);}}
-