1. 为什么要把 Zookeeper 从 Kafka 里拆出来单独搭
很多团队第一次接触 Kafka 的时候,都是直接用 Kafka 安装包里自带的那个 Zookeeper。解压、改两个配置、bin/zookeeper-server-start.sh一跑,Kafka 也跟着起来了,看起来省事。但只要集群规模稍微大一点,或者你手里不止 Kafka 一个系统要用 Zookeeper,这种“捆绑式”的玩法很快就会变成运维噩梦。
我先把结论摆在这里:Kafka 自带的 Zookeeper 只适合本地开发和功能验证,生产环境强烈建议单独搭建一套独立的 Zookeeper 集群。原因不复杂,但每一条都是踩过坑才明白的。
1.1 捆绑 Zookeeper 的三个致命问题
第一个问题是生命周期被绑死。Kafka 自带的 Zookeeper 用的是 Kafka 自己的启动脚本,它的数据目录、日志目录、端口配置全都跟 Kafka 的安装路径耦合在一起。你想单独重启 Zookeeper 而不影响 Kafka?做不到。你想升级 Zookeeper 版本?对不起,得跟着 Kafka 的版本走。Kafka 发行包里带的 Zookeeper 版本往往是固定的,比如某个 Kafka 版本锁死了 Zookeeper 3.5.x,而你的其他系统需要 3.6+ 的特性,这时候就非常尴尬。
第二个问题是多系统争抢。现实中一个 Zookeeper 集群往往不只服务 Kafka。比如 HBase、Solr、Dubbo 的注册中心、分布式锁服务,都可能挂在同一个 ZK 上。如果这个 ZK 是 Kafka 自带的,那其他系统就得蹭 Kafka 的进程,权限、配额、监控全都混在一起。一旦 Kafka 那边做滚动重启,其他系统跟着抖三抖,这种耦合在生产环境里是绝对不能接受的。
第三个问题是运维边界模糊。独立 ZK 集群可以有自己的监控面板、自己的告警规则、自己的扩缩容节奏。而捆绑式的 ZK,你很难说清楚“这个 ZK 节点挂了到底该找 Kafka 团队还是找基础设施团队”。职责不清,故障恢复就慢。
1.2 独立部署带来的实际收益
单独搭 ZK 集群之后,最直观的变化是升级和扩缩容解耦。你可以先把 ZK 从 3.5 升到 3.8,观察几天稳定性,再单独升级 Kafka,两边互不影响。其次是资源隔离,ZK 是典型的 CPU 敏感、内存敏感型服务,独立部署后可以给它固定的 CPU 核数和堆内存,不会被 Kafka 的页缓存挤占。
再一个容易被忽略的点是版本可查。Kafka 自带的 ZK,你有时候根本不知道它到底是哪个小版本,因为 Kafka 的发行包不会在显眼位置标注 ZK 版本。而独立部署的 ZK,版本信息一目了然,排查兼容性问题时省心太多。这也是本文标题里“如何查看 Zookeeper 版本”这个需求背后的真实痛点——很多人被问起“你们 ZK 什么版本”时,只能支支吾吾。
提示:如果你的 Kafka 集群节点数少于 3 台,或者只是做本地功能测试,用自带 ZK 完全没问题。但只要涉及生产、涉及多系统共用、涉及版本管理,就老老实实拆出来。
2. 独立 Zookeeper 集群的架构设计与选型考量
决定要单独搭之后,下一步不是急着敲命令,而是先把架构想清楚。ZK 集群的架构看着简单,但里面的门道不少,尤其是节点数量、数据目录、网络配置这几块,选错了后面改起来很痛苦。
2.1 集群节点数量为什么必须是奇数
Zookeeper 用的是ZAB 协议(Zookeeper Atomic Broadcast),它的选举和写操作都需要“过半数”确认。这就决定了集群节点数必须是奇数,常见的是 3 节点、5 节点、7 节点。
为什么不是偶数?假设你搭了 4 个节点,过半数就是 3。这时候如果挂掉 2 个,剩下 2 个达不到 3,整个集群不可用。而 3 节点集群挂掉 1 个,剩下 2 个达到过半数 2,依然可用。也就是说,4 节点的容错能力和 3 节点一样,但多了一台机器的成本,纯属浪费。5 节点能容忍 2 台故障,7 节点能容忍 3 台,以此类推。
生产环境我一般推荐3 节点起步,5 节点封顶。3 节点适合中小规模,5 节点适合对可用性要求极高的场景。超过 7 个节点,写性能反而会因为广播开销增大而下降,得不偿失。
2.2 数据目录与事务日志的分离策略
ZK 有两类关键数据:快照(snapshot)和事务日志(transaction log)。快照是内存数据的定期落盘,事务日志是每次写操作的追加记录。默认情况下,这两者都放在dataDir下面,但这是性能杀手。
原因在于,事务日志是顺序追加写,对磁盘的同步写性能要求极高;而快照是全量写,数据量大但频率低。如果两者混在同一个磁盘上,快照写入时会阻塞事务日志的追加,导致写延迟飙升。正确的做法是:
dataDir指向一块普通磁盘,存放快照;dataLogDir指向另一块高性能 SSD,专门存放事务日志。
这个配置项叫dataLogDir,在zoo.cfg里设置。我实测过,把事务日志单独放到 SSD 上,写吞吐能提升 2 到 3 倍,尤其是在高并发写入场景下,效果非常明显。
2.3 myid 与集群成员标识
每个 ZK 节点都需要一个唯一的整数 ID,叫myid,取值范围是 1 到 255。这个 ID 写在dataDir目录下的一个名为myid的文件里,内容就是纯数字。
myid必须和zoo.cfg里server.N配置的 N 对应。比如某台机器的myid是 2,那么zoo.cfg里就必须有server.2=host:2888:3888这一行。搞错了会导致节点无法加入集群,日志里会报 “Invalid server id” 之类的错误。
注意:
myid一旦确定,尽量不要改。改myid相当于换了一个节点身份,需要重新同步全量数据,代价很大。
3. 从零搭建三节点 Zookeeper 集群的完整实操
架构想清楚之后,进入动手环节。下面这套流程是我在多个环境里反复验证过的,按顺序执行基本不会出问题。假设我们有三台机器,主机名分别是 zk-node1、zk-node2、zk-node3,IP 分别是 10.0.0.11、10.0.0.12、10.0.0.13。
3.1 环境准备与 JDK 安装
ZK 是 Java 写的,所以第一步是装 JDK。推荐JDK 8 或 JDK 11,这两个版本跟 ZK 的兼容性最好。JDK 17 虽然也能跑,但某些老版本 ZK 会有反射相关的警告,生产环境建议保守一点。
# 以 CentOS 为例,安装 OpenJDK 11 yum install -y java-11-openjdk java-11-openjdk-devel # 验证 java -version装完之后设置JAVA_HOME环境变量,写入/etc/profile.d/java.sh:
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk export PATH=$JAVA_HOME/bin:$PATH然后source /etc/profile.d/java.sh让它生效。三台机器都要做这一步。
接下来关闭防火墙或者放行 ZK 需要的端口。ZK 用到三个端口:
| 端口 | 用途 | 说明 |
|---|---|---|
| 2181 | 客户端连接 | 客户端和运维工具连这个 |
| 2888 | 集群内部通信 | Follower 连 Leader 用 |
| 3888 | 选举通信 | 选主时使用 |
# 放行端口(以 firewalld 为例) firewall-cmd --permanent --add-port=2181/tcp firewall-cmd --permanent --add-port=2888/tcp firewall-cmd --permanent --add-port=3888/tcp firewall-cmd --reload3.2 下载解压与目录规划
从 ZK 官网下载稳定版,我一般选3.8.x系列,比如 3.8.4,这个版本在稳定性和特性之间平衡得比较好。下载后解压到统一目录:
mkdir -p /opt/zookeeper tar -zxvf apache-zookeeper-3.8.4-bin.tar.gz -C /opt/zookeeper cd /opt/zookeeper ln -s apache-zookeeper-3.8.4-bin current用软链接current指向具体版本,以后升级只需要改软链接,配置文件路径不用动,这是个很实用的小技巧。
然后规划数据目录:
mkdir -p /data/zookeeper/data mkdir -p /data/zookeeper/logs/data/zookeeper/data放快照和 myid,/data/zookeeper/logs放事务日志。如果你的机器有两块盘,把 logs 放到 SSD 那块上。
3.3 zoo.cfg 配置文件逐项拆解
进入conf目录,复制一份模板:
cd /opt/zookeeper/current/conf cp zoo_sample.cfg zoo.cfg然后编辑zoo.cfg,三台机器的内容基本一致,只有myid不同。核心配置如下:
# 心跳间隔,单位毫秒,默认 2000 tickTime=2000 # Follower 初始连接 Leader 的超时时间,单位是 tickTime 的倍数 initLimit=10 # Follower 与 Leader 之间心跳超时,单位是 tickTime 的倍数 syncLimit=5 # 数据目录,存放快照和 myid dataDir=/data/zookeeper/data # 事务日志目录,单独指定 dataLogDir=/data/zookeeper/logs # 客户端端口 clientPort=2181 # 集群成员列表 server.1=10.0.0.11:2888:3888 server.2=10.0.0.12:2888:3888 server.3=10.0.0.13:2888:3888 # 最大客户端连接数 maxClientCnxns=600 # 自动清理快照和日志,保留最近 10 个 autopurge.snapRetainCount=10 autopurge.purgeInterval=24这里重点说几个参数。initLimit=10配合tickTime=2000,意味着初始连接超时是 20 秒。如果集群跨机房或者网络延迟高,这个值要适当调大,比如 20 或 30。syncLimit=5表示心跳超时是 10 秒,超过这个时间 Leader 就认为 Follower 挂了。
autopurge这两个参数非常关键。ZK 默认不会自动清理旧快照和日志,跑久了磁盘会被撑爆。purgeInterval=24表示每 24 小时清理一次,snapRetainCount=10表示保留最近 10 个。生产环境一定要开。
3.4 写入 myid 并启动集群
在每台机器上写入各自的 myid:
# zk-node1 echo "1" > /data/zookeeper/data/myid # zk-node2 echo "2" > /data/zookeeper/data/myid # zk-node3 echo "3" > /data/zookeeper/data/myid然后按顺序启动,先启动 node1,再 node2,最后 node3:
/opt/zookeeper/current/bin/zkServer.sh start启动后查看状态:
/opt/zookeeper/current/bin/zkServer.sh status正常输出应该是Mode: leader或Mode: follower。三台里应该有一台 leader,两台 follower。如果全是 follower 或者全是 looking,说明选举没成功,需要查日志。
3.5 验证集群可用性
用zkCli.sh连上去做一次读写测试:
/opt/zookeeper/current/bin/zkCli.sh -server 10.0.0.11:2181 # 进入交互界面后 create /test-node "hello-zk" get /test-node如果能在 node1 上创建,然后在 node2 上get到同样的值,说明集群同步正常。再故意停掉 leader,观察剩下两台是否能重新选出 leader,这是验证高可用的关键一步。
4. 如何准确查看 Zookeeper 的版本信息
版本查询这个需求看着简单,但实际场景里花样很多。不同部署方式、不同权限,能用的方法不一样。我把常用的几种方法都列出来,你按自己的环境挑。
4.1 通过启动脚本查看版本
最直接的方法是用zkServer.sh的version子命令:
/opt/zookeeper/current/bin/zkServer.sh version输出类似:
ZooKeeper JMX enabled by default Using config: /opt/zookeeper/current/bin/../conf/zoo.cfg Apache ZooKeeper, version 3.8.4 - 2024-02-xx xx:xx UTC这个方法不需要 ZK 正在运行,只要脚本和 jar 包在就能查。适合部署前确认版本。
4.2 通过客户端连接查看
如果 ZK 正在运行,可以用zkCli.sh连上去,然后执行:
/opt/zookeeper/current/bin/zkCli.sh -server 10.0.0.11:2181进入交互界面后,直接输入:
version会返回类似ZooKeeper -server host:port -client version 3.8.4的信息。这个方法的好处是能确认实际运行中的版本,而不是磁盘上的版本。有时候升级了 jar 包但没重启,磁盘版本和运行版本会不一致,用这个方法就能发现。
4.3 通过四字命令查看
ZK 提供了一组“四字命令”,用来获取运行时状态。其中stat命令会返回版本信息:
echo stat | nc 10.0.0.11 2181输出里有一行Zookeeper version: 3.8.4-...。不过要注意,四字命令默认在白名单里可能没开,需要在zoo.cfg里加:
4lw.commands.whitelist=stat,ruok,conf,mntr不加的话会返回stat is not executed because it is not in the whitelist。这是 3.5 之后引入的安全机制,很多人第一次用会懵。
4.4 通过 jar 包名反查版本
如果 ZK 进程正在跑,但你没有配置文件权限,可以用ps看进程启动命令:
ps -ef | grep zookeeper | grep -v grep输出里会包含zookeeper-3.8.4.jar这样的路径,版本号一目了然。这个方法在排查“别人搭的 ZK 到底什么版本”时特别好用。
4.5 版本查看方法对比
| 方法 | 是否需要运行 | 是否需要权限 | 适用场景 |
|---|---|---|---|
| zkServer.sh version | 否 | 脚本执行权限 | 部署前确认 |
| zkCli.sh version | 是 | 客户端连接权限 | 确认运行版本 |
| 四字命令 stat | 是 | 需白名单开启 | 远程快速查询 |
| ps 查 jar 包 | 是 | 进程查看权限 | 排查他人部署 |
提示:如果你在容器里跑 ZK,
ps可能看不到完整命令,这时候优先用zkServer.sh version或者进容器看 jar 包名。
5. 常见问题与排查技巧实录
搭 ZK 集群的过程中,报错是家常便饭。我把这些年遇到的高频问题和排查思路整理成一张速查表,再补充几个文档里不会写的坑。
5.1 集群启动失败排查速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 状态一直是 looking | 选举未完成 | 检查 3888 端口是否互通 |
| 报 Invalid server id | myid 与 server.N 不匹配 | 核对 myid 文件和 zoo.cfg |
| 连接被拒绝 | 防火墙未放行 | telnet 测试 2181 端口 |
| 启动后立即退出 | 数据目录权限不足 | 检查 dataDir 属主 |
| 日志报 No space left | 快照未清理 | 开启 autopurge |
| 客户端超时 | initLimit 太小 | 适当调大该值 |
5.2 几个文档里不会写的坑
第一个坑是主机名解析。zoo.cfg里server.N后面跟的主机名,必须能被所有节点解析。如果你写的是主机名而不是 IP,就要确保/etc/hosts里三台机器都配了对应关系。我见过有人只在一台机器上配了 hosts,结果另外两台解析失败,集群死活起不来。
第二个坑是时钟同步。ZK 的选举和心跳依赖时间,如果三台机器时间差超过tickTime的若干倍,会出现莫名其妙的脑裂。生产环境务必开 NTP 同步,这是硬性要求。
第三个坑是 JVM 堆内存。ZK 默认的堆内存可能只有 512MB 或 1GB,节点多了或者 watch 多了会 OOM。建议在zkServer.sh里或者通过JVMFLAGS设置-Xmx2g -Xms2g,具体大小看你的数据量和连接数。一般来说,2GB 到 4GB 足够支撑中等规模集群。
第四个坑是快照文件过大。如果单个快照超过几百 MB,说明内存里数据太多,或者snapRetainCount设得太大。ZK 的全量快照在启动加载时会很慢,影响恢复时间。定期检查dataDir下的快照大小,必要时手动清理。
5.3 滚动重启的正确姿势
生产环境升级或者改配置,不能三台一起停。正确做法是逐台重启:先停一台 follower,等它重启并同步完成,再停第二台 follower,最后停 leader。每次重启后都要用zkServer.sh status确认状态正常,再进行下一台。
停 leader 的时候,集群会自动选出新 leader,这个过程通常几秒到十几秒。如果你的客户端对短暂不可用敏感,要在业务低峰期操作,并且客户端配置里要有重试机制。
注意:滚动重启期间,写操作可能会有短暂失败,读操作一般不受影响。提前跟业务方沟通好窗口期。
6. 独立 ZK 集群与 Kafka 的对接要点
ZK 集群搭好之后,最后一步是让 Kafka 连上它。这一步配置很简单,但有几个细节容易出错。
6.1 Kafka 的 zookeeper.connect 配置
在 Kafka 的server.properties里,把zookeeper.connect指向你的独立 ZK 集群:
zookeeper.connect=10.0.0.11:2181,10.0.0.12:2181,10.0.0.13:2181/kafka注意最后那个/kafka,这是chroot 路径。它的作用是把 Kafka 的元数据写到 ZK 的一个子目录下,而不是根目录。这样做的好处是,同一个 ZK 集群可以给多个 Kafka 集群或者多个系统共用,彼此数据隔离,互不干扰。
如果你不加 chroot,Kafka 的节点信息会直接写在 ZK 根目录下,跟其他系统的数据混在一起,非常乱。所以强烈建议加上 chroot 路径,哪怕你只有一个 Kafka 集群。
6.2 从自带 ZK 迁移到独立 ZK 的注意事项
如果你已经有正在运行的 Kafka 集群,想从自带 ZK 迁移到独立 ZK,不能直接改配置重启。因为两边的 ZK 数据是不通的,Kafka 会认为自己是全新的集群,导致元数据丢失。
正确的迁移方式是:先搭建独立 ZK 集群,然后用 ZK 自带的zkServer.sh或者第三方工具做数据同步,把原 ZK 的/kafka路径下的数据复制到新 ZK。同步完成后再改 Kafka 配置,逐台滚动重启。这个过程比较复杂,建议在测试环境演练一遍再上生产。
6.3 监控与告警建议
独立 ZK 集群上线后,至少要监控这几个指标:
- 节点状态:是否有一个 leader 和多个 follower;
- 连接数:
maxClientCnxns是否接近上限; - 延迟:
avg_latency、max_latency是否异常; - 快照与日志大小:是否持续增长;
- 磁盘使用率:dataDir 和 dataLogDir 所在盘是否告急。
这些指标可以通过四字命令mntr采集,也可以接入通用的监控系统。我个人的经验是,ZK 的告警宁可灵敏一点,因为它一旦出问题,依赖它的 Kafka、HBase 等系统会连锁反应,排查成本极高。
最后分享一个我自己的习惯:每次搭完 ZK 集群,我都会在zoo.cfg旁边留一个README,写清楚版本号、搭建日期、节点列表、chroot 路径、以及当初为什么选这个版本。过半年再回来看,这些信息能救命。版本查询这件事,与其每次去翻 jar 包,不如一开始就记录好,这才是最省事的做法。