05 生产运维——部署、监控与故障处理

摘要

ZooKeeper 是许多大数据和分布式系统的基础设施,其稳定性直接决定了上层系统(Kafka、HBase、Dubbo 等)的可用性。本文从 SRE 视角,系统梳理 ZooKeeper 生产部署的关键配置、监控指标体系、常见故障的根因分析与处理 SOP,以及在大流量场景下如何通过 Observer 节点进行水平扩展。


第 1 章 生产部署:配置与基础设施

1.1 服务器资源规划

ZooKeeper 对资源的需求与大多数中间件不同:

内存(最关键):DataTree 全量在 Heap 中,内存直接决定可以存储的 ZNode 数量上限。典型配置:

  • 小规模集群(< 10 万 ZNode):4~8GB Heap;
  • 中等规模(1050 万 ZNode):816GB Heap;
  • 大规模(> 50 万 ZNode):建议重新审视数据模型,而不是无限加内存。

磁盘

  • 事务日志盘(dataLogDir):必须是 专用 SSD,I/O 延迟直接影响写入延迟;生产中建议 NVMe SSD,容量 200~500GB 通常足够(配合 autopurge);
  • 快照盘(dataDir):普通 SSD 即可,主要用于存储快照文件;
  • 严禁与系统盘或数据盘混用

CPU:ZooKeeper 的 CPU 消耗并不高,通常 4~8 核足够。瓶颈几乎不在 CPU。

网络:Leader 与 Follower 之间的 ZAB 同步和客户端连接,带宽需求适中(100Mbps 通常足够),但网络延迟对写入延迟有直接影响,要求节点间延迟 < 5ms(同数据中心 < 1ms)。

1.2 核心 zoo.cfg 配置详解

# 基本时间单位(毫秒),所有超时配置的基础
tickTime=2000
 
# Follower 启动同步超时 = initLimit × tickTime = 10 × 2000 = 20s
# 如果跨机房部署,适当增大
initLimit=10
 
# 运行时 Leader-Follower 同步超时 = syncLimit × tickTime = 5 × 2000 = 10s
syncLimit=5
 
# 快照存储目录(每个节点独立)
dataDir=/data/zookeeper/data
 
# 事务日志目录(必须专用 SSD,与 dataDir 分开!)
dataLogDir=/data/zookeeper/logs
 
# 客户端监听端口
clientPort=2181
 
# 最大客户端连接数(默认 60,高并发场景需要增大)
maxClientCnxns=1000
 
# 触发快照的事务数阈值
snapCount=100000
 
# 自动清理:保留 5 个快照
autopurge.snapRetainCount=5
 
# 自动清理间隔(小时)
autopurge.purgeInterval=24
 
# 集群成员配置(server.N = hostname:peer_port:leader_election_port)
server.1=zk-node1:2888:3888
server.2=zk-node2:2888:3888
server.3=zk-node3:2888:3888
# Observer 节点配置:在 hostname 前加 observer 标记
# server.4=zk-node4:2888:3888:observer

在每个节点的 dataDir 目录下,需要创建 myid 文件,写入该节点对应的 server.N 中的 N:

# 在 zk-node1 上
echo "1" > /data/zookeeper/data/myid
 
# 在 zk-node2 上
echo "2" > /data/zookeeper/data/myid

1.3 JVM 参数配置

ZooKeeper 的 JVM 参数通过 JVMFLAGS 环境变量或修改启动脚本设置:

# zkEnv.sh 或环境变量
export JVMFLAGS="-Xms8g -Xmx8g 
  -XX:+UseG1GC 
  -XX:MaxGCPauseMillis=100
  -XX:+PrintGCDetails 
  -XX:+PrintGCDateStamps 
  -Xloggc:/var/log/zookeeper/gc.log
  -XX:+UseGCLogFileRotation 
  -XX:NumberOfGCLogFiles=5 
  -XX:GCLogFileSize=20M"

