Zookeeper 的概述、安装和配置

概述

ZK是什么

Zookeeper 是一个开源的分布式的,为分布式应用提供协调服务的Apache项目。Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应,从而实现集群中类似Master/Slave管理模式。Zookeeper=文件系统+通知机制


ZK的特点

架构角色与职责

  • 一主多从:集群由一个 Leader 和多个 Follower 组成。
  • Leader:集群的“大脑”,负责发起投票、处理写请求(更新系统状态)。
  • Follower:负责处理读请求,并将写请求转发给 Leader;参与 Leader 选举和写请求的投票过程。
  • Observer:不参与投票的 Followers。用于在不影响写性能的情况下,线性扩展集群的读性能。

存活与容错机制:

  • 半数机制 (Quorum):集群只要有半数以上节点存活即可服务。
  • 奇数部署 (Best Practice):
    • 原因1:容错性价比。3 台和 4 台机器都只能挂 1 台,但 3 台更省资源。
    • 原因 2:防止脑裂(Split-brain)。确保在网络分区时,只有一个分区能凑齐半数以上节点选出 Leader。

数据一致性保障

  • 全局数据一致 (Single System Image):每个 Server 内存中都维护着同样的数据树。无论 Client 连接哪台 Server,看到的视图基本一致。
  • 原子性 (Atomicity):基于 ZAB 协议,一次更新要么全集群成功,要么全失败,没有中间状态。
  • 实时性 (Eventual Consistency):ZK 保证的是最终一致性。Client 在极短时间内能读到最新数据(若需强一致读,需在读取前调用 sync())。

顺序性与并发控制 (核心亮点)

  • 顺序性 (Sequential Consistency):

    • 全局顺序:Leader 会为每个写请求分配一个全局唯一的递增事务 ID(zxid)。
    • 客户端顺序:来自同一个 Client 的更新请求按其发送顺序执行。
  • 乐观锁机制 :通过 dataVersion 实现,这是应用系统处理抢占式资源的基础。

内存级性能(High Throughput)

ZooKeeper 的全量数据都存储在内存中。

  • 优点:读性能极高(QPS 可达数十万)。
  • 代价:不适合存储大数据。单个 ZNode 数据上限默认为 1MB,通常建议保持在 1KB 以内。

临时节点与会话绑定(Ephemeral Nodes)

这是 ZK 实现分布式协调的杀手锏。

  • 节点的生命周期与客户端的 Session 绑定。
  • 应用掉线后,注册节点自动消失,实现自动化的服务剔除。

监听机制 (Watch Mechanism)

  • 推拉结合。ZK Server 通知客户端 “数据变了”,客户端再主动来拉取。
  • 极大地减少了客户端轮询对网络带宽的消耗,实现即时配置同步。

事务日志与快照(Persistence)

  • 虽然数据在内存,但每一笔写操作都会先记录事务日志 (Transaction Log),并定期生成数据快照 (Snapshot)。
  • 保证了服务器重启后数据能够 100% 恢复。


ZK节点及其类型

图2

ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个ZNode。
每一个ZNode默认能够存储1MB的数据(这是官方要求。整个树状的目录结构全部都放在内存中提升吞吐效率),每个ZNode都可以通过其路径唯一标识。节点有两种类型:

  • 短暂(ephemeral): 客户端和服务器端断开连接后,创建的节点自己删除
  • 持久(persistent): 客户端和服务器端断开连接后,创建的节点不删除

四种形式:

  • 持久化目录节点(PERSISTENT) :客户端与 zookeeper 断开连接后,该节点依旧存在。
  • 持久化顺序编号目录节点(PERSISTENT_SEQUENTIAL):客户端与 zookeeper 断开连接后,该节点依旧存在,只是 Zookeeper 给该节点名称进行顺序编号。
  • 临时目录节点(EPHEMERAL):客户端与 zookeeper 断开连接后,该节点被删除。
  • 临时顺序编号目录节点(EPHEMERAL_SEQUENTIAL):客户端与 zookeeper 断开连接后,该节点被删除,只是 Zookeeper 给该节点名称进行顺序编号。

关于有序节点:

  • 创建 znode 时设置顺序标识,znode 名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
  • 在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序


ZK的应用场景

