☰
ZooKeeper安装配置与集群搭建实战:从单机到三节点调优排错
2026/9/30 6:20:34 网站建设 项目流程

1. 先把 ZooKeeper 这套东西想明白再动手

1.1 它到底解决的是什么问题

很多人第一次接触 ZooKeeper,是在别人的架构图里看到一个小小的方框,标注着"协调服务",然后教程第一句就是"下载解压改配置启动"。照着做完,zkServer.sh status显示Mode: standalone,心里却没什么底——这东西到底干嘛的?

我用一句人话概括:ZooKeeper 是一个高可用的小容量配置与状态存储中心,它保证多个进程看到的数据是完全一致的,并且能在节点故障时自动重新选主继续服务。

拆开来看有三个关键点。第一是"小容量",单个 znode 默认限制 1MB,整棵树的数据全放在内存里,所以别拿它当数据库用,塞几十个 G 的日志进去必然崩。第二是"一致性",一次写成功,集群里所有节点后续读到的都是这个值,这是它最核心的价值,分布式锁、选主、配置下发全靠这个特性。第三是"高可用",只要集群里过半节点活着,服务就不中断。

哪些场景会用到它?最典型的是Hadoop HA 的自动故障切换(ZKFC)、HBase 的 RegionServer 状态管理与元数据入口、Kafka 老版本的 Controller 选举与元数据存储、HiveServer2 的动态服务发现,以及各种自研的分布式锁、命名服务、配置中心。所以你在搜 Hadoop 和 ZooKeeper 整合实战、HBase 安装与配置这些内容时,绕不开的第一步就是先把 ZK 本身跑稳。

1.2 部署形态怎么选:单机、伪集群、真集群

这是个必须先想清楚的问题,选错了后面白折腾。我见过太多人在一台 4G 内存的测试机上强行起三个 JVM 做"集群",结果内存吃满、节点互相抢 CPU,选举来回抖动,最后判定"ZooKeeper 不稳定"。

形态节点数适用场景需要配 server.x 吗故障容忍
单机 standalone1本地开发、跑单元测试、客户端联调不需要无,挂了就是挂了
伪集群3学习选举流程、验证集群配置语法需要,端口要错开容忍 1 台进程退出
真集群3 / 5 / 7开发 / 预发 / 生产环境需要3 节点容忍 1,5 节点容忍 2

这里有个必须记住的数学关系:集群可用节点数必须严格大于总节点数的一半,也就是n = 2f + 1,f 是允许挂掉的节点数。所以 4 个节点的集群和 3 个节点一样只能容忍 1 台故障,多出来的那台纯属浪费。这就是为什么你几乎见不到偶数节点的 ZK 集群。

我的建议很直接:本地开发用单机,学习集群用伪集群,测试环境起步就上 3 台真机。千万别用"伪集群"冒充生产环境,因为三台机器的时间同步、网络延迟、磁盘 IO 干扰模式跟单机三进程完全不同,很多真集群才暴露的问题你在伪集群里永远看不到。

1.3 版本与运行环境的前置确认

版本这块踩过的坑值得单独说一下。ZooKeeper 从 3.5.5 开始,官网下载页上的apache-zookeeper-x.y.z.tar.gz是源码包,只有apache-zookeeper-x.y.z-bin.tar.gz才是能直接跑的二进制包。如果你下错了包,解压后会发现在bin/目录里找不到zkServer.sh,或者勉强找到脚本却报Could not find or load main class org.apache.zookeeper.server.quorum.QuorumPeerMain。这不是配置问题,是包下错了。

版本选择上,我给几个参考区间:

  • 3.4.x:老项目的稳定选择,配置项少,日志走 log4j,很多老教程基于这个版本。但已经不推荐用于新项目。
  • 3.5.x ~ 3.6.x:引入了动态配置、AdminServer、logback 日志体系,是目前存量最多的一代。
  • 3.8.x / 3.9.x:当前主推版本,Java 8 及以上都能跑(3.9 官方要求 Java 8+,实测 Java 11 / 17 也稳定),TLS、指标导出、四字命令白名单都更完善。

JDK 方面,不要用 JDK 6/7 了,直接上JDK 8 的较新小版本或 JDK 11。如果你机器上还跑着 Hadoop 2.x、Hive 1.x 这类老组件,统一用 JDK 8 最省心,避免出现组件之间类库版本打架。

还有两个容易忽略的前置条件:文件句柄数和主机名解析。ZK 每个客户端连接、每次快照文件读写都占文件描述符,官方建议把ulimit -n调到 65535 以上;主机名必须能从每台机器上互相解析到,用/etc/hosts或内网 DNS 都行,否则集群配置里写的server.1=zk1:2888:3888会在启动时卡在"无法连接"的状态上。

2. 安装前的环境准备与依赖梳理

2.1 JDK 的安装与验证,别跳过这一步

