Apache ZooKeeper

技术栈
工具链
协调服务分布式服务发现配置管理选举

概览

Apache ZooKeeper

ZooKeeper 是 Apache 的分布式协调服务,最初是 Yahoo! 内部项目,现广泛用于分布式系统的配置管理、服务发现、分布式锁、Leader 选举等场景。

核心价值:为分布式系统提供高可用的"协调原语",让开发者不必从零实现 Paxos/Raft 等一致性协议。

关键特性

  • 类文件系统的树形命名空间(ZNode)
  • ZNode 分为持久/临时/持久顺序/临时顺序
  • Watcher 机制:客户端监听 ZNode 变化,事件驱动回调
  • 原子广播协议(ZAB)保证一致性
  • 半数以上节点存活即可工作(容忍 N-1/2 故障)

安装

1. 环境准备

  • 操作系统:Linux(推荐)、macOS、Windows
  • Java:JDK 8 / 11 / 17
  • 端口:2181(客户端)、2888(集群 Peer)、3888(集群选举)
  • 集群规模:奇数台(推荐 3/5/7),容忍 (N-1)/2 台故障
  • 磁盘:事务日志建议用独立 SSD

2. 安装命令

Docker 快速体验

# 单机模式
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.9

# 测试连接
docker exec -it zookeeper zkCli.sh -server localhost:2181

Docker Compose 集群

version: '3.8'
services:
  zoo1:
    image: zookeeper:3.9
    hostname: zoo1
    ports: ["2181:2181"]
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
  zoo2:
    image: zookeeper:3.9
    hostname: zoo2
    ports: ["2182:2181"]
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
  zoo3:
    image: zookeeper:3.9
    hostname: zoo3
    ports: ["2183:2181"]
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181

手动安装(Linux)

wget https://dlcdn.apache.org/zookeeper/zookeeper-3.9.2/apache-zookeeper-3.9.2-bin.tar.gz
tar -xzf apache-zookeeper-3.9.2-bin.tar.gz
cd apache-zookeeper-3.9.2-bin

# 创建配置
cp conf/zoo_sample.cfg conf/zoo.cfg
cat >; conf/zoo.cfg <<EOF
tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
initLimit=10
syncLimit=5
EOF

mkdir -p /var/lib/zookeeper
bin/zkServer.sh start

3. 常见安装问题

问题 解决方案
Cannot open channel 集群连接失败 检查 2888/3888 端口是否互通、ZOO_MY_IDZOO_SERVERS 是否一致
myid 文件缺失 echo 1 > /var/lib/zookeeper/myid,每个节点 ID 唯一
选举无限循环 检查集群节点数量是否为奇数,网络延迟是否过高
java.net.BindException 端口被占用,修改 clientPortZOO_PORT
磁盘满导致崩溃 ZooKeeper 对磁盘敏感,配置 autopurge.snapRetainCount=3 定期清理快照

示例

ZooKeeper Python 客户端操作与分布式锁

目标

使用 Python kazoo 库操作 ZooKeeper ZNode,并实现一个分布式锁。

环境准备

pip install kazoo

完整代码

基础操作(zk_basic.py)

from kazoo.client import KazooClient
import time

# 连接
zk = KazooClient(hosts='localhost:2181')
zk.start()

# 创建节点
zk.ensure_path('/services/users')
zk.create('/services/users/node1', b'http://192.168.1.10:8080', ephemeral=True)

# 读取节点
data, stat = zk.get('/services/users/node1')
print(f"数据: {data.decode()}, 版本: {stat.version}")

# 更新节点
zk.set('/services/users/node1', b'http://192.168.1.11:8080')

# 监听节点变化
@zk.DataWatch('/services/users/node1')
def watch_node(data, stat):
    print(f"节点变化: {data.decode() if data else '已删除'}")

# 获取子节点
children = zk.get_children('/services/users')
print(f"子节点: {children}")

# 删除
zk.delete('/services/users/node1')
zk.stop()

分布式锁(zk_lock.py)

from kazoo.client import KazooClient
from kazoo.recipe.lock import Lock
import threading
import time