关键点:

  • -Xms = -Xmx:防止 JVM 在运行时扩展 Heap 触发 Full GC;
  • G1GC:比 ParallelGC 更适合 ZooKeeper 的长时间运行场景,目标暂停时间 100ms;
  • GC 日志:ZooKeeper 的 GC 暂停会直接导致心跳超时和 Leader 选举,GC 日志是故障排查的必备信息。

生产避坑

ZooKeeper 的 Heap 大小不能过大。如果 Heap 设置为 32GB,G1GC 的一次 Full GC 可能暂停数秒——这超过了 syncLimit × tickTime = 10s,会导致 Follower 认为 Leader 已宕机,触发不必要的 Leader 重选举,引发集群震荡。建议 ZooKeeper 的 Heap 控制在 4~16GB,并确保 Old Gen 的使用率长期稳定(没有内存泄漏)。

1.4 操作系统层面的优化

# 1. 关闭 swap(ZooKeeper 对 swap 极度敏感)
swapoff -a
echo "vm.swappiness=0" >> /etc/sysctl.conf
sysctl -p
 
# 2. 增大文件描述符限制
echo "* soft nofile 65536" >> /etc/security/limits.conf
echo "* hard nofile 65536" >> /etc/security/limits.conf
 
# 3. 磁盘调度算法(SSD 使用 mq-deadline 或 none)
echo mq-deadline > /sys/block/nvme0n1/queue/scheduler
 
# 4. 网络参数优化(大量客户端连接场景)
echo "net.core.somaxconn=65536" >> /etc/sysctl.conf
echo "net.ipv4.tcp_max_syn_backlog=65536" >> /etc/sysctl.conf
sysctl -p

第 2 章 Observer 节点:扩展读吞吐量

2.1 为什么需要 Observer

ZooKeeper 的写吞吐受限于 quorum——每次写操作需要超过半数节点的 ACK。增加节点数量会增加 quorum 要求,反而可能降低写吞吐(需要更多节点的 ACK)。

但读请求可以在任意节点上执行,读吞吐可以通过增加节点来水平扩展。问题是:增加普通 Follower 节点会增加 quorum,降低写性能。

Observer 是这个矛盾的解决方案:Observer 节点接受来自客户端的读请求,但不参与 ZAB 协议的投票(不计入 quorum)。因此:

  • 增加 Observer 节点,可以扩展读吞吐量;
  • Observer 不影响写性能(不增加 quorum 要求);
  • Observer 的数据通过 Leader 异步推送,可能比 Follower 更滞后。

2.2 Observer 的适用场景

跨数据中心部署:如果在 3 个数据中心部署 ZooKeeper(每个机房 1 个节点,共 3 节点),网络延迟会影响写操作的 quorum 确认时间。

一种更好的方案:在主机房部署 3 个 Follower(quorum 确认在主机房内快速完成),在异地机房部署 Observer(只复制数据,不参与投票,延迟不影响写吞吐)。异地的客户端(如 DR 站点的消费者)连接到 Observer 读取配置,主机房的写操作不受跨机房延迟影响。

高读取负载场景:某些场景有大量客户端需要监听同一组 ZNode(如服务发现场景,数千个消费方监听同一服务的提供者列表),大量 Watcher 通知分发会给 Follower 带来压力。通过增加 Observer 分担连接,缓解 Follower 的负载。

2.3 Observer 配置

zoo.cfg 中,Observer 节点的配置与 Follower 的区别只是在 server 行末尾加上 :observer

# 普通 Follower
server.1=zk-node1:2888:3888
server.2=zk-node2:2888:3888
server.3=zk-node3:2888:3888
 
# Observer(不参与 quorum)
server.4=zk-node4:2888:3888:observer
server.5=zk-node5:2888:3888:observer

在 Observer 节点自己的 zoo.cfg 中,需要添加:

peerType=observer

第 3 章 监控指标体系

3.1 四字命令监控

ZooKeeper 提供了”四字命令”(Four Letter Words)作为快速诊断工具:

# 最常用:综合统计信息
echo mntr | nc <zk-host> 2181
 
# 快速健康检查(CI/CD 中常用)
echo ruok | nc <zk-host> 2181
 