JDK 安装本身没什么技术含量,但它是最容易埋雷的环节。我见过JAVA_HOME指向了一个 JRE 目录(而不是 JDK),ZK 启动脚本里的java也能跑起来,但某些依赖反射和工具类的组件就会出问题。还有人把JAVA_HOME写在~/.bashrc里,然后用sudo或 systemd 启动服务,环境变量根本没加载,脚本直接报JAVA_HOME is not set。

标准操作是这样:

# 1. 解压 JDK(示例路径,按你实际情况调整) tar -zxvf jdk-8u381-linux-x64.tar.gz -C /usr/local/ mv /usr/local/jdk1.8.0_381 /usr/local/jdk8 # 2. 配置全局环境变量,写在 /etc/profile.d/ 下更稳妥 cat > /etc/profile.d/java.sh <<'EOF' export JAVA_HOME=/usr/local/jdk8 export PATH=$JAVA_HOME/bin:$PATH export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar EOF # 3. 立即生效并验证 source /etc/profile.d/java.sh java -version echo $JAVA_HOME

注意:/etc/profile只对登录 shell 生效。如果你打算用 systemd 托管 ZK 服务,systemd 不会读取/etc/profile,必须在 service 文件里用Environment=JAVA_HOME=/usr/local/jdk8显式声明,这是我自己被坑过两次才记住的细节。

验证的时候别只看java -version,要确认三件事:java -version输出的版本和你预期一致、which java指向的是/usr/local/jdk8/bin/java而不是系统自带的/usr/bin/java、echo $JAVA_HOME有值。三者都对,才算过关。

2.2 主机规划、免密登录与时间同步

假设我们规划三台机器,事先约定好主机名和 IP,后面所有配置都基于这个表来写,避免写错一个字母排查半小时。

主机名IPmyid角色
zk1192.168.10.111任意(选主后可能成为 Leader)
zk2192.168.10.122任意
zk3192.168.10.133任意

每台机器上执行hostnamectl set-hostname zk1,然后统一在三台机器的/etc/hosts里加上三行映射。这一步比配 SSH 免密还重要,因为 ZK 集群内部的server.x列表用的是主机名或 IP,如果解析失败,启动日志里会反复刷Cannot open channel to 2 at election address。

时间同步是另一个隐形杀手。ZK 的会话超时、心跳检测都基于时间,如果两台机器时间差超过几十秒,跟随者会被 Leader 判定为失联,触发反复选举,表现为集群状态在LOOKING和LEADING之间来回跳。用 chrony 配一下:

yum install -y chrony # CentOS/RHEL 系 systemctl enable --now chronyd chronyc sources -v # 确认已同步到上游时间源 date # 三台机器分别执行,误差应在秒级

SSH 免密不是 ZK 运行的必需条件,但它能让你在分发配置和批量启动时省掉大量重复劳动。生成密钥后ssh-copy-id到另外两台,之后一条for循环就能搞定所有节点的启停。

2.3 端口开放与防火墙策略

ZK 用到三个端口,各自职责完全不同,务必记牢:

  • 2181:客户端连接端口,所有业务程序连这个。
  • 2888:集群内部数据同步端口,Follower 连 Leader 用的。
  • 3888:选举通信端口,选主阶段所有节点互相通信用的。

生产环境只对业务网段开放 2181,2888 和 3888 只对集群内部三台机器开放,这是基本的安全边界。如果你用的是 firewalld:

# 只在集群内部互相开放 firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.0/24" port port="2888" protocol="tcp" accept' firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.10.0/24" port port="3888" protocol="tcp" accept' firewall-cmd --permanent --add-port=2181/tcp firewall-cmd --reload

另外提醒一个 3.5 之后版本特有的坑:新版本会自动启动一个AdminServer,默认占用8080 端口。如果你的机器上已经跑了 Tomcat、Nacos 或者别的什么占着 8080,ZK 启动时会在日志里报端口绑定失败。解决办法是在zoo.cfg里加一行admin.enableServer=false(如果不需要这个 HTTP 管理接口),或者改成别的端口admin.serverPort=8081。这个小东西坑了不少人,因为它的报错信息不算特别醒目。

3. 单机模式安装实操:从解压到跑起来

3.1 安装包获取与目录规划

先把安装包拿到本地。官网地址可能会变,用wget下载的时候认准带-bin的那个文件名。假设我们在/opt下操作:

cd /opt wget https://dlcdn.apache.org/zookeeper/zookeeper-3.8.4/apache-zookeeper-3.8.4-bin.tar.gz tar -zxvf apache-zookeeper-3.8.4-bin.tar.gz mv apache-zookeeper-3.8.4-bin /opt/zookeeper

目录规划这块我要多说两句,因为这是很多人后期扩容和排查问题的痛点。数据目录和事务日志目录最好分开,并且都不要放在系统盘上。

原因在于 ZK 的写路径是这样的:事务日志(transaction log)是顺序追加写入的,每来一个写请求都要fsync到磁盘才会应答客户端;而快照(snapshot)是定期全量落盘。顺序写加上频繁 fsync,对磁盘的写延迟极其敏感。如果事务日志和系统盘上的其他 IO 抢资源,写延迟就会上升,进而拉长事务提交时间,严重的时候会触发会话超时,整集群跟着抖。

