杰瑞科技汇

ZooKeeper使用教程,从入门到实践怎么学?

Zookeeper 使用教程:从入门到实践

目录

  1. 什么是 Zookeeper?
  2. 核心概念
  3. 环境准备与安装
  4. Zookeeper Shell 常用命令
  5. Java API 编程实践
  6. 实际应用场景
  7. 总结与进阶
  8. 常见问题 (FAQ)

什么是 Zookeeper?

Zookeeper 是一个开源的、分布式的、为分布式应用提供协调服务的高性能高可用具有严格顺序访问控制能力的分布式协调服务。

ZooKeeper使用教程,从入门到实践怎么学?-图1
(图片来源网络,侵删)

你可以把它想象成一个分布式的小型文件系统,或者一个内存中的数据库,它专门用来存储和管理一些关键的数据,并为分布式系统中的各个节点提供同步、配置管理、命名和分组等服务。

主要特点:

  • 高可用性:通常部署在奇数台(如 3, 5, 7 台)服务器上,只要集群中半数以上的节点存活,整个服务就可用。
  • 数据一致性:所有客户端看到的数据模型都是一致的,数据更新是原子性的。
  • 数据模型:类似于文件系统的树形结构,由一系列被称为 znode 的数据节点组成。
  • Watcher 机制:客户端可以对某个 znode 设置一个“监视器”(Watcher),当该 znode 的数据或子节点发生变化时,Zookeeper 会异步通知设置了监视器的客户端。
  • 顺序性:对于来自同一个客户端的请求,Zookeeper 会按照其发送的顺序进行处理。

核心概念

在学习使用之前,必须理解 Zookeeper 的几个核心概念:

1 Znode (数据节点)

Zookeeper 的数据存储单元,类似于文件系统中的文件或目录,每个 znode 都可以存储数据,并且可以拥有子节点。

ZooKeeper使用教程,从入门到实践怎么学?-图2
(图片来源网络,侵删)

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 的访问权限,权限包括:

ZooKeeper使用教程,从入门到实践怎么学?-图3
(图片来源网络,侵删)
  • CREATE: 创建子节点
  • READ: 读取节点数据和子节点列表
  • WRITE: 更新节点数据
  • DELETE: 删除节点
  • ADMIN: 设置 ACL

环境准备与安装

我们以 Linux 环境为例,演示两种最常用的安装模式。

1 单机模式安装

适合开发测试环境。

步骤:

  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
  2. 解压

    tar -zxvf apache-zookeeper-3.8.3-bin.tar.gz
    mv apache-zookeeper-3.8.3-bin /usr/local/zookeeper
  3. 配置

    • 进入 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
  4. 创建数据目录

    mkdir -p /usr/local/zookeeper/data
  5. 启动 Zookeeper

    # 回到 bin 目录
    cd /usr/local/zookeeper/bin
    # 启动服务
    ./zkServer.sh start
  6. 检查状态

    ./zkServer.sh status
    # 应该输出类似信息:
    # Zookeeper running (pid=12345)
    # Mode: standalone
  7. 连接客户端

    ./zkCli.sh
    # 连接成功后,会进入命令行界面,显示 [zk: localhost:2181(CONNECTED) 0]

2 伪集群模式安装

在一台物理机上模拟一个 Zookeeper 集群,适合学习和测试集群功能。

步骤:

  1. 准备工作:完成单机模式安装的前两步(下载、解压)。

  2. 创建集群目录和数据目录

    # 创建集群工作目录
    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
  3. 配置每个节点

    • 复制 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
  4. 创建 myid 文件 myid 文件用于标识当前服务器在集群中的 ID,必须与 zoo.cfgserver.XX 对应。

    • 在 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
  5. 启动集群

    # 分别启动三个节点
    /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
  6. 检查集群状态

    # 检查 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,提供了更高级、更易用的特性,如连接管理、重试策略、分布式锁、计数器等。

步骤:

  1. 添加 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>
  2. 创建客户端连接 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();
        }
    }
  3. 基本操作:创建、获取、更新、删除节点

    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();
            }
        }
    }
  4. 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 的核心价值在于解决分布式系统中的共性问题。

  1. 分布式锁

    • 场景:在多个服务实例中,保证对某个共享资源(如数据库记录、文件)的互斥访问。
    • 实现:多个客户端竞争创建一个临时顺序节点,创建成功的客户端获得锁,没有获得锁的客户端,去监听它前一个节点的删除事件,一旦前一个节点被删除(锁释放),它就尝试获取锁。
    • 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("释放锁");
        }
    }
  2. 配置中心

    • 场景:将应用配置(如数据库连接、功能开关)集中管理,并在配置变更时动态通知所有应用实例,无需重启。
    • 实现:将配置信息存储在 Zookeeper 的某个 znode 中,应用启动时从该节点读取配置,并设置一个 Watcher,当配置变更时,Zookeeper 通知应用,应用重新拉取最新配置。
  3. 服务发现与注册

    • 场景:在微服务架构中,服务消费者如何动态找到服务提供者的地址列表。
    • 实现
      • 服务提供者:启动时,将自己的 IP 和端口信息作为临时节点注册到 Zookeeper 的某个服务目录下(如 /services/user-service)。
      • 服务消费者:订阅 /services/user-service 目录,获取所有子节点(即所有可用的服务提供者地址列表),当有服务上线或下线时,消费者会收到通知并更新本地地址列表。
  4. 分布式队列

    • 场景:在多个消费者之间分配任务,保证每个任务只被处理一次。
    • 实现:所有消费者都向一个父目录下创建临时顺序节点,节点名代表任务,消费者只获取父目录下所有子节点,并消费序号最小的节点(任务),处理完成后删除该节点,其他消费者会监听节点的删除事件,从而处理下一个任务。

总结与进阶

  • 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 refusedSession expired 错误怎么办?

    • A:
      • Connection refused: 检查 Zookeeper 服务是否启动 (zkServer.sh status),检查 clientPort 和网络连接是否正确。
      • Session expired: 通常是客户端长时间没有与服务器进行心跳通信,导致会话超时,可能是因为网络不稳定,或者客户端代码中长时间阻塞了网络 I/O,检查网络环境和客户端逻辑。
  • Q: 为什么我创建的临时节点在我关闭客户端后没有消失?

    • A: 临时节点的生命周期与客户端会话绑定,确保你使用的是 close() 方法正确地关闭了 CuratorFramework 客户端实例,而不是仅仅退出 JVM。close() 方法会显式地与服务器断开连接,从而触发会话结束。
分享:
扫描分享到社交APP
上一篇
下一篇