# 当前连接的详细信息
echo dump | nc <zk-host> 2181
 
# 环境信息(版本、OS 等)
echo envi | nc <zk-host> 2181

mntr 的关键指标(需监控):

指标正常范围告警条件
zk_avg_latency< 10ms> 50ms 持续 5 分钟
zk_max_latency< 100ms> 1000ms
zk_outstanding_requests0~5> 50 持续 1 分钟
zk_znode_count业务决定超过 100 万(内存压力)
zk_watch_count业务决定超过 50 万(内存压力)
zk_open_file_descriptor_count< 60% 上限> 80% 上限
zk_packets_sent/received趋势稳定突然下降(可能断连)

3.2 JMX 监控与 Prometheus

ZooKeeper 3.6+ 内置了 Prometheus 格式的指标暴露端口(需配置 metricsProvider.className):

# zoo.cfg
metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider
metricsProvider.httpHost=0.0.0.0
metricsProvider.httpPort=7000
metricsProvider.exportJvmInfo=true

配置后,访问 http://zk-host:7000/metrics 即可获取 Prometheus 格式的指标。

对于旧版 ZooKeeper,使用 zookeeper_exporter(第三方工具)将四字命令输出转换为 Prometheus 格式。

3.3 关键告警规则

必须立即响应(P0):

# Leader 选举发生(正常情况下不应频繁发生)
- alert: ZookeeperLeaderElection
  expr: changes(zookeeper_election_term[5m]) > 0
  annotations:
    summary: "ZooKeeper 发生 Leader 选举,请检查节点稳定性"
 
# 节点离线
- alert: ZookeeperNodeDown
  expr: up{job="zookeeper"} == 0
  annotations:
    summary: "ZooKeeper 节点 {{ $labels.instance }} 离线"

需要当天处理(P1):

# 请求延迟过高
- alert: ZookeeperHighLatency
  expr: zookeeper_avg_latency > 50
  for: 5m
  annotations:
    summary: "ZooKeeper 平均延迟超过 50ms,持续 5 分钟"
 
# 待处理请求积压
- alert: ZookeeperOutstandingRequests
  expr: zookeeper_outstanding_requests > 50
  for: 1m
  annotations:
    summary: "ZooKeeper 请求积压超过 50 个,可能过载"
 
# 磁盘空间不足
- alert: ZookeeperDiskLow
  expr: node_filesystem_avail_bytes{mountpoint="/data/zookeeper"} / 
        node_filesystem_size_bytes{mountpoint="/data/zookeeper"} < 0.2
  annotations:
    summary: "ZooKeeper 数据目录磁盘剩余不足 20%"

第 4 章 常见故障的根因分析与处理

4.1 故障一:Leader 频繁重选举

现象:ZooKeeper 日志中频繁出现 LOOKING 状态,应用层出现大量 Connection Loss 异常,业务短暂中断(每次选举 1~5 秒)。

根因排查:

原因 A:GC Pause 导致心跳超时
  检查方法:查看 gc.log,寻找超过 syncLimit×tickTime(默认 10s)的 GC 暂停
  解决方案:
    - 减小 Heap(ZooKeeper 的 Heap 不需要很大,8GB 通常足够)
    - 调整 G1GC 参数:-XX:MaxGCPauseMillis=200,-XX:G1HeapRegionSize=16m
    - 如果 Old Gen 持续增长,检查是否存在内存泄漏(过多的 Watcher、Session 积累)

原因 B:磁盘 IO 过高导致 fsync 超时
  检查方法:iostat -x 1 10(查看 await 和 util),
            同时检查 ZooKeeper 日志的 fsync 耗时(grep "fsync" zookeeper.log)
  解决方案:
    - 将事务日志迁移到专用 SSD
    - 检查是否有其他进程占用磁盘 IO(同磁盘的 Kafka 或数据库)