我给一个常用的目录结构参考:

mkdir -p /data/zookeeper/data # 存放 myid 和快照 mkdir -p /data/zookeeper/logs # 存放事务日志 chown -R zookeeper:zookeeper /data/zookeeper # 如果建了专用账号

实操心得:/data最好是独立的 SSD 或高性能云盘。如果预算有限只能一块盘,那至少保证数据目录不在根分区,避免磁盘写满导致整个系统挂掉——ZK 的快照目录不清理的话,是真的能把盘写满的,后面我会讲自动清理配置。

3.2 zoo.cfg 逐行拆解与参数含义

配置文件在conf/目录下。官方给了个zoo_sample.cfg模板,直接复制改就行:

cd /opt/zookeeper/conf cp zoo_sample.cfg zoo.cfg

单机模式的最小可用配置长这样:

# 基本时间单元,毫秒。ZK 里所有时间相关的参数都是它的倍数 tickTime=2000 # 数据目录,必须存在且可写 dataDir=/data/zookeeper/data # 客户端连接端口 clientPort=2181 # 单个客户端 IP 允许的最大并发连接数,默认 60 maxClientCnxns=200 # 关闭 AdminServer,避免 8080 端口冲突 admin.enableServer=false # 四字命令白名单,生产环境按需开启,不要图省事写 * 4lw.commands.whitelist=mntr,ruok,conf,stat

我把几个关键参数的真实含义讲清楚,这样你改配置的时候心里有数:

tickTime是整个 ZK 的时间度量基准,单位毫秒,默认 2000。它本身不控制任何具体行为,而是作为其他参数的"乘数因子"。理解这一点很重要,后面讲集群参数时你会看到initLimit=10实际代表 20 秒。

dataDir存两样东西:一是myid文件(集群模式才需要),二是快照文件,放在version-2/子目录里,文件名类似snapshot.0后面跟一串十六进制的事务 ID。注意:如果没配dataLogDir,事务日志也会写到这里,这就是我前面说要把两者分开的原因。

maxClientCnxns限制的是单个客户端 IP 的连接数,不是总连接数。生产环境默认 60 往往不够,因为一个应用服务器上可能起了几十个线程池连同一个 ZK,加上连接池复用不当,很容易撞上限。撞上之后的表现是客户端报ConnectionLoss或者干脆连不上,但服务端日志看不出明显异常,排查起来很费劲。

还有一个容易被忽略的配置是clientPortAddress。默认情况下 ZK 监听所有网卡的 2181 端口,如果机器有多张网卡(比如一张业务网卡一张管理网卡),建议用clientPortAddress明确指定只监听业务网卡,减少暴露面。

3.3 启动、验证与关闭的三板斧

配置写完就能起了:

cd /opt/zookeeper ./bin/zkServer.sh start # 启动(后台运行) ./bin/zkServer.sh status # 查看状态 ./bin/zkServer.sh stop # 停止 ./bin/zkServer.sh restart # 重启

启动后status应该输出Mode: standalone。如果输出的是Client port found: 2181. Client address: localhost.之类的信息但没有 Mode,说明进程可能没起来,去看日志。

日志位置取决于版本。3.5 之前走 log4j,日志默认在$ZOO_LOG_DIR指向的目录,通常是启动脚本所在的目录;3.5 之后走 logback,配置文件是conf/logback.xml,日志目录由ZOO_LOG_DIR环境变量控制。如果找不到日志,直接看nohup.out或者用ps -ef | grep zookeeper看进程在不在。

# 确认进程和端口 ps -ef | grep QuorumPeerMain | grep -v grep ss -lntp | grep 2181 # 用四字命令验证服务活着 echo ruok | nc 127.0.0.1 2181 # 期望返回 imok

ruok返回imok是服务健康的标志。注意 3.4.10 之后四字命令默认是白名单制的,只有srvr在白名单里,其他命令不配置就返回ruok is not executed because it is not in the whitelist.。这不是故障,是安全策略,按需在zoo.cfg里加上4lw.commands.whitelist=mntr,ruok,conf,stat就行。

3.4 用 systemd 接管,别再用脚本硬启

zkServer.sh start是能跑,但它有几个问题:机器重启后不会自动拉起、日志分散、进程挂了没人管、多节点批量操作只能靠 SSH 循环。生产环境建议用 systemd 托管。

# /etc/systemd/system/zookeeper.service [Unit] Description=Apache ZooKeeper After=network.target [Service] Type=forking User=zookeeper Group=zookeeper Environment=JAVA_HOME=/usr/local/jdk8 Environment=ZOO_LOG_DIR=/data/zookeeper/logs ExecStart=/opt/zookeeper/bin/zkServer.sh start ExecStop=/opt/zookeeper/bin/zkServer.sh stop ExecReload=/opt/zookeeper/bin/zkServer.sh restart Restart=on-failure RestartSec=10 LimitNOFILE=65535 [Install] WantedBy=multi-user.target
systemctl daemon-reload systemctl enable --now zookeeper systemctl status zookeeper