提供的服务包括:

  • 分布式API目录:典型的应用著名的Dubbo分布式框架就是应用了ZooKeeper分布式的JNDI能力。在Dubbo中,使用ZooKeeper维护全局的服务接口API地址列表。大致的思路为:
    • 服务提供者(Provider)在启动的时候向ZK上的指定节点写 入自己的API地址,这个操作就相当于服务的公开。类似的API地址节点为:/dubbo/${serviceName}/providers
    • 服务消费者(Consumer)启动的时候,订阅节 点/dubbo/{serviceName}/providers下的Provider服务提供者URL地 址,获得所有访问提供者的API。
  • 分布式ID生成器:实际生产中为了防止zk压力过大,使用的是ID的分段机制,例如美团的 Leaf-ZK 方案,ZK 不负责生成每一个 ID,而是负责管理ID段。ZK存一个持久节点/id_generator,内容为1000。业务服务器 A 启动时,去 ZK 把这个值改为2000(利用 dataVersion 乐观锁确保安全),服务器 A 拿到了 1001~2000 这一千个 ID,在本地内存中利用 AtomicLong 自增分发,当服务器 A 这一千个 ID 用完了,再去 ZK 领下一批。当然也可以利用Redis的原子操作INCR和 INCRBY生成全局唯一的ID。
    • 使用zk生成全局ID:千万不要用 ZK 的“顺序节点”直接生成每一个 ID,那会拖垮 ZK。ZK 仅作为“发号器”的管理者。每台业务服务器从 ZK 申请一个“号段”(如 1000~2000),然后由服务器在本地内存中通过 AtomicLong 自增。这种方式的优点是数据强一致(CP),ZK 的事务日志和过半协议保证了申请记录绝对不会丢失,ID 绝对不会重复,只要 ZK 集群半数以上存活,号段分发就不会停。但是这种方式复杂度也高,需要处理本地缓存用尽后的同步预取逻辑,如果号段设置不当,频繁访问 ZK 会产生性能瓶颈。比较适用于订单号、支付流水类的对重复ID零容忍的场景,强烈推荐 ZK + Snowflake (最强结合)
    • 使用Redis的原子操作INCRs生成全局ID:优点是性能极高,纯内存操作,单机支撑万级甚至十万级 QPS 毫无压力,开发也简单,几行代码即可实现。Redis 默认的 RDB 持久化会丢失几分钟数据,即使开启 AOF(建议设置 everysec),在极端宕机情况下仍可能丢失 1 秒的数据,从而导致 ID 重复。比较适用于点赞数、短链接、非核心流水号等场景。
  • 分布式节点的命名:一个分布式系统通常会由很多节点组成,而且节点的数量不是固定的,是不断动态变化的。比如当业务不断膨胀和流量洪峰到来时,可能会动态加入大量的节点到集群中。一旦流量洪峰过去,就需 要下线大量的节点。再比如说,由于机器或者网络的原因,一些节点会主动离开集群。如何为大量的动态节点命名呢?一种简单的办法是,通过配置文件手动进行每一个节点的命名。如果节点数据量太大,或者说变动频 繁,手动命名是不现实的,这就需要用到分布式节点的命名服务。

  • 统一配置管理:将配置信息配置到zk某个目录下,监听者监听配置信息是否发生变化,若发生变化则立刻同步

  • 软负载均衡:存储每个每个服务节点的访问数,进行软负载均衡
  • 分布式锁:获取 xxx/locknode下的有序节点列表,判断自己创建的节点是否是序号最小的,是则获取锁,否则监听比自己小一号的节点。
    • 优点:ZooKeeper分布式锁(如InterProcessMutex)能有效地解决分布式问题、不可重入问题,使用起来较为简单。
    • 缺点:ZooKeeper实现的分布式锁性能不太高。因为每次在创建锁和释放锁的过程中都要动态创建、销毁瞬时节点。在 ZooKeeper中,创建和删除节点只能通过Leader服务器来执行, 然后Leader服务器还需要将数据同步到所有的Follower机器 上,这样频繁的网络通信,性能的短板是非常突出的。
    • 在高性能、高并发的场景下,不建议使用ZooKeeper的分布式锁。由于ZooKeeper具有高可用特性,因此在并发量不是太高的场景推荐使用ZooKeeper的分布式锁。


ZKLeader的选举机制

核心选举指标

