主题
ZooKeeper 生产踩坑与运维
一句话:ZK 的生产事故基本落在磁盘、会话、连接三类;处置的关键不是背命令,而是先判断「还有没有 quorum」,再决定重启、恢复还是清数据。
适用版本:ZooKeeper 3.8 / 3.9。4lw 行为以 3.5.3+ 为准;更早版本行为不同,不能照搬本文的白名单结论。
为什么需要
ZK 通常不在业务请求链路上,但选主、锁、注册都依赖它。它挂掉时,业务可能先表现为「锁拿不到」「实例列表不更新」「任务不重新分片」,而不是「ZK 报错」。更麻烦的是,ZK 的恢复动作本身有顺序要求:单节点起不来就急着清数据,可能把唯一完好的副本删掉;过半挂了却只重启一台,集群仍然选不出 Leader。
核心机制
磁盘:事务日志和快照只增不减
每次写事务先追加到 dataDir(或 dataLogDir)下的事务日志,默认每约 snapCount=100000 条事务触发一次快照(实际有随机抖动)。日志不会自动删除,除非开启自动清理:
autopurge.purgeInterval=0(默认,单位小时,0 表示不清理)autopurge.snapRetainCount=3(默认保留最近 3 份快照及对应日志)
所以「ZK 磁盘会满」不是概率问题,是默认配置下的时间问题。生产要把 dataDir 和 dataLogDir 分盘,并监控目录增长;不要把数据目录放在 NFS 等共享文件系统上,ZK 依赖本地磁盘的 fsync 语义。
会话:客户端请求,服务端夹逼
sessionTimeout 由客户端提出,服务端把它夹在 2×tickTime ~ 20×tickTime 之间(minSessionTimeout / maxSessionTimeout 默认都是 -1,即取这两个倍数)。默认 tickTime=2000ms 时范围是 4s–40s;客户端请求 60s 会被截成 40s,想让 60s 生效要先把 tickTime 调到 3000ms。
Java 客户端的 ping 间隔约为协商后超时的 1/3(源码里 readTimeout=2/3×S,在 readTimeout/2 时发 ping),不是 tickTime/2。tickTime×syncLimit 是 Follower 允许落后 Leader 的同步上限,不是会话超时;把这两个公式混用,会得出完全错误的结论。
会话过期后,该会话的所有临时节点删除。GC 长停顿、网络分区、服务端长时间无响应都可能导致过期;客户端会收到 SessionExpired,此前的锁和注册全部失效。
连接:maxClientCnxns 是每 IP 每服务端的上限
maxClientCnxns=60(默认)限制单个 IP 到单个 ZK 服务端的连接数。应用每次请求都 new ZooKeeper()、用完不 close(),连接会持续累积;重新部署时旧连接还要等会话超时才释放,新 Pod 又建新连接,很容易打满。
排查用 cons 看每个连接的来源 IP 和 Session ID,按 IP 聚合;治理用 Curator 单例连接、优雅关闭、连接数监控。cons 输出可能很大,先 wc -l 看总量,再抽样。
Quorum:单节点恢复后不能选主
ZK 的写和选举都要求多数派:3 节点容忍 1 台,5 节点容忍 2 台,4 节点和 3 节点容错相同。只剩 1 台时,即使它数据完整,也无法选出 Leader;必须至少恢复 2/3(或 3/5)才有 quorum。全挂恢复时,先启动 zxid 最大的节点,再启动第二个节点形成多数派,最后启动其余节点。
4lw:3.5.3+ 默认只放行 srvr
四字命令(four-letter word,4lw)在 3.5.3+ 默认只允许 srvr(只读模式下还有 isro)。stat、cons、wchs、mntr、ruok 都需要显式加入白名单,否则返回「not in the whitelist」。白名单只在启动时读取,改完要滚动重启。
最小上手
在 zoo.cfg 中加入:
properties
4lw.commands.whitelist=srvr,ruok,mntr,stat,cons,wchs采集与判断:
bash
echo ruok | nc 127.0.0.1 2181 # 期望 imok(只说明进程在,不代表有 quorum)
echo srvr | nc 127.0.0.1 2181 # 看 Mode: leader/follower/standalone
echo mntr | nc 127.0.0.1 2181 # 指标:状态、延迟、堆积、znode、watch、连接
echo cons | nc 127.0.0.1 2181 | wc -l # 当前连接数
echo wchs | nc 127.0.0.1 2181 # Watch 总数与按路径汇总mntr 重点字段:
| 字段 | 判断 |
|---|---|
zk_server_state | leader / follower;looking 表示未选出 Leader |
zk_synced_followers vs zk_followers | 前者小于后者,说明有 Follower 没同步上 |
zk_outstanding_requests | 持续大于 0,说明请求堆积,查磁盘和网络 |
zk_avg_latency / zk_max_latency | 写延迟异常升高,先看 fsync 和磁盘 |
zk_znode_count / zk_ephemerals_count | 异常增长可能是客户端泄漏临时节点 |
zk_watch_count | 异常增长可能是 Watch 未重注册或羊群效应 |
zk_pending_syncs | 持续大于 0,说明 Follower 同步跟不上 |
生产动作
Runbook 1:单节点挂
判断:3 节点挂 1 台,剩余 2 台仍有 quorum;客户端通常无感,但不要继续重启其他节点。
bash
echo ruok | nc <node> 2181 # 无响应或非 imok
tail -200 zookeeper.out # 查 OOM、磁盘满、端口占用
zkServer.sh start # 只重启这一台
zkServer.sh status # 期望 follower 或 leader
echo mntr | nc <node> 2181 | grep -E 'zk_server_state|zk_synced_followers'判断依据:重启后 zk_server_state 为 follower,且 Leader 的 zk_synced_followers 恢复;其他节点日志没有新的 Exception。不要同时重启另外两台。
Runbook 2:过半挂
判断:3 节点挂 2 台,或 5 节点挂 3 台。剩余节点处于 LOOKING,stat 可能返回「not currently serving requests」,客户端读写失败。
bash
echo srvr | nc <survivor> 2181 # Mode: looking,说明没有 quorum
ls -l /data/version-2/log.* # 比较各节点最大 zxid
zkServer.sh start # 先启动 zxid 最大的宕机节点
zkServer.sh status
echo mntr | nc <node> 2181 | grep zk_server_state判断依据:至少 2/3(或 3/5)节点起来后,才会出现 Leader;zk_synced_followers 逐步追上。一次只启动一台,确认集群恢复后再启动下一台。不要清空任何节点的数据目录。
Runbook 3:全挂
判断:所有节点进程或主机不可用。恢复顺序是「数据最新的先起,凑够 quorum 再起其余」。
bash
# 1. 在每个节点确认 myid 存在,且与 server.N 对应
cat /data/myid
# 2. 找最大 zxid:日志和快照文件名中的数字
ls -1 /data/version-2/log.* /data/version-2/snapshot.* | sort -V | tail
# 3. 先启动 zxid 最大的节点,再启动第二大的节点
zkServer.sh start
zkServer.sh status
# 4. 两台形成 quorum 后,再启动剩余节点判断依据:前两台起来后应出现 leader + follower;如果两台都停在 LOOKING,检查 myid、zoo.cfg 的 server.N、防火墙和 2888/3888 端口。全挂后客户端会话已过期,临时节点全部消失;业务要重新注册实例、重新抢锁。
Runbook 4:数据损坏
判断:启动日志出现 EOFException、Invalid snapshot、Unable to load database,或节点反复重启。先确认其他节点仍有 quorum。
bash
tail -200 zookeeper.out | grep -E 'EOFException|Invalid snapshot|Unable to load'
ls -l /data/version-2/
# 仅对损坏的单节点:停进程,移走数据,保留 myid,让它从 Leader 全量同步
mv /data/version-2 /data/version-2.bak
cp /data/myid /data/myid.bak # 确认 myid 不被一起移走
zkServer.sh start判断依据:节点启动后 zk_server_state 变为 follower,zxid 追上 Leader,日志不再报损坏。清空 dataDir 必须保留 myid,否则节点身份丢失,可能以错误身份加入集群。若多数节点同时损坏,不要逐个清空;先停集群,从备份恢复,或用 zkTxnLogToolkit.sh 检查事务日志(参数以官方文档为准)。
边界
- 不要把
tickTime×syncLimit当会话超时。 它是 Follower 同步上限;会话超时是客户端请求后按2×tickTime ~ 20×tickTime协商。 - 不要单节点跑生产。 单节点没有 quorum,重启期间完全不可用,也无法验证选举和会话行为。
- 不要在 quorum 不足时清数据。 先确认还有其他完好副本,再处理损坏节点。
- 不要清空 dataDir 时连
myid一起删。 节点身份丢失比数据损坏更难恢复。 - 不要用
4lw.commands.whitelist=*。 全部放开会把cons、wchs等重命令暴露给任何能连 2181 的人。 - 不要把 ZK 当数据库或高吞吐写入系统。 单节点 1MB、写走 Leader、过半 fsync,容量和吞吐都不适合业务数据。
讲清楚
ZK 的事故先分三类:磁盘、会话、连接。磁盘看事务日志和快照有没有自动清理,
autopurge.purgeInterval默认 0,不开早晚会满;会话看客户端请求的超时是否被服务端夹到2×tickTime ~ 20×tickTime,GC 停顿超过会话超时就会失锁、掉临时节点;连接看maxClientCnxns和cons,多数是没关连接或没用单例。处置前先判断 quorum:3 节点挂 1 台还有多数派,挂 2 台就没有;单节点恢复后不能选主,至少要 2/3 才能选出 Leader。全挂时先起 zxid 最大的节点,再起第二台凑够 quorum。清 dataDir 必须保留 myid,否则节点身份就没了。把这些顺序记清楚,比记住某个命令更重要。