接手过三套 ZK 集群,两套跟着 Kafka、一套跟着老 HBase。云原生时代新项目基本不会再引入它了,但存量系统的坑你还是得能扛。这篇把踩过的东西记下来。
Zookeeper 核心概念#
ZNode 类型#
Zookeeper 的数据模型是一棵树形结构,每个节点称为 ZNode。ZNode 有四种类型:
/
├── /kafka
│ ├── /brokers (Persistent - 持久节点)
│ ├── /controller (Ephemeral - 临时节点)
│ └── /config
├── /hadoop
│ └── /leader (Ephemeral - 临时节点)
└── /locks
└── /distributed-lock- (Ephemeral Sequential - 临时顺序节点)
├── /distributed-lock-0000000001
├── /distributed-lock-0000000002
└── /distributed-lock-0000000003
Persistent(持久节点):
- 创建后永久存在,直到显式删除
- 典型用途:存储配置信息、服务注册表
Ephemeral(临时节点):
- 与创建它的客户端 Session 绑定
- Session 断开后节点自动删除
- 典型用途:服务健康检测、Leader 选举
- 注意:临时节点不能有子节点
Persistent Sequential(持久顺序节点):
- 在父节点下自动追加单调递增的 10 位序号(如
lock-0000000001) - 典型用途:分布式队列、全局唯一 ID 生成
Ephemeral Sequential(临时顺序节点):
- 结合了临时和顺序的特性
- 典型用途:公平分布式锁(Watch 前一个序号节点,实现排队等待)
Zookeeper 3.6+ 新增 Container 和 TTL 节点:
- Container:当所有子节点被删除后,Container 节点由服务端自动清理
- TTL:节点超过指定时间未被修改则自动删除
Watcher 机制#
Watcher 是 Zookeeper 实现通知的核心机制,客户端可以在读操作(getData、getChildren、exists)上注册一次性监听器。
客户端 ZooKeeper 服务端
│ │
│ getData("/config", watch=true) │
│─────────────────────────────────────>│
│ 返回数据 + 注册 Watcher │
│<─────────────────────────────────────│
│ │
│ (某时刻 /config 被修改) │
│ │
│ NodeDataChanged 事件通知 │
│<─────────────────────────────────────│
│ │
│ (客户端重新读取获取最新值) │
│ getData("/config", watch=true) │ ← 必须重新注册!
│─────────────────────────────────────>│
Watcher 的关键特性:
- 一次性:触发后自动失效,客户端需要重新注册(这是实现代码中最容易忽略的点)
- 顺序性:同一 Session 收到的 Watcher 事件是有序的
- 轻量级通知:事件本身不携带数据,客户端收到通知后需主动拉取最新值
- Session 绑定:Session 断开时,已注册的 Watcher 会被清除,客户端重连后需重新注册
Watcher 事件类型:
| 事件类型 | 触发条件 |
|---|---|
| NodeCreated | 节点被创建(对 exists 的 watch 生效) |
| NodeDeleted | 节点被删除 |
| NodeDataChanged | 节点数据被修改 |
| NodeChildrenChanged | 子节点列表变化 |
| DataWatchRemoved | watch 被移除(3.6+ 永久 Watcher 专用) |
ZAB 协议与选举算法#
ZAB(Zookeeper Atomic Broadcast)是 Zookeeper 的核心一致性协议,分为两个阶段:
阶段一:崩溃恢复(Leader 选举)
当集群启动或 Leader 失联时,触发选举。默认选举算法为 FastLeaderElection(epoch + zxid + myid 三元组投票):
选举规则(按优先级排序):
1. 优先选 epoch(逻辑时钟/纪元)最大的节点
2. epoch 相同时,优先选 zxid(事务 ID)最大的节点
3. zxid 相同时,优先选 myid 最大的节点
目标:选出数据最新(zxid 最大)的节点作为 Leader,保证不丢数据
选举流程示例(3 节点集群,myid 分别为 1、2、3):
1. 初始状态:所有节点都投票给自己
节点1: vote(epoch=0, zxid=100, myid=1)
节点2: vote(epoch=0, zxid=102, myid=2) ← zxid 最大
节点3: vote(epoch=0, zxid=101, myid=3)
2. 节点1、3 收到节点2 的投票,发现 zxid=102 > 自己,改投节点2
节点1: vote(epoch=0, zxid=102, myid=2)
节点2: vote(epoch=0, zxid=102, myid=2)
节点3: vote(epoch=0, zxid=102, myid=2)
3. 节点2 收到超过半数(3/3)的票,成为 Leader
选举完成,耗时通常 < 200ms(单机房)
阶段二:消息广播(正常写入)
客户端 → Leader:写请求
Leader:生成新的 zxid,向所有 Follower 发送 Proposal(提案)
Follower:写入本地事务日志,回复 ACK
Leader:收到 Quorum(半数以上)ACK → 发送 Commit
Leader → 客户端:写入成功
关键点:Leader 不需要等所有 Follower ACK,只需 Quorum(n/2+1)即可提交
3 节点集群:需要 2 个 ACK
5 节点集群:需要 3 个 ACK
集群部署#
节点数量选择#
3 节点集群:可容忍 1 台故障
5 节点集群:可容忍 2 台故障
7 节点集群:可容忍 3 台故障
公式:N 节点集群,可容忍 (N-1)/2 台故障
为什么是奇数?
偶数节点集群并不增加容错能力:
- 4 节点 = 容忍 1 台故障(需要 3 个 ACK)
- 4 节点和 3 节点容错能力相同,但 4 节点资源消耗更多
生产推荐:3 节点集群满足大多数场景,ZooKeeper 本身是轻量级服务,不需要太多节点。
myid 配置#
每个节点必须有唯一的数字标识,写入 dataDir 下的 myid 文件:
# 节点1
echo 1 > /data/zookeeper/myid
# 节点2
echo 2 > /data/zookeeper/myid
# 节点3
echo 3 > /data/zookeeper/myid
zoo.cfg 完整配置#
# /etc/zookeeper/conf/zoo.cfg
# ==================== 基础配置 ====================
# 心跳基本单位(毫秒)
tickTime=2000
# dataDir 必须挂载独立磁盘(与 OS 分开),避免 I/O 竞争
dataDir=/data/zookeeper/data
# 事务日志目录,强烈建议与 dataDir 挂不同的磁盘
# 事务日志是顺序写,对磁盘 IOPS 要求高
dataLogDir=/data/zookeeper/txlog
# 客户端连接端口
clientPort=2181
# ==================== 集群配置 ====================
# 集群成员:server.myid=host:集群通信端口:选举端口
server.1=zk1.internal:2888:3888
server.2=zk2.internal:2888:3888
server.3=zk3.internal:2888:3888
# Follower 与 Leader 建立连接的最大 tick 数
# 实际超时 = initLimit * tickTime = 10 * 2000 = 20 秒
initLimit=10
# Follower 与 Leader 同步数据的最大 tick 数
# 超过此时间未同步完成,Follower 与 Leader 断开
# 实际超时 = syncLimit * tickTime = 5 * 2000 = 10 秒
syncLimit=5
# ==================== 性能配置 ====================
# 单次批量提交的最大事务数(默认 1000)
maxBatchSize=1000
# 客户端连接超时(毫秒),客户端 Session 超时的最小/最大值
minSessionTimeout=4000 # 默认 2 * tickTime
maxSessionTimeout=40000 # 默认 20 * tickTime
# 单客户端最大并发连接数(防止单客户端耗尽连接池)
maxClientCnxns=200
# Snapcount:事务日志超过此数量后触发快照
snapCount=100000
# ==================== 安全配置 ====================
# 4 字命令白名单(生产建议只开必要的)
4lw.commands.whitelist=mntr,ruok,stat,dump,conf,isro
# 开启 JMX 监控(配合 Prometheus exporter)
# 通过 JVM 参数配置,见后文
# ==================== 3.5+ 新特性 ====================
# 开启管理端 UI(访问 http://host:8080/commands)
admin.enableServer=true
admin.serverPort=8080
# 自动清理快照和事务日志(防止磁盘写满)
autopurge.purgeInterval=24 # 每 24 小时清理一次
autopurge.snapRetainCount=5 # 保留最近 5 个快照
Docker Compose 部署(测试/开发环境)#
# docker-compose.yml
version: '3.8'
services:
zoo1:
image: zookeeper:3.8
hostname: zoo1
ports:
- "2181:2181"
- "8080:8080"
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
ZOO_DATA_DIR: /data
ZOO_DATA_LOG_DIR: /datalog
ZOO_TICK_TIME: 2000
ZOO_INIT_LIMIT: 10
ZOO_SYNC_LIMIT: 5
ZOO_MAX_CLIENT_CNXNS: 200
ZOO_AUTOPURGE_PURGEINTERVAL: 24
ZOO_AUTOPURGE_SNAPRETAINCOUNT: 5
ZOO_4LW_COMMANDS_WHITELIST: "mntr,ruok,stat,dump,conf"
volumes:
- zoo1-data:/data
- zoo1-log:/datalog
zoo2:
image: zookeeper:3.8
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
volumes:
- zoo2-data:/data
- zoo2-log:/datalog
zoo3:
image: zookeeper:3.8
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
volumes:
- zoo3-data:/data
- zoo3-log:/datalog
volumes:
zoo1-data:
zoo1-log:
zoo2-data:
zoo2-log:
zoo3-data:
zoo3-log:
Kubernetes StatefulSet 部署#
# zookeeper-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
namespace: middleware
spec:
serviceName: zookeeper-headless
replicas: 3
selector:
matchLabels:
app: zookeeper
template:
metadata:
labels:
app: zookeeper
spec:
# ZooKeeper 对延迟敏感,建议反亲和性确保跨节点部署
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: zookeeper
topologyKey: kubernetes.io/hostname
containers:
- name: zookeeper
image: zookeeper:3.8
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: follower
- containerPort: 3888
name: election
- containerPort: 8080
name: admin
env:
- name: ZOO_MY_ID
valueFrom:
fieldRef:
fieldPath: metadata.name # 配合 init container 解析序号
- name: ZOO_SERVERS
value: "server.1=zookeeper-0.zookeeper-headless:2888:3888;2181 server.2=zookeeper-1.zookeeper-headless:2888:3888;2181 server.3=zookeeper-2.zookeeper-headless:2888:3888;2181"
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2"
volumeMounts:
- name: data
mountPath: /data
- name: datalog
mountPath: /datalog
livenessProbe:
exec:
command: ["/bin/bash", "-c", "echo ruok | nc localhost 2181 | grep imok"]
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
exec:
command: ["/bin/bash", "-c", "echo ruok | nc localhost 2181 | grep imok"]
initialDelaySeconds: 10
periodSeconds: 5
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd # 使用 SSD 存储类
resources:
requests:
storage: 20Gi
- metadata:
name: datalog
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
name: zookeeper-headless
namespace: middleware
spec:
clusterIP: None
selector:
app: zookeeper
ports:
- name: client
port: 2181
- name: follower
port: 2888
- name: election
port: 3888
生产配置调优#
JVM 参数调优#
# zkEnv.sh 或通过环境变量 SERVER_JVMFLAGS 设置
export SERVER_JVMFLAGS="
-Xmx2g
-Xms2g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1HeapRegionSize=16m
-XX:+ParallelRefProcEnabled
-XX:+UnlockExperimentalVMOptions
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/var/log/zookeeper/zk-gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=100m
-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
"
堆内存指导原则:
- ZooKeeper 将所有数据常驻内存(内存即数据库)
- 堆大小 ≥ 数据集大小 × 2(为 GC 预留空间)
- 生产环境建议
2g ~ 4g,数据量大的场景可到8g - 避免配置过大(>8g)导致 GC 停顿影响选举超时
zoo.cfg 关键参数调优#
# tickTime:基础时间单位,是 Watcher 超时和 Session 超时的基准
# 调小(如 1000ms):更快检测故障,但网络抖动时选举更频繁
# 调大(如 3000ms):更稳定,但故障检测延迟增加
# 推荐:单机房 2000ms,跨机房 4000ms
tickTime=2000
# syncLimit 的选择关键:
# 如果 Leader 和 Follower 之间网络延迟较高(如跨 AZ),需要调大
# syncLimit * tickTime 必须大于数据同步所需时间
# 如果有大量写入导致 Follower 频繁 lag,适当调大此值
syncLimit=5
# 客户端 Session 超时范围
# 如果客户端频繁出现 SessionExpired,考虑适当调大 maxSessionTimeout
minSessionTimeout=4000
maxSessionTimeout=40000
磁盘规划建议#
# 推荐的磁盘布局
/data/zookeeper/
├── data/ → SSD 独立挂载点,存放快照(Snapshot)
│ └── myid
└── txlog/ → SSD 独立挂载点(最好与 data 不同盘),存放事务日志
# 为什么要分开?
# 事务日志是高频顺序写(每次写入都 fsync),与快照 I/O 在同一磁盘会相互干扰
# 生产环境中因磁盘 I/O 竞争导致 Zookeeper 超时的案例非常常见
# 磁盘容量估算
# 事务日志:每秒 1000 TPS × 1KB/事务 = 1MB/s
# 快照:随数据集大小,通常 100MB ~ 1GB
# 日志保留:建议保留 5 个快照 + 对应的事务日志
# 建议数据盘和日志盘各 50GB(足够大多数场景)
四字命令诊断#
四字命令是 Zookeeper 内置的诊断接口,通过 nc 或 telnet 直接发送:
# 通用查询方式
echo <cmd> | nc <host> 2181
# 或使用 zookeeper-shell(如果安装了 ZK 客户端工具)
# 注意:生产环境需要在 zoo.cfg 中配置 4lw.commands.whitelist
ruok:健康检查#
$ echo ruok | nc zk1.internal 2181
imok
返回 imok 表示进程正常运行。注意:ruok 只检查进程是否响应,不检查是否处于正常服务状态(如选举中的节点也会返回 imok)。
stat:服务状态概览#
$ echo stat | nc zk1.internal 2181
Zookeeper version: 3.8.3-6ad6d364c7c0bcf0de452d54ebefa3c3fc0a7548, built on 09/07/2023 05:39 GMT
Clients:
/10.0.1.5:52341[1](queued=0,recved=1254,sent=1254)
/10.0.1.6:48892[1](queued=0,recved=8765,sent=8766)
/10.0.1.7:61023[0](queued=0,recved=1,sent=0)
Latency min/avg/max: 0/1/45
Received: 287643
Sent: 287644
Connections: 23
Outstanding: 0
Zxid: 0x10000043f
Mode: leader ← 当前角色(leader/follower/observer)
Node count: 15234
关键字段解读:
Mode: leader/follower:确认节点角色,用于判断集群是否正常Outstanding: 0:待处理请求数,若持续 > 0 说明处理能力不足Latency avg:平均处理延迟,正常应 < 10ms,若持续 > 100ms 需排查Connections:当前客户端连接数,超过maxClientCnxns会拒绝新连接
mntr:详细指标(Prometheus 拉取主要来源)#
$ echo mntr | nc zk1.internal 2181
zk_version 3.8.3-6ad6d364c7c0bcf0de452d54ebefa3c3fc0a7548, built on 09/07/2023 05:39 GMT
zk_avg_latency 1
zk_max_latency 45
zk_min_latency 0
zk_packets_received 287643
zk_packets_sent 287644
zk_num_alive_connections 23
zk_outstanding_requests 0
zk_server_state leader
zk_znode_count 15234
zk_watch_count 4521 ← 活跃 Watcher 数量
zk_ephemerals_count 234 ← 临时节点数量
zk_approximate_data_size 2048576 ← 内存中数据集大小(字节)
zk_open_file_descriptor_count 128
zk_max_file_descriptor_count 65536
zk_followers 2 ← Leader 视角:当前 Follower 数(只有 Leader 输出)
zk_synced_followers 2 ← 已同步的 Follower 数(应等于 followers)
zk_pending_syncs 0 ← 等待同步的 Follower 数
zk_last_proposal_size 32
zk_max_proposal_size 1024
zk_min_proposal_size 32
重点告警指标:
zk_outstanding_requests > 10:请求积压,检查 Leader 处理能力zk_synced_followers < 2(3 节点集群):Follower 掉线,集群可能失去 Quorumzk_watch_count > 100000:Watcher 数量异常,可能有内存泄漏zk_approximate_data_size增速异常:数据集意外膨胀
dump:会话与临时节点信息#
$ echo dump | nc zk1.internal 2181
SessionTracker dump:
Session Sets (3):
0x10000000000001 VALID # Session ID
0x10000000000002 VALID
0x10000000000003 CLOSING
ephemeral nodes dump:
Sessions with Ephemerals (2):
0x10000000000001: # 该 Session 持有的临时节点
/kafka/controller
/kafka/brokers/ids/1
0x10000000000002:
/kafka/brokers/ids/2
dump 用于排查"临时节点为什么没有消失"——找到对应 Session ID,结合 Session 状态判断。
conf:查看运行时配置#
$ echo conf | nc zk1.internal 2181
clientPort=2181
secureClientPort=-1
dataDir=/data/zookeeper/data/version-2
dataLogDir=/data/zookeeper/txlog/version-2
tickTime=2000
maxClientCnxns=200
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=1
initLimit=10
syncLimit=5
electionAlg=3
electionPort=3888
quorumPort=2888
peerType=0
membership:
server.1=zk1.internal:2888:3888:participant
server.2=zk2.internal:2888:3888:participant
server.3=zk3.internal:2888:3888:participant
常见问题排查#
选举超时导致集群不可用#
症状:客户端报 ConnectionLoss,Zookeeper 日志持续出现 LOOKING 状态
# 查看选举日志
grep -E "LOOKING|LEADING|FOLLOWING|election" /var/log/zookeeper/zookeeper.log | tail -50
# 检查节点间网络连通性(2888 和 3888 端口)
nc -zv zk2.internal 3888
nc -zv zk2.internal 2888
# 查看是否存在 GC 停顿导致的超时
grep "GC pause\|Stop-the-world" /var/log/zookeeper/zk-gc.log | tail -20
常见原因与处理:
- 网络分区:检查防火墙规则,确认 2888/3888 端口双向可达
- GC 停顿过长:调整 JVM 参数,使用 G1GC,降低 MaxGCPauseMillis
- 磁盘 I/O 过高:检查
iostat -x 1磁盘利用率,事务日志写入慢会导致心跳超时 - 时钟偏差:Zookeeper 依赖本机时钟,检查
ntpstat或chronyc tracking
# 强制触发新一轮选举(已确认某节点数据落后时使用)
# 方法:停止数据最旧的节点,让其他节点先选出 Leader
systemctl stop zookeeper
# 清除数据(仅当节点数据已无法恢复时)
# 危险操作!确保集群中至少有 n/2+1 个节点数据完整
rm -rf /data/zookeeper/data/version-2
rm -rf /data/zookeeper/txlog/version-2
systemctl start zookeeper
# 重启后该节点会作为 Learner 从 Leader 全量同步数据
连接风暴(Connection Storm)#
症状:短时间内 zk_num_alive_connections 急剧增长,Zookeeper CPU 飙高,大量请求超时
触发场景:Kafka Broker 全部重启时,所有 Broker 同时重连 Zookeeper,形成连接风暴。
# 查看连接数变化趋势
while true; do
echo -n "$(date): "
echo stat | nc zk1.internal 2181 | grep "Connections"
sleep 1
done
# 查看哪些 IP 连接数最多
echo stat | nc zk1.internal 2181 | grep "^/" | awk -F'[/:]' '{print $2}' | sort | uniq -c | sort -rn | head -20
缓解措施:
# zoo.cfg 配置连接限流
# 单客户端 IP 最大连接数
maxClientCnxns=200
# 3.6.1+ 新增:全局连接限制
globalOutstandingLimit=1000
// 客户端侧:使用指数退避重连策略
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("zk1:2181,zk2:2181,zk3:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 10)) // 1s 起步,最多 10 次重试
.sessionTimeoutMs(30000)
.connectionTimeoutMs(15000)
.build();
磁盘满导致服务中断#
Zookeeper 的事务日志采用 fsync 确保持久化,磁盘满后事务日志无法写入,直接导致所有写操作超时。
# 紧急处理:清理旧的快照和事务日志
# 方法一:使用内置清理工具
java -cp /opt/zookeeper/lib/*:/opt/zookeeper/zookeeper-*.jar \
org.apache.zookeeper.server.PurgeTxnLog \
/data/zookeeper/data \
/data/zookeeper/txlog \
-n 3 # 保留最近 3 个快照及对应的事务日志
# 方法二:手动删除旧文件
# 快照文件命名:snapshot.<zxid>
# 事务日志命名:log.<zxid>
ls -lt /data/zookeeper/data/version-2/snapshot.* | tail -n +6 | xargs rm -f
ls -lt /data/zookeeper/txlog/version-2/log.* | tail -n +6 | xargs rm -f
# 配置 autopurge 防止再次发生
# zoo.cfg
autopurge.purgeInterval=24
autopurge.snapRetainCount=5
脑裂(Split Brain)处理#
脑裂是指网络分区导致集群出现两个 Leader 的情况。ZAB 协议通过 Quorum 机制防止脑裂:只要集群中没有一半以上节点可达,就不会选出新 Leader。
3 节点集群中,如果节点 A 网络隔离:
- 节点 A 自己认为自己是 Leader(但实际上 B+C 已经选出新 Leader)
- 节点 A 处于老 epoch,它的写入客户端已连接不上(因为 Quorum 在 B+C 侧)
- 节点 A 重新加入后,会发现自己 epoch 落后,转变为 Follower
但如果同时存在多个旧 epoch 的 “幽灵 Leader”,可能导致客户端状态混乱:
# 确认当前集群 Leader 是哪个节点
for host in zk1 zk2 zk3; do
echo -n "${host}: "
echo stat | nc ${host}.internal 2181 | grep "Mode"
done
# 正常输出应该只有一个 leader,其余为 follower
与 Kafka 的关系#
Kafka 对 Zookeeper 的依赖(旧版本)#
在 Kafka 2.8 之前,Zookeeper 是 Kafka 的核心依赖:
Kafka 使用 Zookeeper 存储的数据:
/kafka/brokers/ids/ - Broker 注册与发现
/kafka/controller - Controller 选举(临时节点)
/kafka/brokers/topics/ - Topic 元数据(分区数、副本分配)
/kafka/config/topics/ - Topic 配置
/kafka/consumers/ - Consumer Group 位移(老版本)
/kafka/admin/ - 管理操作状态
/kafka/isr_change_notification - ISR 变更通知
Zookeeper 的可用性直接影响 Kafka:
- Zookeeper 不可用时,Kafka Controller 无法工作,分区 Leader 选举停止
- 新的 Consumer Group 无法创建
- Topic 配置无法修改
Kafka KRaft 模式:告别 Zookeeper#
Kafka 2.8 引入 KRaft(Kafka Raft)模式,3.3 版本进入 Production Ready,Kafka 4.0 已彻底移除 Zookeeper 支持。
KRaft 架构变化:
旧架构:Kafka Broker + 独立 Zookeeper 集群(3 节点)
新架构:Kafka 自身实现 Raft 共识(Controller 节点负责)
KRaft 的 Controller 节点职责:
- 存储集群元数据(取代 Zookeeper)
- 基于 Raft 协议选举,无需外部依赖
- 元数据存储在 __cluster_metadata 主题中
迁移策略:
# 检查当前 Kafka 版本
kafka-broker-api-versions.sh --bootstrap-server localhost:9092 | head -5
# KRaft 迁移路径(生产谨慎操作)
# Kafka 3.x 支持 ZK 模式 → KRaft 迁移工具
kafka-storage.sh random-uuid # 生成新的 Cluster ID
# 如果是新集群,直接使用 KRaft 模式
kafka-storage.sh format \
--config /etc/kafka/kraft/server.properties \
--cluster-id $(kafka-storage.sh random-uuid)
什么时候还需要 Zookeeper#
在 2025 年,仍然需要 Zookeeper 的场景:
- Kafka 版本 < 2.8 的存量集群(未完成升级时)
- HBase:HBase 仍深度依赖 Zookeeper(RegionServer 注册、Master 选举、分布式锁)
- Apache Hadoop YARN:ResourceManager HA 使用 Zookeeper 做 Leader 选举
- Apache Curator 框架:基于 Zookeeper 实现的分布式原语(锁、选举、队列)
- 老旧的 SOA 服务发现(如 Dubbo 2.x 默认注册中心)
已有替代方案的场景:
| 用途 | Zookeeper | 替代方案 |
|---|---|---|
| 服务注册发现 | Dubbo + ZK | Nacos / Consul / etcd |
| 分布式锁 | Curator InterProcessMutex | Redis Redlock / etcd |
| 配置中心 | ZK 节点存配置 | Nacos / Apollo / etcd |
| Leader 选举 | 临时节点 + Watcher | etcd / etcd-based K8s leaderelection |
| Kafka 元数据 | ZK 存储 | Kafka KRaft |
监控体系#
Prometheus zookeeper_exporter#
# docker-compose.yml 添加 exporter
zookeeper-exporter:
image: dabealu/zookeeper-exporter:v0.1.9
command:
- --zk-hosts=zk1.internal:2181,zk2.internal:2181,zk3.internal:2181
- --web.listen-address=:9141
- --timeout=5
ports:
- "9141:9141"
Prometheus scrape 配置:
scrape_configs:
- job_name: 'zookeeper'
static_configs:
- targets:
- zookeeper-exporter:9141
relabel_configs:
- source_labels: [__address__]
target_label: instance
关键 Prometheus 指标#
# 节点是否存活(1=正常,0=不可达)
zk_up
# 当前服务器角色(leader=2, follower=1, standalone=3)
zk_server_state
# 活跃连接数
zk_num_alive_connections
# 请求积压(持续 > 0 需告警)
zk_outstanding_requests
# 平均处理延迟(ms)
zk_avg_latency
zk_max_latency
# ZNode 数量(监控数据集增长)
zk_znode_count
# 活跃 Watcher 数量
zk_watch_count
# 临时节点数量
zk_ephemerals_count
# 内存数据集大小(字节)
zk_approximate_data_size
# Leader 视角:Follower 同步状态
zk_followers
zk_synced_followers # 应等于 zk_followers
zk_pending_syncs # 应为 0
告警规则#
# zookeeper-alerts.yaml
groups:
- name: zookeeper.alerts
rules:
# 节点不可达
- alert: ZookeeperNodeDown
expr: zk_up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Zookeeper 节点 {{ $labels.instance }} 不可达"
# 集群失去 Quorum(3 节点集群中超过 1 个节点故障)
- alert: ZookeeperQuorumLost
expr: count(zk_up == 1) < 2
for: 30s
labels:
severity: critical
annotations:
summary: "Zookeeper 集群可能失去 Quorum,当前存活节点: {{ $value }}"
# Follower 与 Leader 不同步
- alert: ZookeeperFollowerNotSynced
expr: zk_synced_followers{} < zk_followers{}
for: 2m
labels:
severity: warning
annotations:
summary: "Zookeeper 存在未同步的 Follower: synced={{ $value }}"
# 请求积压
- alert: ZookeeperOutstandingRequestsHigh
expr: zk_outstanding_requests > 20
for: 2m
labels:
severity: warning
annotations:
summary: "Zookeeper 请求积压: {{ $value }} 个未处理请求"
# 连接数过高(接近 maxClientCnxns)
- alert: ZookeeperConnectionsHigh
expr: zk_num_alive_connections > 180
for: 5m
labels:
severity: warning
annotations:
summary: "Zookeeper 连接数过高: {{ $value }}(上限 200)"
# 平均延迟过高
- alert: ZookeeperLatencyHigh
expr: zk_avg_latency > 100
for: 5m
labels:
severity: warning
annotations:
summary: "Zookeeper 处理延迟过高: {{ $value }}ms"
# Watcher 数量异常(可能内存泄漏)
- alert: ZookeeperWatcherCountHigh
expr: zk_watch_count > 100000
for: 10m
labels:
severity: warning
annotations:
summary: "Zookeeper Watcher 数量异常: {{ $value }}"
Grafana Dashboard 配置#
推荐导入 Grafana Dashboard ID 10465(Zookeeper 3.x),重点面板:
- 集群健康状态:各节点
zk_up和角色分布 - 请求处理性能:
zk_avg_latency+zk_max_latency趋势 - 连接数监控:
zk_num_alive_connections时序图 - 数据集大小:
zk_approximate_data_size增长趋势 - Follower 同步状态:
zk_synced_followersvszk_followers
数据备份与迁移#
快照备份#
#!/bin/bash
# zk-backup.sh - 定期备份 Zookeeper 快照
BACKUP_DIR="/backup/zookeeper/$(date +%Y%m%d)"
DATA_DIR="/data/zookeeper/data/version-2"
TXLOG_DIR="/data/zookeeper/txlog/version-2"
S3_BUCKET="s3://my-backups/zookeeper"
mkdir -p "${BACKUP_DIR}"
# 备份最近的快照和对应的事务日志
ls -t "${DATA_DIR}"/snapshot.* | head -3 | xargs -I{} cp {} "${BACKUP_DIR}/"
ls -t "${TXLOG_DIR}"/log.* | head -10 | xargs -I{} cp {} "${BACKUP_DIR}/"
# 压缩并上传 S3
tar -czf "/tmp/zk-backup-$(date +%Y%m%d).tar.gz" "${BACKUP_DIR}"
aws s3 cp "/tmp/zk-backup-$(date +%Y%m%d).tar.gz" "${S3_BUCKET}/"
# 清理本地临时文件
rm -rf "${BACKUP_DIR}" "/tmp/zk-backup-$(date +%Y%m%d).tar.gz"
echo "备份完成: $(date)"
# crontab 每天凌晨 3 点执行备份
0 3 * * * /opt/scripts/zk-backup.sh >> /var/log/zk-backup.log 2>&1
数据迁移#
# 场景:将 Zookeeper 数据从旧集群迁移到新集群
# 方法:使用 zkCopy 工具(比手动重放快照更安全)
# 安装 zkcopy
pip install kazoo
# Python 迁移脚本
python3 << 'EOF'
from kazoo.client import KazooClient
import sys
def copy_zk_tree(src_client, dst_client, path="/"):
"""递归复制 ZNode 树"""
try:
data, stat = src_client.get(path)
# 在目标创建节点(跳过根节点)
if path != "/":
if not dst_client.exists(path):
dst_client.create(path, data, makepath=True)
else:
dst_client.set(path, data)
# 递归处理子节点
children = src_client.get_children(path)
for child in children:
child_path = f"{path}/{child}" if path != "/" else f"/{child}"
copy_zk_tree(src_client, dst_client, child_path)
except Exception as e:
print(f"Error copying {path}: {e}", file=sys.stderr)
src = KazooClient(hosts="old-zk1:2181,old-zk2:2181,old-zk3:2181")
dst = KazooClient(hosts="new-zk1:2181,new-zk2:2181,new-zk3:2181")
src.start()
dst.start()
# 只迁移需要的路径
for root_path in ["/kafka", "/dubbo", "/config"]:
print(f"迁移: {root_path}")
copy_zk_tree(src, dst, root_path)
src.stop()
dst.stop()
print("迁移完成")
EOF
云原生场景下的定位#
Zookeeper 的历史地位与现状#
Zookeeper 在 2010 年代是分布式协调的首选方案,但随着生态演进,它在新系统中的使用逐渐减少:
不推荐在新项目中引入 Zookeeper 的原因:
- 运维复杂:需要维护独立的 JVM 集群,故障影响面广
- 已有更好的替代:etcd(更轻量,K8s 生态原生)、Nacos(服务发现+配置)
- Kafka 已去 ZK 化:Kafka 4.0 彻底移除 ZK 依赖,新部署直接用 KRaft
- 云厂商托管成本:云上 Zookeeper 使用场景极少,独立维护性价比低
仍值得投入的场景:
- HBase 存量系统:无法短期替换,需要保障 ZK 稳定性
- Dubbo 2.x 老服务:升级到 Nacos 需要较长迁移周期
- 大数据平台(Hadoop/YARN):生命周期与集群绑定
运维建议:
- 使用托管 ZooKeeper(如 AWS MSK 内置、阿里云 Kafka 版内置),减少自建运维成本
- 制定明确的迁移计划,新业务不引入 ZK 依赖
- 存量系统每季度检查一次:能否用 etcd/Nacos 替换
# 评估现有 ZK 依赖的快速方法
# 查看 ZK 中注册的服务列表
echo ls / | zkCli.sh -server zk1:2181 2>/dev/null | grep "^\["
# 典型输出
[zookeeper, kafka, dubbo, hadoop-ha, hbase]
# 逐一确认哪些可以替换,哪些强依赖
ZK 的设计思想今天看依然值得学,但在生产里该让位就让位。会运维、也敢推着存量系统"毕业"——这两件事一起做好,才算真把它玩明白。