原因 C:网络分区或高延迟
  检查方法:ping 各节点之间的延迟,查看是否有丢包
            tcpdump 抓包分析 ZooKeeper peer 端口(2888)的延迟
  解决方案:
    - 排查网络设备(交换机、防火墙)
    - 增大 syncLimit(如从 5 增大到 10),给网络更多容忍度

4.2 故障二:客户端大量 Connection Loss

现象:应用日志中大量 org.apache.zookeeper.KeeperException$ConnectionLossException,ZooKeeper 服务端本身看起来正常。

根因排查:

原因 A:ZooKeeper 连接数达到上限(maxClientCnxns)
  检查方法:echo mntr | nc zk-host 2181 | grep connections
  解决方案:增大 maxClientCnxns(如从 60 增大到 1000)

原因 B:Session 超时(客户端 GC 或处理慢导致心跳未发送)
  检查方法:客户端 GC 日志(是否有超过 SessionTimeout 的暂停)
            ZooKeeper 服务端日志(grep "SessionExpiredException")
  解决方案:
    - 增大 SessionTimeout(如从 10s 增大到 30s)
    - 优化客户端 GC(减少长暂停)

原因 C:ZooKeeper 服务端过载(outstanding_requests 积压)
  检查方法:echo mntr | nc zk-host 2181 | grep outstanding
  解决方案:
    - 增加 Observer 节点分担读请求
    - 检查是否有异常的大量写请求(某个客户端的 bug 导致频繁写)
    - 升级硬件(内存/SSD)

4.3 故障三:集群无法选出 Leader(stuck in LOOKING)

现象:所有节点都处于 LOOKING 状态,无法选出 Leader,集群完全不可用。

根因分析:

这种情况通常意味着存活节点数 < quorum(不足半数节点在线):

3 节点集群,需要 2 个节点存活。如果有 2 个节点同时宕机:
  node1: 运行中(LOOKING 状态)
  node2: 宕机
  node3: 宕机
  
node1 单独无法达到 quorum(需要 2/3 投票),永远无法选出 Leader

处理流程:

步骤 1:确认节点状态
  echo stat | nc zk-node1 2181
  echo stat | nc zk-node2 2181
  echo stat | nc zk-node3 2181

步骤 2:尝试恢复宕机节点
  - 检查宕机原因(OOM? 磁盘满? 进程被 kill?)
  - 解决根因后重启:systemctl start zookeeper

步骤 3:如果只有 1 个节点存活且其他节点数据无法恢复
  ⚠️ 这是数据丢失的场景,需要评估可接受的数据丢失范围
  
  方案 A(推荐):等待其他节点硬件修复后再恢复
  
  方案 B(紧急,可能丢失数据):
    在存活节点上临时修改 zoo.cfg 为单节点模式:
    删除 server.2 和 server.3 的配置行
    重启该节点,它会以单节点模式运行
    恢复服务后,尽快将 zoo.cfg 恢复为集群模式并重新加入其他节点

生产避坑

将集群临时降为单节点模式是极度危险的操作,只应在完全无法等待其他节点恢复的极端情况下使用。单节点模式没有数据冗余,任何写操作都是单点,一旦该节点再次宕机,数据可能永久丢失。执行前必须通知所有相关方,并在其他节点恢复后立即还原集群模式。

4.4 故障四:磁盘满导致 ZooKeeper 停止服务

现象dataLogDir 所在磁盘使用率达到 100%,ZooKeeper 无法写入事务日志,所有写操作失败,服务端报 IOException

紧急处理步骤:

# 步骤 1:暂停 ZooKeeper 服务(避免继续写入积累错误)
systemctl stop zookeeper
 