注意:Type=forking是必须的,因为zkServer.sh start会派生后台进程然后退出。这个字段写错成simple的话,systemd 会认为进程已经结束,然后反复重启,日志里能看到明显的循环。另外LimitNOFILE=65535这行务必加上,systemd 默认的句柄数限制是 1024,对 ZK 来说远远不够。

4. 集群模式搭建:伪集群与三节点真集群

4.1 伪集群怎么搭:一台机器演三个角色

伪集群的价值在于验证配置语法和观察选举过程,学习阶段非常有意义,但别拿它当生产环境的替身。核心思路是:三个实例,各自独立的dataDir、独立的clientPort、独立的日志目录,但共用同一份安装包(通过三个配置文件区分)。

假设做在/opt/zookeeper下:

mkdir -p /data/zk-cluster/{zk1,zk2,zk3}/data mkdir -p /data/zk-cluster/{zk1,zk2,zk3}/logs

三个配置文件片段,分别放到conf/zoo1.cfg、conf/zoo2.cfg、conf/zoo3.cfg:

# zoo1.cfg tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zk-cluster/zk1/data dataLogDir=/data/zk-cluster/zk1/logs clientPort=2181 admin.enableServer=false server.1=127.0.0.1:2888:3888 server.2=127.0.0.1:2889:3889 server.3=127.0.0.1:2890:3890

zoo2.cfg只需要把clientPort换成 2182、dataDir/dataLogDir换成 zk2 的路径,而server.x列表必须三个文件里完全一致。这是伪集群最容易写错的地方——很多人只改自己那一行,结果三个实例互相找不到对方,一直卡在LOOKING状态。

然后给每个实例写 myid:

echo 1 > /data/zk-cluster/zk1/data/myid echo 2 > /data/zk-cluster/zk2/data/myid echo 3 > /data/zk-cluster/zk3/data/myid

启动时通过环境变量指定配置文件名:

ZOO_LOG_DIR=/data/zk-cluster/zk1/logs \ ./bin/zkServer.sh --config conf start zoo1.cfg

依次启动三个实例,然后分别status,你应该能看到一个Mode: leader、两个Mode: follower。如果三个都是Mode: standalone,说明server.x配置没被识别,检查配置文件路径和文件名是否对应上了。

4.2 真集群的完整操作流程

真集群的配置反而比伪集群简单,因为三个文件内容完全一样(除了 myid)。以第一节规划的三台机器为例,每台机器上都执行:

# /opt/zookeeper/conf/zoo.cfg(三台机器内容完全相同) tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper/data dataLogDir=/data/zookeeper/logs clientPort=2181 maxClientCnxns=500 autopurge.snapRetainCount=5 autopurge.purgeInterval=24 admin.enableServer=false 4lw.commands.whitelist=mntr,ruok,conf,stat server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888

注意server.x后面的两个端口,格式是主机:数据同步端口:选举端口。两个端口号在三台机器上可以相同(因为 IP 不同),但在同一台机器上跑多个实例时绝对不能冲突,这就是伪集群要错开端口的原因。

myid 是每台机器唯一不同的地方:

# zk1 上执行 echo 1 > /data/zookeeper/data/myid # zk2 上执行 echo 2 > /data/zookeeper/data/myid # zk3 上执行 echo 3 > /data/zookeeper/data/myid

注意:myid 文件里只写一个数字,不要有多余的空格或换行符形式的内容(数字后面跟一个换行符是允许多数版本接受的,但为了保险用echo -n更稳)。我遇到过有人用echo "1 " > myid写了个带空格的,结果启动时报java.lang.NumberFormatException: For input string: "1 ",报错信息在一大堆日志里特别不显眼,找了很久。

启动顺序没有强制要求,依次启动即可。等所有节点起来,每个都执行zkServer.sh status,正常情况下应该有一个 Leader 两个 Follower:

# zk1 Mode: follower # zk2 Mode: leader # zk3 Mode: follower

验证集群是否真的在工作,最直接的方法是往一个节点写数据,从另一个节点读:

# 在 zk1 上连接 ./bin/zkCli.sh -server zk1:2181 [zk: zk1:2181(CONNECTED) 0] create /cluster-test "hello-zk" # 在 zk3 上连接 ./bin/zkCli.sh -server zk3:2181 [zk: zk3:2181(CONNECTED) 0] get /cluster-test # 能读到 hello-zk 就说明数据同步正常

4.3 myid 与 server.x 的对应关系,以及集群参数推导

myid和server.x的对应关系是集群配置里唯一需要"配对"的地方,配错的后果很严重:轻则节点起不来,重则两个节点认为自己是同一个 ID,导致选举混乱。