简单说就是这个 Epoch, ZXID, SID 三元组。当服务器处于 LOOKING 状态时,每台机器都会发出一个投票(Vote),这个投票本质上是一张名片,上面写着:我的逻辑时钟,我的数据 ID,我的服务器 ID。以下是选举权重的优先级排位:

  • 逻辑时钟 (Epoch/Election Epoch)最高优先级。 就像选票的“版本号”。如果你的投票版本比我旧,我直接不理你;如果你的比我新,我立刻更新自己的版本并听你的。
  • 数据 ID (ZXID)第二优先级。 谁的 ZXID 越大,说明谁持有的数据越新。数据更新、最全的服务器理应更有资格当 Leader(保证 CP 系统的强一致性)。
  • 服务器 ID (SID)最后的底牌。 如果大家的逻辑时钟和数据版本都一模一样(通常是刚开机时),那就看 SID,数字大的赢。


选举状态机

在选举过程中,每个节点都在以下四个状态间切换:


选举场景

场景一:集群启动(白手起家)

假设有 3 台服务器(SID 为 1、2、3),按顺序启动:

  • Server 1 启动:给自己投一票。目前票数 $1/3$,没过半,状态为 LOOKING。
  • Server 2 启动:给自己投一票,并与 Server 1 交换信息。
    • PK 开始:Server 2 发现自己的 SID (2) > Server 1 的 SID (1)。
    • 结果:Server 1 “认输”,改投 Server 2。
    • 统计:Server 2 现在有 2 票(自己的一票 + 1 的一票),超过半数 (2/3)!
    • 定局:Server 2 成为 LEADING,Server 1 变为 FOLLOWING。
  • Server 3 启动:发现江湖已有老大(Server 2),直接认怂,变为 FOLLOWING。


场景二:运行中 Leader 宕机(中途换届)

这是最体现 ZXID 价值的时候。假设 Leader (Server 2) 突然挂了:

  • 状态切换:Server 1 和 Server 3 发现 Leader 失联,立即将状态改为 LOOKING
  • 亮底牌:Server 1 发出 (Epoch=2, ZXID=100, SID=1);Server 3 发出 (Epoch=2, ZXID=105, SID=3)。
  • PK 结果:虽然 Server 3 的 SID 大,但核心胜出点在于 ZXID (105 > 100)。这意味着 Server 3 掌握了更多尚未同步的事务。最终 Server 3 毫无疑问成为新 Leader。


ZK的安装配置和常用命令

ZK的安装和配置

在安装 ZK 之前,请确保基础环境就绪:

  • JDK:建议安装 JDK 17(ZK 3.8 对高版本支持很好)。
  • 防火墙:确保三台机器之间 2181(客户端连接)、2888(集群内通信)、3888(选举专用)端口互通。
  • 用户:建议创建专门的 zookeeper 用户,避免使用 root 运行。
1
2
3
4
5
6
7
8
9
10
11
12
$ tar -zxvf /opt/apache-zookeeper-3.8.6-bin.tar.gz
$ ln -s /opt/apache-zookeeper-3.8.6-bin zookeeper

# 配置 myid 文件(具体位置参考zoo.cfg配置文件):这是 ZK 区分集群节点的唯一标识,每台机器的值必须不同。
# 在官方文档中,myid 文件的内容只要是一个 1 到 255 之间的整数即可,这是一个历史惯例。
# 在实际的分布式架构中,如果需要超过255个节点的需求,处理方式通常不是去寻找更大的ID范围,而是从架构层面进行重构。
# 如果你需要255个甚至更多的节点来处理读请求,你绝对不应该让它们全部参与投票。
# 正常的做法是保持3、5或7个节点作为 Participant(参与者),剩下的几百个节点全部配置为 Observer。
# Observer 不参与选举和写投票,它们只是同步数据并响应读请求。这样你可以在不牺牲写性能的前提下,水平扩展出成百上千个节点。
在 192.168.1.149 上: echo "1" > /var/lib/zookeeper/myid
在 192.168.1.166 上: echo "2" > /var/lib/zookeeper/myid
在 192.168.1.224 上: echo "3" > /var/lib/zookeeper/myid

