Zookeeper 使用教程:从入门到实践
目录
什么是 Zookeeper?
Zookeeper 是一个开源的、分布式的、为分布式应用提供协调服务的高性能、高可用、具有严格顺序访问控制能力的分布式协调服务。

你可以把它想象成一个分布式的小型文件系统,或者一个内存中的数据库,它专门用来存储和管理一些关键的数据,并为分布式系统中的各个节点提供同步、配置管理、命名和分组等服务。
主要特点:
- 高可用性:通常部署在奇数台(如 3, 5, 7 台)服务器上,只要集群中半数以上的节点存活,整个服务就可用。
- 数据一致性:所有客户端看到的数据模型都是一致的,数据更新是原子性的。
- 数据模型:类似于文件系统的树形结构,由一系列被称为
znode的数据节点组成。 - Watcher 机制:客户端可以对某个
znode设置一个“监视器”(Watcher),当该znode的数据或子节点发生变化时,Zookeeper 会异步通知设置了监视器的客户端。 - 顺序性:对于来自同一个客户端的请求,Zookeeper 会按照其发送的顺序进行处理。
核心概念
在学习使用之前,必须理解 Zookeeper 的几个核心概念:
1 Znode (数据节点)
Zookeeper 的数据存储单元,类似于文件系统中的文件或目录,每个 znode 都可以存储数据,并且可以拥有子节点。

znode 有几种类型,决定了它们的行为:
- PERSISTENT (持久节点):最普通的节点,创建后,会一直存在于 Zookeeper 服务器上,直到被手动删除。
- EPHEMERAL (临时节点):生命周期与客户端的会话绑定,客户端会话结束(如客户端崩溃或网络断开),该节点会被自动删除,不能有子节点。
- PERSISTENT_SEQUENTIAL (持久顺序节点):与持久节点类似,但在节点名后会被自动追加一个 10 位的单调递增序号。
- EPHEMERAL_SEQUENTIAL (临时顺序节点):与临时节点类似,节点名后也会被追加一个 10 位的单调递增序号。
2 版本
每个 znode 都会有一个数据版本(dataVersion)、子节点版本(cversion)和自身版本(aclVersion),每次对 znode 的数据修改都会使其 dataVersion 加 1,这个机制可以用于乐观锁,在更新数据时检查版本号,如果版本不匹配则更新失败。
3 Watcher (事件监听器)
客户端可以在读取 znode 数据时(getData)或获取子节点列表时(getChildren)设置一个 Watcher。
- 一次性:Watcher 是一次性的,触发一次后就会被自动移除,如果需要持续监听,必须在事件回调中重新设置。
- 异步通知:当被监视的
znode发生变化时,服务器会向客户端发送一个事件通知。 - 事件类型:包括节点创建、节点删除、节点数据变更、子节点列表变更等。
4 ACL (Access Control List)
访问控制列表,用于控制对 znode 的访问权限,权限包括:

CREATE: 创建子节点READ: 读取节点数据和子节点列表WRITE: 更新节点数据DELETE: 删除节点ADMIN: 设置 ACL
环境准备与安装
我们以 Linux 环境为例,演示两种最常用的安装模式。
1 单机模式安装
适合开发测试环境。
步骤:
-
下载 Zookeeper
# 官网下载地址: https://zookeeper.apache.org/releases.html wget https://archive.apache.org/dist/zookeeper/zookeeper-3.8.3/apache-zookeeper-3.8.3-bin.tar.gz
-
解压
tar -zxvf apache-zookeeper-3.8.3-bin.tar.gz mv apache-zookeeper-3.8.3-bin /usr/local/zookeeper
-
配置
- 进入 Zookeeper 的
conf目录。 - 将
zoo_sample.cfg复制为zoo.cfg。cd /usr/local/zookeeper/conf cp zoo_sample.cfg zoo.cfg
- 编辑
zoo.cfg,主要修改dataDir配置项,指定数据存储目录。vim zoo.cfg # 修改或添加以下配置 dataDir=/usr/local/zookeeper/data # (可选) 修改客户端连接端口,默认2181 # clientPort=2181
- 进入 Zookeeper 的
-
创建数据目录
mkdir -p /usr/local/zookeeper/data
-
启动 Zookeeper
# 回到 bin 目录 cd /usr/local/zookeeper/bin # 启动服务 ./zkServer.sh start
-
检查状态
./zkServer.sh status # 应该输出类似信息: # Zookeeper running (pid=12345) # Mode: standalone
-
连接客户端
./zkCli.sh # 连接成功后,会进入命令行界面,显示 [zk: localhost:2181(CONNECTED) 0]
2 伪集群模式安装
在一台物理机上模拟一个 Zookeeper 集群,适合学习和测试集群功能。
步骤:
-
准备工作:完成单机模式安装的前两步(下载、解压)。
-
创建集群目录和数据目录
# 创建集群工作目录 mkdir -p /usr/local/zookeeper-cluster # 进入并创建三个节点的目录 cd /usr/local/zookeeper-cluster mkdir zk1 zk2 zk3 # 为每个节点创建数据目录和日志目录 mkdir zk1/data zk1/logs mkdir zk2/data zk2/logs mkdir zk3/data zk3/logs
-
配置每个节点
-
复制 Zookeeper 安装目录到三个节点目录中。
cp -r /usr/local/zookeeper/* zk1/ cp -r /usr/local/zookeeper/* zk2/ cp -r /usr/local/zookeeper/* zk3/
-
分别修改三个节点的
zoo.cfg文件。 -
修改 zk1 的配置 (
/usr/local/zookeeper-cluster/zk1/conf/zoo.cfg)dataDir=/usr/local/zookeeper-cluster/zk1/data dataLogDir=/usr/local/zookeeper-cluster/zk1/logs clientPort=2181 # 集群配置 server.1=127.0.0.1:2888:3888 server.2=127.0.0.1:2889:3889 server.3=127.0.0.1:2890:3890
-
修改 zk2 的配置 (
/usr/local/zookeeper-cluster/zk2/conf/zoo.cfg)dataDir=/usr/local/zookeeper-cluster/zk2/data dataLogDir=/usr/local/zookeeper-cluster/zk2/logs clientPort=2182 # 集群配置 server.1=127.0.0.1:2888:3888 server.2=127.0.0.1:2889:3889 server.3=127.0.0.1:2890:3890
-
修改 zk3 的配置 (
/usr/local/zookeeper-cluster/zk3/conf/zoo.cfg)dataDir=/usr/local/zookeeper-cluster/zk3/data dataLogDir=/usr/local/zookeeper-cluster/zk3/logs clientPort=2183 # 集群配置 server.1=127.0.0.1:2888:3888 server.2=127.0.0.1:2889:3889 server.3=127.0.0.1:2890:3890
-
-
创建 myid 文件
myid文件用于标识当前服务器在集群中的 ID,必须与zoo.cfg中server.X的X对应。- 在 zk1 的 data 目录下创建
myid文件,内容为1。echo "1" > /usr/local/zookeeper-cluster/zk1/data/myid
- 在 zk2 的 data 目录下创建
myid文件,内容为2。echo "2" > /usr/local/zookeeper-cluster/zk2/data/myid
- 在 zk3 的 data 目录下创建
myid文件,内容为3。echo "3" > /usr/local/zookeeper-cluster/zk3/data/myid
- 在 zk1 的 data 目录下创建
-
启动集群
# 分别启动三个节点 /usr/local/zookeeper-cluster/zk1/bin/zkServer.sh start /usr/local/zookeeper-cluster/zk2/bin/zkServer.sh start /usr/local/zookeeper-cluster/zk3/bin/zkServer.sh start
-
检查集群状态
# 检查 zk1 /usr/local/zookeeper-cluster/zk1/bin/zkServer.sh status # 应该输出: Mode: leader # 检查 zk2 /usr/local/zookeeper-cluster/zk2/bin/zkServer.sh status # 应该输出: Mode: follower # 检查 zk3 /usr/local/zookeeper-cluster/zk3/bin/zkServer.sh status # 应该输出: Mode: follower
集群中会有一个 Leader 和多个 Follower。
Zookeeper Shell 常用命令
使用 zkCli.sh 进入客户端后,可以执行以下命令:
| 命令 | 示例 | 描述 |
|---|---|---|
ls |
ls / |
列出根目录下的所有节点 |
ls -w |
ls -w / |
列出节点并设置 Watcher |
create |
create /app "my_app_data" |
创建一个持久节点 /app,数据为 "my_app_data" |
create -e |
create -e /session_node "temp" |
创建一个临时节点 |
create -s |
create -s /seq_node "seq" |
创建一个持久顺序节点,节点名会变为 /seq_node0000000001 |
get |
get /app |
获取节点 /app 的数据和元数据(如版本号) |
get -w |
get -w /app |
获取节点数据并设置 Watcher |
set |
set /app "new_data" |
更新节点 /app 的数据 |
delete |
delete /app |
删除节点(节点必须为空) |
deleteall |
deleteall /parent |
递归删除节点及其所有子节点 |
stat |
stat /app |
查看节点状态,如创建时间、修改次数、子节点版本等 |
history |
history |
显示当前客户端会话执行过的命令历史 |
redo |
redo 3 |
重新执行历史命令列表中的第 3 条命令 |
实践示例:
# 1. 连接到 Zookeeper ./zkCli.sh -server 127.0.0.1:2181 # 2. 查看根节点 [zk: 127.0.0.1:2181(CONNECTED) 0] ls / [zookeeper] # 3. 创建一个持久节点 [zk: 127.0.0.1:2181(CONNECTED) 1] create /config "server-config" Created /config # 4. 在 /config 下创建一个临时顺序节点 [zk: 127.0.0.1:2181(CONNECTED) 2] create -e -s /config/instance "instance-1" Created /config/instance0000000001 # 5. 查看 /config 节点信息 [zk: 127.0.0.1:2181(CONNECTED) 3] get /config server-config cZxid = 0x2 ctime = Wed Nov 15 10:30:00 CST 2025 mZxid = 0x2 mtime = Wed Nov 15 10:30:00 CST 2025 pZxid = 0x3 cversion = 1 dataVersion = 0 aclVersion = 0 ephemeralOwner = 0x0 dataLength = 14 numChildren = 1 # 6. 更新节点数据 [zk: 127.0.0.1:2181(CONNECTED) 4] set /config "new-server-config" [zk: 127.0.0.1:2181(CONNECTED) 5] get /config new-server-config ... dataVersion = 1 # 注意版本号从0变成了1 # 7. 删除节点 [zk: 127.0.0.1:2181(CONNECTED) 6] delete /config Node not empty: /config, /config/instance0000000001 # 不能直接删除非空节点 [zk: 127.0.0.1:2181(CONNECTED) 7] deleteall /config [zk: 127.0.0.1:2181(CONNECTED) 8] ls / [zookeeper] # 8. 退出 [zk: 127.0.0.1:2181(CONNECTED) 9] quit
Java API 编程实践
使用 Java API 是 Zookeeper 最常见的使用方式,我们将使用 Curator 框架,它是 Netflix 公司开源的一套 Zookeeper 客户端框架,它封装了 Zookeeper 底层 API,提供了更高级、更易用的特性,如连接管理、重试策略、分布式锁、计数器等。
步骤:
-
添加 Maven 依赖 在你的
pom.xml文件中添加 Curator 依赖。<dependencies> <!-- Curator Framework --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>5.5.0</version> <!-- 使用较新稳定版本 --> </dependency> <!-- Curator Recipes (提供了高级特性) --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>5.5.0</version> </dependency> <!-- 日志 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.36</version> </dependency> </dependencies> -
创建客户端连接
CuratorFramework是核心客户端对象,推荐使用工厂模式CuratorFrameworkFactory创建。import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; public class CuratorUtils { private static final String ZK_CONNECTION_STRING = "127.0.0.1:2181"; private static final int SESSION_TIMEOUT_MS = 5000; private static final int CONNECTION_TIMEOUT_MS = 3000; public static CuratorFramework createClient() { // 重试策略:初始等待1秒,重试3次,每次等待时间递增 ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3); // 使用工厂创建客户端 return CuratorFrameworkFactory.builder() .connectString(ZK_CONNECTION_STRING) .sessionTimeoutMs(SESSION_TIMEOUT_MS) .connectionTimeoutMs(CONNECTION_TIMEOUT_MS) .retryPolicy(retryPolicy) .build(); } } -
基本操作:创建、获取、更新、删除节点
import org.apache.curator.framework.CuratorFramework; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.KeeperException; import org.apache.zookeeper.data.Stat; public class ZnodeExample { public static void main(String[] args) throws Exception { // 1. 创建客户端 CuratorFramework client = CuratorUtils.createClient(); client.start(); // 必须调用 start() 方法启动客户端 String path = "/example/node"; try { // 2. 创建节点 // createParentsIfAbsent() 会自动创建不存在的父节点 String createdPath = client.create() .creatingParentsIfNeeded() .withMode(CreateMode.PERSISTENT) // 节点类型 .forPath(path, "hello world".getBytes()); System.out.println("节点创建成功: " + createdPath); // 3. 获取节点数据 byte[] data = client.getData().forPath(path); System.out.println("节点数据: " + new String(data)); // 4. 更新节点数据 Stat stat = client.setData() .withVersion(-1) // -1 表示忽略版本检查,无条件更新 .forPath(path, "new data".getBytes()); System.out.println("节点更新成功,新版本: " + stat.getVersion()); // 5. 检查节点是否存在 Stat existsStat = client.checkExists().forPath(path); System.out.println("节点是否存在: " + (existsStat != null)); // 6. 删除节点 client.delete().deletingChildrenIfNeeded().forPath(path); System.out.println("节点删除成功"); } catch (KeeperException.NoNodeException e) { System.err.println("节点不存在: " + e.getMessage()); } finally { // 7. 关闭客户端 client.close(); } } } -
Watcher 机制使用
Curator 对 Zookeeper 的原生 Watcher 进行了封装,提供了
CuratorWatcher,它的事件对象CuratorEvent包含了更丰富的信息。import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.api.CuratorEvent; import org.apache.curator.framework.api.CuratorListener; import org.apache.zookeeper.WatchedEvent; public class WatcherExample { public static void main(String[] args) throws Exception { CuratorFramework client = CuratorUtils.createClient(); client.start(); String path = "/example/watcher_node"; // 确保节点存在 if (client.checkExists().forPath(path) == null) { client.create().forPath(path, "initial".getBytes()); } // 使用 CuratorListener CuratorListener listener = new CuratorListener() { @Override public void eventReceived(CuratorFramework client, CuratorEvent event) throws Exception { // 只关心节点数据变化事件 if (event.getType() == CuratorEvent.Type.NODE_CHANGED) { System.out.println("收到节点数据变更事件! Path: " + event.getPath()); byte[] data = client.getData().usingWatcher(this).forPath(event.getPath()); System.out.println("最新数据: " + new String(data)); } } }; client.getCuratorListenable().addListener(listener); // 第一次获取数据并设置监听 byte[] data = client.getData().usingWatcher(listener).forPath(path); System.out.println("初始数据: " + new String(data)); // 在另一个地方(或另一个程序)修改数据,观察控制台输出 // 例如使用 Shell: set /example/watcher_node "changed by shell" // 保持程序运行以接收事件 Thread.sleep(60000); client.close(); } }
实际应用场景
Zookeeper 的核心价值在于解决分布式系统中的共性问题。
-
分布式锁
- 场景:在多个服务实例中,保证对某个共享资源(如数据库记录、文件)的互斥访问。
- 实现:多个客户端竞争创建一个临时顺序节点,创建成功的客户端获得锁,没有获得锁的客户端,去监听它前一个节点的删除事件,一旦前一个节点被删除(锁释放),它就尝试获取锁。
- Curator 实现:
InterProcessMutex,使用非常简单。
InterProcessMutex lock = new InterProcessMutex(client, "/locks/my_lock"); if (lock.acquire(10, TimeUnit.SECONDS)) { try { // 获取锁成功,执行业务逻辑 System.out.println("获得锁,执行任务..."); Thread.sleep(5000); } finally { lock.release(); // 一定要在 finally 中释放锁 System.out.println("释放锁"); } } -
配置中心
- 场景:将应用配置(如数据库连接、功能开关)集中管理,并在配置变更时动态通知所有应用实例,无需重启。
- 实现:将配置信息存储在 Zookeeper 的某个
znode中,应用启动时从该节点读取配置,并设置一个Watcher,当配置变更时,Zookeeper 通知应用,应用重新拉取最新配置。
-
服务发现与注册
- 场景:在微服务架构中,服务消费者如何动态找到服务提供者的地址列表。
- 实现:
- 服务提供者:启动时,将自己的 IP 和端口信息作为临时节点注册到 Zookeeper 的某个服务目录下(如
/services/user-service)。 - 服务消费者:订阅
/services/user-service目录,获取所有子节点(即所有可用的服务提供者地址列表),当有服务上线或下线时,消费者会收到通知并更新本地地址列表。
- 服务提供者:启动时,将自己的 IP 和端口信息作为临时节点注册到 Zookeeper 的某个服务目录下(如
-
分布式队列
- 场景:在多个消费者之间分配任务,保证每个任务只被处理一次。
- 实现:所有消费者都向一个父目录下创建临时顺序节点,节点名代表任务,消费者只获取父目录下所有子节点,并消费序号最小的节点(任务),处理完成后删除该节点,其他消费者会监听节点的删除事件,从而处理下一个任务。
总结与进阶
- Zookeeper 是分布式系统的瑞士军刀,通过其简单的树形数据模型和强大的 Watcher 机制,解决了众多分布式协调问题,学习它的核心在于理解
znode的四种类型和 Watcher 的一次性、异步特性。 - 进阶:
- 深入理解 ZAB 协议:Zookeeper 的高可用性依赖于其核心的 Zab (Zookeeper Atomic Broadcast) 协议,了解 Leader 选举、数据同步等机制有助于你更好地排查问题。
- 掌握 Curator Recipes:Curator 的
recipes包提供了大量开箱即用的分布式组件,如分布式锁、计数器、屏障等,强烈建议深入学习。 - 性能与调优:了解 Zookeeper 的性能瓶颈(如写操作慢),学习如何进行 JVM 调优、磁盘 I/O 优化等。
- 替代方案:了解 Etcd,它也是目前非常流行的分布式协调服务,在某些场景下(如 Kubernetes)是 Zookeeper 的有力竞争者。
常见问题 (FAQ)
-
Q: Zookeeper 和 Nacos 有什么区别?
- A: Nacos 是一个更现代的服务发现和配置管理平台,功能更全面(集成了服务发现、配置管理、DNS 服务等),并且对 Spring Cloud 等生态支持更好,Zookeeper 是一个更底层的通用协调服务,需要自己在其上构建服务发现、配置中心等功能,Nacos 通常被认为更易用、功能更聚焦。
-
Q: Zookeeper 集群为什么推荐奇数台?
- A: 这是为了保证“脑裂”问题的安全,在投票选举 Leader 时,需要超过半数节点同意,假设有 5 台节点,允许 2 台节点宕机,剩下 3 台可以正常工作(3 > 5/2),如果变成 4 台,允许 1 台宕机,剩下 3 台可以工作,但如果此时网络分区,2 台在一个区,2 台在另一个区,两个分区都会认为自己是大多数,从而产生两个 Leader,即“脑裂”,奇数台可以避免偶数台在特定网络分区情况下出现平票的问题。
-
Q: 连接 Zookeeper 时出现
Connection refused或Session expired错误怎么办?- A:
Connection refused: 检查 Zookeeper 服务是否启动 (zkServer.sh status),检查clientPort和网络连接是否正确。Session expired: 通常是客户端长时间没有与服务器进行心跳通信,导致会话超时,可能是因为网络不稳定,或者客户端代码中长时间阻塞了网络 I/O,检查网络环境和客户端逻辑。
- A:
-
Q: 为什么我创建的临时节点在我关闭客户端后没有消失?
- A: 临时节点的生命周期与客户端会话绑定,确保你使用的是
close()方法正确地关闭了CuratorFramework客户端实例,而不是仅仅退出 JVM。close()方法会显式地与服务器断开连接,从而触发会话结束。
- A: 临时节点的生命周期与客户端会话绑定,确保你使用的是