规则很简单:某台机器的 myid 文件内容,必须等于zoo.cfg里代表这台机器的那一行的编号 x。比如 zk1 的 myid 是 1,那server.1=zk1:2888:3888这一行的主机名就必须是 zk1。如果你把 zk1 的 myid 写成 2,那 zk1 会以 ID=2 的身份加入集群,而配置里 ID=2 对应的是 zk2 的地址,其他节点会尝试往 zk2 发数据,整个集群的拓扑就乱了。

再来说集群参数怎么推导,这两个参数很多人都是照抄别人的,从来不知道为什么是 10 和 5。

initLimit=10的含义是:Follower 在启动阶段与 Leader 完成连接和数据同步的最长时间限制,单位是tickTime的倍数。默认tickTime=2000(2 秒),所以initLimit=10代表 20 秒。如果你的数据量很大(比如几十万个 znode 的快照),Follower 同步需要更久,20 秒可能不够,这时就要调大initLimit,比如 30 甚至 50。

syncLimit=5的含义是:运行期间 Follower 与 Leader 之间心跳超时的限制,同样是 tickTime 倍数,默认 10 秒。如果网络抖动严重或者机器负载很高,心跳可能延迟,syncLimit太小会导致 Follower 被踢出集群,触发重新选举。网络条件一般的环境可以调到 10。

还有一个跟客户端直接相关的推导:会话超时时间(session timeout)的取值范围是 2 倍到 20 倍的 tickTime。默认 tickTime 是 2000 毫秒,那客户端能协商的超时就是 4 秒到 40 秒之间。这个细节很关键——如果你在客户端代码里设置了 60 秒的超时,ZK 服务端会拒绝并强制降到 40 秒,你在日志里会看到Session establishment complete on server之后超时值和预期不一致。这就是为什么调整tickTime会连带影响客户端行为,改之前一定要评估现有应用设置。

5. 关键参数调优:JVM 与运行时

5.1 JVM 堆内存到底给多少才合适

这是个高频问题,网上的回答从 "512M 就够" 到 "给 16G" 都有,差距大得离谱。我用一个粗算模型来推导,你照着套自己的数据量。

ZK 把所有 znode 的元数据都放在内存里,包括 znode 路径(字符串)、数据内容(字节数组)、ACL、状态信息,以及几个 watch 相关的哈希表。经验上,单个 znode 的固定内存开销大致在 150 到 250 字节之间,再加上路径字符串本身和数据内容。粗略估算:

总内存需求 ≈ znode 数量 × (路径长度 + 数据大小 + 200 字节) × 2

公式里乘 2 是给 DataTree 内部多份结构和 GC 碎片留的余量。假设你有 30 万个 znode,平均路径长度 40 字节,每个 znode 数据 100 字节,那单份大约是 30万 × 340 字节 ≈ 100MB,乘 2 就是 200MB 左右。这种情况下堆给 1G 绰绰有余。

但现实里 ZK 内存爆炸往往不是因为 znode 多,而是因为客户端滥用:比如把配置信息整个塞进一个 znode(动辄几百 KB)、创建海量临时节点做服务注册(每秒几千个)、或者 watch 数量失控。我见过一个案例,某个服务用 ZK 做分布式 ID 生成器,每次请求创建一个 znode,一晚上积累了两百万个节点,堆直接 OOM。

所以实际给堆大小的时候我按这个顺序判断:

znode 规模建议堆大小说明
10 万以内1G ~ 2G绝大多数中小规模场景,绰绰有余
10 万 ~ 50 万2G ~ 4G需要通过 mntr 监控 znode 数量增长趋势
50 万以上4G ~ 8G,并考虑架构优化堆越大 GC 停顿越长,反而影响会话稳定性

堆不是越大越好,这是我想强调的重点。ZK 对延迟极其敏感,一次 Full GC 停顿 5 秒,就可能让一批客户端的会话超时、触发重连甚至重新选举。所以宁可把 znode 数量控制住,也不要靠堆来硬扛。

配置 JVM 参数的地方在conf/java.env,这个文件默认不存在,需要自己创建:

cat > /opt/zookeeper/conf/java.env <<'EOF' export JVMFLAGS="-Xms4g -Xmx4g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m \ -XX:+UseG1GC -XX:MaxGCPauseMillis=200 \ -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/zookeeper/logs/ \ -Dzookeeper.skipACL=no" EOF

几个选择理由:-Xms和-Xmx设成一样,避免运行期堆伸缩带来的额外开销和停顿;用 G1 而不是 CMS(CMS 在新版 JDK 已经被移除),G1 的MaxGCPauseMillis=200能帮助我们约束单次停顿;加上 HeapDumpOnOutOfMemoryError,万一真 OOM 了至少有现场可查,不然只能靠猜。

5.2 日志与快照的清理,不做迟早出事

我在前面反复提"磁盘会被写满",现在说清楚为什么。ZK 每次写操作会往事务日志追加记录,每隔snapCount(默认 100000)次事务会做一次全量快照,快照和日志都是新文件,旧文件不删。一个中等写入压力的集群,事务日志一天能长几百 MB 到几个 G,快照文件单个可能上百 MB。跑上几个月,磁盘就满了。