生产级配置文件 /opt/zookeeper/conf/zoo.cfg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 基础心跳配置
## 配置单元时间。单元时间是ZooKeeper的时间计算单元,其他的时间间隔都是使用tickTime的倍数来表示的。
## 如果不进行配置,则单元时间默认值为3000,单位是毫秒(ms)。
tickTime=2000
## 节点的初始化时间。该参数用于Follower(从节点)启动,并完成从Leader(主节点)同步数据的时间。
## Follower节点在启动过程中会与Leader建立连接并完成对数据的同步,从而确定自己的起始状态。
## Leader节点允许Follower在initLimit内完成这个工作。该参数的默认值为10,表示是参数tickTime值的10倍。
initLimit=10
## 心跳最大延迟周期。该参数用于配置Leader服务器和Follower之间进行心跳检测的最大延时时间。
## 在ZooKeeper集群运行的过程中,Leader服务器会通过心跳检测来确定Follower服务器是否存活。
## 如果Leader服务器在syncLimit时间内无法获取到Follower的心跳检测响应,那么Leader就会认为
## 该Follower已经脱离了与自己的同步。该参数默认值为5,表示是参数tickTime值的5倍。
syncLimit=5

# 数据与日志路径(建议物理隔离)
dataDir=/var/lib/zookeeper #数据目录,myid文件处于此目录下
dataLogDir=/var/log/zookeeper #日志目录,没配置默认将使用和dataDir相同的设置。

# 客户端连接端口
clientPort=2181

# 生产环境性能调优
maxClientCnxns=60
preAllocSize=65536
snapCount=100000

# 自动清理快照和日志文件(生产环境必配,保留3个,每小时清理一次)
autopurge.snapRetainCount=3
autopurge.purgeInterval=1

# 禁用 AdminServer(除非需要 Jetty 监控页面,否则建议关掉以节省资源和提高安全)
admin.enableServer=false

# 集群节点配置 (server.id=IP:通信端口:选举端口)
# 在ZooKeeper集群中,每个节点都需要感知到整个集群是由哪些节点组成的,所以每个配置文件都需要配置全部节点。
# 在“.cfg”配置文件中可以使用“server.id”格式进行节点的配置,每一行都代表一个节点。
# 一般2888用于节点之间的通信,3888用于选举Leader的通信
server.1=192.168.1.149:2888:3888
server.2=192.168.1.166:2888:3888
server.3=192.168.1.224:2888:3888

VM 参数调优 (java.env):在 conf 目录下新建 java.env 文件。ZK 是内存敏感型组件,必须锁定堆内存避免 Swap。

1
2
3
# 假设你的服务器是 8G 内存,建议分配 2-4G 给 ZK
# export JAVA_HOME=/your/java/path
export JVMFLAGS="-Xms2048m -Xmx2048m -Xmn1024m -XX:+UseG1GC -XX:+ParallelRefProcEnabled -Dcom.sun.management.jmxremote"

依次在三台机器上启动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ systemctl stop firewalld
$ systemctl disable firewalld
$ zcall systemctl stop firewalld

$ zcall /opt/zookeeper/bin/zkServer.sh start
-------------host01---------------
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... already running as process 2180.
-------------host02---------------
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
-------------host03---------------

$ zcall /opt/zookeeper/bin/zkServer.sh status
-------------host01---------------
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower
-------------host02---------------
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: leader
-------------host03---------------
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost. Client SSL: false.
Mode: follower


常用客户端命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 客户端连接与管理
$ zkCli.sh
$ zkCli.sh -server 192.168.1.166:2181
$ zkCli.sh -timeout 5000 -r -server 192.168.1.166:2181 #单位ms,-r只读

# 显示所有命令
[zk: localhost:2181(CONNECTED) 0] help
ZooKeeper -server host:port -client-configuration properties-file cmd args
addWatch [-m mode] path # optional mode is one of [PERSISTENT, PERSISTENT_RECURSIVE] - default is PERSISTENT_RECURSIVE
addauth scheme auth
close
config [-c] [-w] [-s]
connect host:port
create [-s] [-e] [-c] [-t ttl] path [data] [acl]
delete [-v version] path
deleteall path [-b batch size]
delquota [-n|-b|-N|-B] path
get [-s] [-w] [-b] [-x] path
getAcl [-s] path
getAllChildrenNumber path
getEphemerals path
history
listquota path
ls [-s] [-w] [-R] path
printwatches on|off
quit
reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
redo cmdno
removewatches path [-c|-d|-a] [-l]
set path data [-s] [-v version] [-b]
setAcl [-s] [-v version] [-R] path acl
setquota -n|-b|-N|-B val path
stat [-w] path
sync path
version
whoami