def worker(worker_id, lock):
    print(f"Worker-{worker_id} 尝试获取锁...")
    with lock:
        print(f"Worker-{worker_id} 获得锁,开始处理")
        time.sleep(2)
        print(f"Worker-{worker_id} 处理完成,释放锁")

# 连接
zk = KazooClient(hosts='localhost:2181')
zk.start()

# 创建锁
lock = Lock(zk, '/locks/resource-a')

# 多线程竞争
threads = []
for i in range(5):
    t = threading.Thread(target=worker, args=(i, lock))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("所有 Worker 完成")
zk.stop()

Leader 选举(zk_leader.py)

from kazoo.client import KazooClient
from kazoo.recipe.election import Election

zk = KazooClient(hosts='localhost:2181')
zk.start()

election = Election(zk, '/elections/leader')

def leader_callback():
    print("✅ 我成为了 Leader!")
    # 做 Leader 的工作...

def follower_callback():
    print("👤 我是 Follower")

# 参与选举
election.run(leader_callback)

# 阻塞等待
import time
time.sleep(60)
zk.stop()

运行步骤

# 确保 ZooKeeper 运行
docker ps | grep zookeeper

# 运行基础操作
python zk_basic.py

# 运行分布式锁(观察串行输出)
python zk_lock.py

# 运行 Leader 选举(终端1 + 终端2 各跑一个)
python zk_leader.py

预期输出

分布式锁示例中,5 个 Worker 串行执行(不会同时输出"获得锁")。Leader 选举中,只有一个实例成为 Leader。

教程

ZooKeeper 入门教程:分布式协调核心原理

1. 为什么需要独立的协调服务?

在分布式系统中,节点间需要就某些事情达成一致:

  • 谁是 Leader?
  • 配置变更了谁通知?
  • 这个资源谁在使用?

与其每个服务自己实现 Paxos/Raft,不如用一个经过验证的通用协调服务——这就是 ZooKeeper 的定位。

2. ZNode 详解

ZooKeeper 数据模型类似文件系统:

/
├── config/           # 持久节点
│   ├── db_url        # 数据库连接串
│   └── feature_flag  # 功能开关
├── services/         # 临时节点(服务注册)
│   ├── service-a-001
│   └── service-a-002
└── locks/            # 临时顺序节点(分布式锁)
    ├── lock-0000001
    └── lock-0000002

四种节点类型

类型 创建命令 生命周期
PERSISTENT create /path data 持续到显式删除
PERSISTENT_SEQUENTIAL create -s /path data 同上 + 自增后缀
EPHEMERAL create -e /path data 客户端断开即删除
EPHEMERAL_SEQUENTIAL create -e -s /path data 同上 + 自增后缀

3. Watcher 机制

客户端可以对 ZNode 设置 Watcher,触发一次回调:

@zk.DataWatch('/config/timeout')
def on_config_change(data, stat):
    if data:
        update_timeout(int(data.decode()))

注意:Watcher 是一次性的!触发后需重新注册。Kazoo 等客户端已封装此行为。

4. 经典应用场景

配置管理

/config/app/db_url = "mysql://..."
/config/app/log_level = "INFO"

所有节点 Watch /config,配置变更时实时推送。

服务发现

/services/order-service/server-001 (Ephemeral)
/services/order-service/server-002 (Ephemeral)

服务器启动创建临时节点,宕机自动删除。消费者监听 /services/order-service 子节点变化。

分布式锁

利用临时顺序节点:

  1. 所有请求者在 /lock/resource 下创建临时顺序节点
  2. 序号最小的获得锁
  3. 释放锁时删除节点,下一个最小序号的被唤醒

5. ZAB 协议简介

ZooKeeper Atomic Broadcast:事务日志 + 原子广播

  • Leader 处理写请求,转发 Proposal 给 Followers
  • 半数以上 ACK → Commit
  • 与 Raft 相似但不完全相同

6. 思考题

  1. 为什么 ZooKeeper 集群推荐奇数台?2 台可以吗?
  2. 临时节点和 Watcher 如何配合实现服务发现?
  3. ZooKeeper vs etcd 选型?什么场景该用 etcd?

参考资料

暂无参考文献