官方提供了自动清理参数:

# 保留最近 5 份快照(以及与之相关的日志) autopurge.snapRetainCount=5 # 每 24 小时清理一次,单位是小时,设为 0 表示不自动清理 autopurge.purgeInterval=24

这两个参数必须一起配,只配purgeInterval不配snapRetainCount的话,默认保留 3 份,可能不够;只配snapRetainCount不配purgeInterval的话,清理任务根本不会启动,这是新手最容易犯的错。

如果因为某些原因(比如老版本)不能用自动清理,那就写个定时任务手工清理,思路是保留最近 N 份快照文件和它们对应的事务日志,删除更早的。但我要提醒一句:删除事务日志之前,必须确认这批日志对应的快照已经存在。日志是用来在快照基础上回放的,如果删掉了比最新快照还新的日志,数据就丢了。所以手工清理比较危险,能用官方参数就别自己写脚本。

5.3 数据目录的权限与文件系统选择

数据目录的所有者必须是运行 ZK 的用户。如果你用 root 启动过一次,version-2目录就归 root 了,之后换 zookeeper 用户启动会报Permission denied,而且这个报错有时候只出现在日志文件里,启动脚本返回的信息看起来很"正常"。养成习惯:先chown -R再启动。

chown -R zookeeper:zookeeper /data/zookeeper chmod 755 /data/zookeeper

文件系统方面,XFS 和 ext4 都能跑,但要注意挂载参数。如果用了data=writeback之类的延迟写模式,ZK 的fsync语义会打折扣,虽然性能数据好看,但掉电时的一致性就没了保证。普通业务用默认挂载参数就行,别为了跑分去调这些。

还有一点是关于dataLogDir的磁盘预分配。ZK 会为事务日志预分配空间,默认preAllocSize=65536(单位 KB,也就是 64MB)。这个值的含义是每次预分配 64MB 的文件空间,如果写入量很小,会浪费一些空间;如果写入量大,增大它(比如 128MB)可以减少文件扩容次数。只有在你的日志盘空间特别紧张时才需要关注这个参数,一般保持默认。

6. 客户端连接与节点基础操作验证

6.1 用 zkCli 走一遍节点基本操作

集群搭完,用自带的命令行客户端验证是最直接的方式。zkCli.sh的用法:

./bin/zkCli.sh -server zk1:2181,zk2:2181,zk3:2181

连接串里写上多个地址是个好习惯,客户端会自动尝试列表里的节点,某个挂了能切换到下一个。虽然内网环境一般不会频繁切换,但生产环境的客户端代码都应该这么写。

进去之后是交互式命令行,我把最常用的几个操作列一下,这也是"节点基本操作"这一系列内容的核心:

# 创建持久节点 create /app "app-root" # 创建带数据的子节点 create /app/config "timeout=3000" # 列出子节点 ls /app # 读取节点的数据和状态信息 get /app/config # 输出里 cZxid / ctime / mZxid / pZxid / cversion / dataVersion # 这些字段是排查并发写问题的关键,后面细说 # 修改数据 set /app/config "timeout=5000" # 创建临时节点(会话结束自动删除) create -e /app/lock-001 "holder-1" # 创建顺序节点(自动在名字后加 10 位序号) create -s /app/task/task- "job" # 删除节点(只能删没有子节点的) delete /app/config # 递归删除(不同版本命令名不同,先敲 help 确认)

get命令返回的那些字段值得单独解释,很多人看不懂就直接忽略了。dataVersion每改一次数据加 1,这是实现乐观锁的基础——客户端可以用set的版本参数做 CAS 操作。cversion是子节点列表的版本号,每增删一个子节点加 1。ephemeralOwner如果不为 0,说明这是个临时节点,值是持有该会话的 ID。

实操心得:删除带子节点的路径时,delete会直接报错。老版本(3.4)有rmr命令可以递归删,3.5 之后这个命令被移除,改成了别的形式,而且不同小版本之间命令有增减。最稳的做法是进命令行先敲help看一眼当前版本支持什么,别照着两年前的教程硬敲,报 "Command not found" 还以为是环境问题。

6.2 四字命令监控,比看日志快得多

排查 ZK 问题的时候,日志往往是滞后的、海量的,而四字命令是即时的、结构化的。前面已经在zoo.cfg里放开了白名单,现在可以用了:

echo mntr | nc 127.0.0.1 2181

mntr返回的是一行一个key value的指标,重点关注这几个:

指标含义异常判断
zk_avg_latency平均请求延迟(毫秒)持续超过 10ms 就要关注磁盘 IO
zk_max_latency最大请求延迟偶发高值可能是 GC 停顿
zk_outstanding_requests排队请求数长期大于 0 说明处理不过来
zk_znode_countznode 总数增长趋势比绝对值更重要
zk_watch_countwatch 数量异常增长可能是客户端泄漏
zk_num_alive_connections活跃连接数突然暴涨要查客户端连接池
zk_open_file_descriptor_count打开的文件句柄数接近上限时服务会开始拒绝连接
zk_followersFollower 数量少于预期说明有节点掉线
zk_server_state节点角色leader / follower / standalone