节点操作基础

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 创建节点
# create [-s] [-e] path data acl
# -s:表示当前创建的节点是一个顺序节点
# -e:表示当前创建的节点是一个临时节点,临时节点下面不能再创建子节点,并且当前会话消失后该临时节点被删除
# -s和-e两个参数可以同时使用
create /node
create /node/im-149 -e #创建临时节点(会话关闭,节点被删除)
create -c /container c1 #容器节点,当子节点全被删除,该节点会自动被ZK清理,适合管理临时组
create -t ttl /xxx dataxx #带过期时间的节点,需配置 extendedTypesEnabled=true,可用于定时任务锁

# 顺序节点
create -s /snode 1
create -s /snode 2
ls /
[.., snode0000000006, snode0000000007, ..]

# ls
ls /node

# get
get -s /node/im-149
null
cZxid = 0x100000012 #节点创建的事务ID,全局唯一,全局有序
mZxid = 0x100000012 #修改时的事务ID
pZxid = 0x100000012 #子节点的最后一次创建或者修改时间,与孙子节点无关。
ctime = Mon Apr 20 10:11:47 CST 2026 #节点创建时的时间戳
mtime = Mon Apr 20 10:11:47 CST 2026 #节点最新一次更新发生时的时间戳。
dataVersion = 0 #数据版本号
cversion = 0 #子节点版本号
aclVersion = 0 #节点的ACL权限修改版本号
ephemeralOwner = 0x2000032ab330000 ##记录了谁拥有这个临时节点,对于持久节点恒为0x0
dataLength = 0
numChildren = 0

# set
set /node/im-149 sever149 -v 0 #-v乐观锁写入,更新配置时传入上一次的 dataVersion,防止并发冲突

# delete
delete /banzhang/id #该节点没有子节点的情况下才能被删除
deleteall xxx #递归删除,类似 rm -rf,清理旧业务数据时比 delete 好用

# stat
stat /node/im-149
cZxid = 0x100000012
mZxid = 0x100000021
pZxid = 0x100000012
ctime = Mon Apr 20 10:11:47 CST 2026
mtime = Mon Apr 20 10:31:38 CST 2026
cversion = 0
dataVersion = 6
aclVersion = 0
ephemeralOwner = 0x2000032ab330000
dataLength = 8
numChildren = 0

# 监听
ls /banzhang -w # 监听本路径下子节点的创建和删除(一次有效),注意不包含子节点值的变化和孙子节点的任何变化
get /node/im-149 -w # 监听本节点值的变化和自身删除事件,注意watcher使用一次后需再次注册才有效,该监听不包括子节点的任何变化

# mode 为 PERSISTENT 持久化监听,触发后不会自动失效。PERSISTENT_RECURSIVE 为深度递归监听,包括该路径下所有孙子节点的变化。
# 通常用于分布式配置中心,一旦 /config 目录下任何内容变动,客户端都能收到。
addWatch -m {mode} /xxx/xx

# 非必要的 PERSISTENT_RECURSIVE 监听会消耗服务器大量的内存和网络带宽,建议在配置同步完成后,及时使用 removewatches 清理掉不常使用的路径监听。
# -c (Children):仅删除子节点列表变化的监听(对应 NodeChildrenChanged)。
# -d (Data):仅删除节点数据变化的监听(对应 NodeDataChanged)。
# -a (Any):删除指定路径上的所有类型监听(这是最常用的清理方式)。
# -l (Local):如果连接断开,仅在本地删除该监听记录(不通知服务器)。
removewatches -a /xxx/xxx

# 在删除之前,你可能需要确认当前会话到底挂载了多少监听器。
printwatches on # 开启打印,当触发或删除时会看到提示


ZK权限管理(ACL)