# 步骤 2:清理旧日志文件(保留最近 3 个快照)
java -cp /opt/zookeeper/lib/*:/opt/zookeeper/*.jar \
  org.apache.zookeeper.server.PurgeTxnLog \
  /data/zookeeper/data /data/zookeeper/logs -n 3
 
# 步骤 3:确认磁盘空间释放
df -h /data/zookeeper/logs
 
# 步骤 4:重启 ZooKeeper
systemctl start zookeeper
 
# 步骤 5:验证集群恢复
echo mntr | nc localhost 2181 | grep -E "zk_server_state|zk_avg_latency"
 
# 步骤 6:配置 autopurge 防止复发
# zoo.cfg 中添加:
# autopurge.snapRetainCount=5
# autopurge.purgeInterval=24

第 5 章 ZooKeeper 集群的扩缩容

5.1 添加新节点

向正在运行的 ZooKeeper 集群添加节点需要滚动重启

步骤 1:在 zoo.cfg 的集群成员列表中添加新节点(所有现有节点和新节点都要更新)
步骤 2:逐一重启现有节点(一次一个,等待该节点重新加入集群后,再重启下一个)
步骤 3:启动新节点(它会自动从 Leader 同步数据)

注意:在添加节点之前,要仔细计算 quorum 变化。从 3 节点集群变为 4 节点集群,quorum 从 2 变为 3,这意味着写操作需要更多节点确认,可能略微影响写性能。

5.2 移除节点

从集群中移除一个节点(如永久下线):

步骤 1:确保剩余节点数 > quorum(移除后仍然能形成多数派)
步骤 2:停止要移除节点上的 ZooKeeper 服务
步骤 3:从所有节点的 zoo.cfg 中删除该节点的配置行
步骤 4:滚动重启所有剩余节点(逐一重启,等待重加入后再重启下一个)

5.3 升级 ZooKeeper 版本

滚动升级步骤(以 3.5 → 3.6 为例):

步骤 1:阅读目标版本的 Release Notes,确认 Breaking Changes
步骤 2:在测试环境验证新版本与现有客户端(Curator 版本)的兼容性
步骤 3:逐一升级 Follower 节点(停服 → 升级软件包 → 启动 → 验证加入集群)
步骤 4:最后升级 Leader 节点(会触发一次 Leader 重选举)
步骤 5:验证整个集群正常运行(各节点 stat、mntr 输出正常)

小结

本文从 SRE 视角系统梳理了 ZooKeeper 的生产运维:

  • 部署配置:事务日志必须使用专用 SSD;Heap 控制在 4~16GB 防止 GC 导致选举震荡;JVM 参数开启 G1GC 和 GC 日志;
  • Observer 节点:在不影响写性能的前提下水平扩展读吞吐量,适合高读取负载和跨机房场景;
  • 监控告警mntr 四字命令提供核心指标,zk_outstanding_requestszk_avg_latency 是最重要的过载信号;Leader 选举告警是最关键的 P0 告警;
  • 常见故障:Leader 频繁选举多为 GC 或磁盘 IO 问题;Connection Loss 多为连接数上限或 Session 超时;集群不可用需先确认存活节点是否满足 quorum;磁盘满需立即清理日志并配置 autopurge。

下一篇文章将从整体视角,对比 ZooKeeper 与 ETCD 的架构差异,并梳理 ZooKeeper 在 Kafka 新版本中被去除的背景与意义。


思考题

  1. ZooKeeper 的 Session 超时时间(sessionTimeout)决定了临时节点的生存期。设置过短(如 2 秒)可能因网络抖动导致频繁的 Session 断开和重连——触发大量临时节点删除。设置过长(如 60 秒)导致节点故障后很久才被检测到。在 Kafka 使用 ZooKeeper 的场景中,Session 超时如何影响 Broker 故障检测的速度?
  2. ZooKeeper 的事务日志(Transaction Log)需要写入磁盘——forceSync=yes(默认)每次事务都 fsync。在 SSD 上 fsync 延迟约 0.1ms,在 HDD 上约 10ms。将事务日志放在独立的 SSD 盘上可以显著提升写性能。dataLogDirdataDir 分离是常见的最佳实践——为什么?
  3. ZooKeeper 的 JVM 堆大小通常设为 3-4GB 足够(因为数据量不大)。但 GC 暂停可能导致 Leader 被误认为’已死’并触发重新选举。你应该选择什么 GC 算法(如 G1)和参数来最小化 GC 暂停?tickTimeinitLimit/syncLimit 的设置如何与 GC 暂停时间配合?