另外两个常用的:echo stat | nc host 2181输出概览信息,包含连接列表和节点角色;echo cons | nc host 2181输出每个连接的详细信息,包括客户端地址、会话 ID、排队请求数,排查"到底是哪个客户端在压我"的时候特别有用。

6.3 和 Hadoop / HBase / Hive 整合时的对接要点

这是很多人搭完 ZK 之后的下一步,也是报错最多的地方。核心原则是:ZK 只提供存储和一致性,其他组件通过"命名空间(znode 路径)"来隔离自己的数据。不同组件默认用的路径不一样,路径对不上就是各种"读不到配置"。

几个常见组件的默认路径和各自主的配置项:

组件关键配置项默认根路径说明
Hadoop HAha.zookeeper.quorum/hadoop-haZKFC 用来做自动主备切换
HBasehbase.zookeeper.quorum/zookeeper.znode.parent/hbaseHBase 的元数据入口
HiveServer2hive.zookeeper.quorum/hive.zookeeper.namespace/hiveserver2动态服务发现
Kafka(老版本)zookeeper.connect无默认根路径,需显式指定现在新版本已逐步弃用 ZK

拿热词里提到的那个典型报错来说:unable to read hiveserver2 configs from zookeeper。这个报错的字面意思是"从 ZK 读不到 HiveServer2 的配置",根因通常有三种:客户端的hive.zookeeper.quorum指向的 ZK 地址不对;hive.zookeeper.namespace配置的路径跟 ZK 上实际注册的路径不一致;或者 HiveServer2 实例根本没启动完,还没往 ZK 上写自己的连接信息。排查顺序就是先确认 ZK 地址通不通,再用zkCli连上去ls /hiveserver2看路径存不存在、里面有没有子节点,两者信息一对照,问题基本就定位了。

注意:如果你的 HBase 和 Hadoop 共用同一个 ZK 集群,务必确认两边的根路径不同(比如/hbase和/hadoop-ha)。同时强烈建议给 ZK 加权限控制,开启 ACL 并配置zookeeper.DigestAuthenticationProvider.superDigest,否则任何能访问 2181 端口的人都能读写这些关键节点,把整个集群的元数据删掉也不是不可能。生产环境把这个当安全底线,别嫌麻烦。

7. 踩坑实录与常见问题速查

7.1 启动后进程直接没了,日志也找不到

这是最让人抓狂的情况:敲完start,命令返回了,ps一看进程不存在,日志目录空空如也。这类问题的排查路径固定,按顺序走:

第一步,看标准输出有没有被重定向。新版zkServer.sh会把启动日志写到$ZOO_LOG_DIR/zookeeper.out或者logs/zookeeper.out。找不到就全局搜:find / -name "zookeeper.out" -mmin -10。

第二步,手动前台启动看报错。这是最快的方法:

cd /opt/zookeeper bin/zkServer.sh start-foreground

前台启动会把日志直接打在屏幕上,报错信息一目了然。常见的几种报错和对应原因:

报错关键词根因解决方向
java.net.BindException: Address already in use2181/2888/3888/8080 被占用ss -lntp找占用进程,或关掉 AdminServer
No such file or directory: .../myiddataDir 下缺 myid 文件检查路径和文件名拼写
NumberFormatExceptionmyid 内容不是纯数字去掉多余空格,用echo -n重写
JAVA_HOME is not set环境变量没加载在 systemd 或启动前显式 export
ClassNotFoundException: QuorumPeerMain下错包(源码包)换成-bin.tar.gz
Permission denied写 version-2dataDir 属主不对chown -R修正

第三步,检查磁盘空间。磁盘满了的话,ZK 启动时会因为写不了快照文件而失败,报错信息有时候很含蓄。df -h看一眼,尤其检查dataDir和dataLogDir所在分区。

7.2 集群起不来,一直卡在 LOOKING 状态

集群搭好后zkServer.sh status一直显示Mode: Looking,或者报Error contacting service. It is probably not running.。这是集群模式下最典型的问题,按下面的清单挨个对:

第一,三个节点的zoo.cfg里的server.x列表是否完全一致。这是最高频的失误。三台机器的配置文件除了 myid 之外,其他内容必须一模一样。用md5sum快速对比:

md5sum /opt/zookeeper/conf/zoo.cfg # 三台机器分别执行,结果应该一致(myid 是单独的文件,不影响 zoo.cfg 的哈希)

第二,myid 和 server.x 的编号是否配对。前面讲过,zk1 的 myid 必须是 1,且server.1那行必须写 zk1 的地址。

第三,2888 和 3888 端口是否互通。从 zk1 上测试:

nc -zv zk2 2888 nc -zv zk2 3888

如果其中一个不通,就是防火墙或安全组的锅。

第四,主机名能否解析。ping zk2看是否通。用 hostname 配置的话,三台机器(或它们的/etc/hosts)都必须能解析到彼此。