传统的文件系统中,ACL分为两个维度,一个是属组,一个是权限,子目录/文件默认继承父目录的ACL。而在Zookeeper中,node的ACL是没有继承关系的,是独立控制的。Zookeeper的ACL,可以从三个维度来理解:一是scheme; 二是user; 三是permission,通常表示为 scheme:id:permissions, 下面从这三个方面分别来介绍:

  • scheme:zk3 缺省支持下面几种scheme:
    • ip:它对应的id为客户机的IP地址,设置的时候可以设置一个ip段,比如ip:192.168.1.0/16, 表示匹配前16个bit的IP段
    • digest:它对应的id为username:BASE64(SHA1(password)),它需要先通过username:password形式的authentication
    • auth:它不需要id, 只要是通过authentication的user都有权限(zookeeper支持通过kerberos来进行authencation, 也支持username/password形式的authentication)
    • super:在这种scheme情况下,对应的id拥有超级权限,可以做任何事情(crwda)
    • world:它下面只有一个id, 叫anyone, world:anyone代表任何人,zookeeper中对所有人有权限的结点就是属于world:anyone的
    • sasl:zk3还提供了对sasl的支持,不过缺省是没有开启的,需要配置才能启用。sasl的对应的id,是一个通过sasl authentication用户的id,zk3中的sasl authentication是通过kerberos来实现的,也就是说用户只有通过了kerberos认证,才能访问它有权限的node.
  • id:id与scheme是紧密相关的,具体的情况在上面介绍scheme的过程都已介绍,这里不再赘述。
  • permission:zookeeper目前支持下面一些权限
    • CREATE(c): 创建权限,可以在在当前node下创建child node
    • READ(r): 读权限,可以获取当前node的数据,可以list当前node所有的child nodes
    • WRITE(w): 写权限,可以向当前node写数据
    • DELETE(d): 删除权限,可以删除当前的node
    • ADMIN(a): 管理权限,可以设置当前node的permission
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#创建数据节点时设置acl
#create [-s] [-e] path data acl
> create /znode02 456 ip:172.16.127.131:crw
Created /znode02

> create /znode03 0 digest:tom:Aasasla+azxasU=:crwda #user:BASE64(SHA1(password))
Created /znode03

#获取当前节点的acl信息
> getAcl /znode02
'ip,'172.16.127.131
: crw

#设置节点的acl
#setAcl path acl
> setAcl /znode03 ip:172.16.127.131:crwd
cZxid = 0x4140000008f
...
aclVersion = 1 #aclVersion递增1
...
numChildren = 0

#添加认证以对节点数据进行访问
#addauth scheme auth
> addauth digest tom:123456

#当设置了znode权限,但是密码忘记了怎么办?还好Zookeeper提供了超级管理员机制
#修改zkServer.sh,加入super权限设置(只对当前zk集群节点)
> vim zkServer.sh
-----------------------------------------------
98 _ZOO_DAEMON_OUT="$ZOO_LOG_DIR/zookeeper.out"
99 SUPER_ACL="-Dzookeeper.DigestAuthenticationProvider.superDigest=super:gG7s8t3oDEtIqF6DM9LlI/R+9Ss="
100
101
102 case $1 in
...
111 nohup "$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" "$SUPER_ACL"\
112 -cp "$CLASSPATH" $JVMFLAGS $ZOOMAIN "$ZOOCFG" > "$_ZOO_DAEMON_OUT" 2>&1 < /dev/null &
113 if [ $? -eq 0 ]
...
-----------------------------------------------

#重新启动Zookeeper,这时候使用 addauth digest super:super 进行认证
> ./zkServer.sh restart
> ./zkCli.sh

> get /znode02
Authentication is not valid : /znode02

> addauth digest super:super
> get /znode02
456
cZxid = 0x4140000008c
...
numChildren = 0

> delete /znode02


ZK的监听原理

传统监听机制

传统监听机制的三大核心特性:

  • 一次性触发:无论是 get -w 还是 ls -w,一旦数据发生变化,ZooKeeper 就会向客户端发送一个通知,然后该 Watcher 就会被立即从服务端的注册列表中删除。如果你想持续监听,必须在处理完事件后手动重新注册。
  • 轻量级通知:ZooKeeper 的通知非常吝啬,它只会告诉你数据变了或子节点变了,但不会告诉你具体变成了什么。客户端收到通知后,必须再次发起一次请求(如 get)才能拿到新数据。
  • 客户端串行执行:客户端的 process() 方法是由一个专门的 EventThread 调用的。这意味着如果你在 process() 里写了耗时很长的逻辑(比如复杂的业务计算或阻塞 IO),会卡死所有的监听回调。

