跳到正文

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_stateleader / 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,否则节点身份就没了。把这些顺序记清楚,比记住某个命令更重要。

把一门技术讲透到既懂也会用