第五,时间是否同步。时间差过大会导致选举消息里的逻辑时间戳乱掉。用date对比三台机器。

第六,看选举日志。日志里搜Notification、Looking、Vote这些关键词,能看到完整的选举过程。如果日志里有节点反复在LOOKING和LEADING之间切换,多半是网络不稳或者时间不同步。

7.3 客户端连接报错,会话频繁掉线

客户端侧的问题表现是:ConnectionLoss、SessionExpired、OperationTimeout,或者应用日志里刷KeeperErrorCode = ConnectionLoss。

先区分两类问题。ConnectionLoss说明请求发出了但没收到响应,通常是网络抖动或服务端处理不过来;SessionExpired更严重,说明客户端在会话超时时间内没能和服务端心跳成功,这个会话已经彻底失效,会话上创建的临时节点全部被删除了。

排查方向:

第一,检查会话超时设置是否合理。客户端设置的超时要落在2×tickTime到20×tickTime之间。很多人为了"更稳"把超时设成 60 秒,结果被服务端强制降到 40 秒,客户端代码里如果按 60 秒做重试逻辑就会错位。

第二,检查 GC 停顿。服务端如果发生长 GC,所有客户端都会受影响。用mntr看zk_max_latency,如果它和你的 GC 停顿时间量级接近,那问题就在堆配置上。

第三,检查网络是否有丢包或延迟抖动。ZK 的心跳对网络质量比较敏感,跨机房部署尤其要注意,能用同机房就别跨。

第四,检查客户端连接池实现。有些连接池在检测到会话失效后不会主动重建,就一直拿着死会话重试,表现是永久性的失败。这种情况要在代码层加会话事件监听,收到Expired事件时重建客户端。

7.4 常见问题速查表

我把上面这些零散的经验整合成一张表,出问题的时候可以直接对照:

现象最可能的原因快速验证方法处理动作
单机启动失败,无日志JDK 环境变量或包下载错误bin/zkServer.sh start-foreground看报错修 JAVA_HOME / 换 bin 包
集群一直 Lookingzoo.cfg 不一致或 myid 配错md5sum 对比配置;核对 myid统一配置,纠正 myid
节点反复切主时间不同步 / 网络抖动date对比;ping测丢包配 chrony;排查网络
客户端 ConnectLoss会话超时设置不合理检查客户端超时是否在 2x~20x tickTime调整超时并加重试
磁盘持续增长未开自动清理du -sh看 version-2 目录配 autopurge 参数
8080 端口冲突AdminServer 默认启用`ss -lntpgrep 8080`
四字命令返回白名单提示未配置 4lw 白名单直接执行 `echo mntrnc`
OOMznode 数量过多或客户端滥用看堆 dump;mntr看 znode_count清理无用节点,优化客户端逻辑
句柄耗尽ulimit 限制过低mntr看 open_file_descriptor_count调大 LimitNOFILE 到 65535
写延迟高事务日志盘 IO 瓶颈mntr看 avg_latency日志盘换 SSD,dataLogDir 分离

7.5 版本升级时最容易忽略的两件事

如果你的 ZK 已经跑了几年,想升级到 3.8 或 3.9,有两件事必须先确认,不然升级完会有一堆莫名其妙的报错。

第一是日志框架的切换。3.5 之后从 log4j 换成 logback,配置文件从log4j.properties变成logback.xml。如果你之前改过log4j.properties来调整日志级别和滚动策略,升级后这些改动全部失效,日志会按新版本的默认策略输出。更麻烦的是,如果你的安装目录里还残留着老的 log4j jar 包,可能会出现"类路径下有多个 SLF4J 绑定"的警告,甚至启动时类加载冲突。

第二是 AdminServer 的默认开启。这是 3.5 引入的新特性,默认监听 8080。老版本没有这个东西,升级完如果机器上 8080 被占用,启动就会失败。所以升级前先确认端口占用情况,或者在配置里提前关掉它。

实操心得:升级 ZK 千万别直接覆盖安装目录。我的做法是:新版本解压到一个新目录,把老的zoo.cfg、myid、java.env复制过去,停掉老版本,改软链接指向新目录,然后用同样的路径启动。这样出问题可以秒回滚——把软链接改回去就行。ZK 的数据目录格式在 3.4 到 3.5 之间有过兼容性说明,跨大版本升级前一定要读官方的升级说明,确认数据目录能不能直接复用,必要时先在一个从节点上做灰度。

再补一个小技巧:验证新版本是否正常,除了看status和mntr,还可以观察一段时间内zk_avg_latency和zk_max_latency的变化趋势,跟老版本的数据做对比。我自己的经验是,如果升级后平均延迟没有明显变化、最大延迟没有出现规律性的尖峰,基本可以判定升级平稳;如果最大延迟的尖峰变得频繁,多半是 GC 参数需要跟着新版本的默认值重新调整,这时候回到java.env里重新调一版 JVM 参数就行了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询