监听的整个流程:

  • 第一阶段:注册(逻辑上的“标记”)。客户端调用 getData(“/path”, true),请求发给 ZK Server,Server 会在对应的 ZNode 上记录下 “Session ID 为 0x123 的哥们想看这个节点的变化”。
  • 第二阶段:触发(服务端的“广播”)。另一个客户端修改了 “/path” 的数据。ZK Server 检查这个路径上的监听列表,发现 0x123 注册过。Server 发送一个 WatchedEvent 给客户端。发送完后,Server 会立刻把 0x123 从这个路径的监听名单中踢出去。
  • 第三阶段:回调(客户端的“反应”)。客户端的 SendThread 接收到事件,将其放入 EventQueue。EventThread 从队列里取出事件,执行你写的 process()。


新增的 addWatch

新增的addWatch (3.6+) 改变了游戏规则,它是对你图中传统监听模式的重大升级:

在传统模式下,你收到通知 $\rightarrow$ 调用 get 获取数据 $\rightarrow$ 顺便再次注册。在“收到通知”和“再次注册”之间有一个微小的时间空隙。如果在这个瞬间数据又变了,你是收不到第二次通知的。重要业务(如集群配置)务必使用 addWatch 的持久模式。


ZK写数据的流程

写数据的过程

从 CAP 定理的角度来看,ZooKeeper 是典型的 CP 系统(强一致性 + 分区容错性)。

  • Client 向 ZooKeeper 的 Server1 上写数据,发送一个写请求。
  • 如果Server1不是Leader,那么Server1 会把接受到的请求进一步转发给Leader,因为每个ZooKeeper的Server里面有一个是Leader。
    这个Leader 会将写请求广播给各个Server,比如Server1和Server2,各个Server写成功后就会通知Leader。
  • 当Leader收到大多数 Server 数据写成功了,那么就说明数据写成功了。如果这里三个节点的话,只要有两个节点数据写成功了,那么就认为数据写成功了。写成功之后,Leader会告诉Server1数据写成功了。
  • Server1会进一步通知 Client 数据写成功了,这时就认为整个写操作成功。ZooKeeper 整个写数据流程就是这样的。

关于写的过程,在生产环境下,还有两个关键点决定了 ZK 的写性能:

  • 事务日志与快照(The WAL):当 Server1、Server2 收到 Leader 的写请求时,它们并不是直接写到内存就完了。每个 Server 必须先将这个写操作记录到事务日志(Transaction Log)中并持久化。只有磁盘写入成功后,才会给 Leader 发 ACK。这就是为什么我们之前建议你将 ZK 的 dataLogDir 放在独立 SSD 上的原因——磁盘 IO 的快慢直接决定了你写数据的延迟。
  • 两阶段提交(Two-Phase Commit, 2PC):ZK 的写流程实际上是一个简化的 2PC。第一阶段 (Proposal) 是 Leader 发起提议,询问大家能不能写。第二阶段 (Commit) 是半数以上回复 OK 后,Leader 再次广播 Commit 指令,所有人正式提交。


为什么 ZK 选择 CP?

在分布式系统的三要素中,ZooKeeper 的取舍如下:

  • Consistency (一致性 - C): ZK 追求的是最终一致性(严格来说是顺序一致性)。Leader 必须收到半数以上(Quorum)节点的确认(ACK)才会提交事务。这意味着任何时刻,只要集群正常工作,你读到的数据要么是最新的,要么是正在同步中的,绝不会出现长期的逻辑冲突。
  • Partition Tolerance (分区容错性 - P): 这是分布式系统的底线。在网络发生分区时(比如 149 和 166、224 断开),ZK 集群必须能继续生存。
  • Availability (可用性 - A) - 被牺牲项: 这是 ZK 最被外界“诟病”但也最自豪的地方。在 Leader 选举期间,整个 ZK 集群是不可用的。如果我们的 149(Leader)宕机了,166 和 224 会立即开始选举。在选举出新 Leader 之前的几十秒内,集群不接受任何读写请求。为了保证数据绝对不错,它宁愿暂时“罢工”。


ZK的读性能

虽然说 ZK 是 CP,但它的读操作非常快,甚至表现得像 AP,因为 Client 连接到哪个 Server,就从哪个 Server 直接读数据,不需要经过 Leader。这导致了 ZK 的读是 “模糊” 的。如果 Leader 刚写完,但同步给 Server2 的包还在路上,你从 Server2 读到的可能是旧值。如果你在代码中要求必须读到最新值,需要在读之前先调用 sync() 命令。


代码示例

参考:https://github.com/kinglyjn/zdemo-zookeeper