Ubuntu 18.04下ZooKeeper高可用集群手动部署实战

1. 为什么ZooKeeper集群不能只装单机——从“伪分布式”到真实高可用的跨越

Apache ZooKeeper不是那种装上就能跑、跑起来就完事的普通服务。我第一次在Ubuntu 18.04上部署它时,图省事直接用 apt install zookeeperd 配了个单节点,结果上线第三天凌晨,一个磁盘I/O抖动导致ZK进程卡死37秒——下游所有依赖它的Kafka消费者集体失联,Flink作业状态检查点全部失败,监控告警像鞭炮一样炸了一整晚。第二天复盘才发现:ZooKeeper官方文档开篇第一句就写着“ ZooKeeper is designed to be replicated over a set of hosts ”,它天生就不是为单点设计的。所谓“单机ZK”,本质上只是开发测试用的玩具,连“伪分布式”(standalone mode with multiple instances on one host)都算不上真正的容错架构。

真正生产环境里,ZooKeeper集群必须满足 奇数节点、法定票数(quorum)机制、独立物理/网络隔离 三大铁律。Ubuntu 18.04作为当时LTS版本,其内核调度、systemd服务管理、防火墙策略与ZK的Java堆内存模型存在几处隐蔽冲突——比如默认的 vm.swappiness=60 会让ZK在内存压力下频繁触发swap,而ZK对GC延迟极度敏感,一次 ParNew GC 耗时超过500ms就可能触发会话超时(session timeout),导致客户端误判节点宕机。这不是ZK的bug,而是Linux系统参数与分布式协调服务之间典型的“水土不服”。

你可能会问:现在都2024年了,为什么还要折腾Ubuntu 18.04?因为大量遗留金融、电信核心系统仍运行在此版本上,它们无法轻易升级内核或迁移容器平台。我去年帮某省级农信社做灾备改造时,就遇到三台物理服务器锁死在Ubuntu 18.04 + OpenJDK 8u292组合上,连 apt update 都要走离线镜像源。这种场景下,硬套Kubernetes Operator或Ansible一键部署方案反而会引入更多不可控变量。最稳妥的路径,是回归本质:用最原始的手动配置,把每个字节都刻进 zoo.cfg myid 文件里,让ZK进程完全暴露在你的掌控之下。

提示:本文所有操作均基于OpenJDK 8(非OpenJDK 11+),因ZooKeeper 3.4.x系列(Ubuntu 18.04官方源默认版本)与JDK 11存在JMX连接兼容性问题,强行升级会导致 zkServer.sh status 命令永远返回“Error contacting service”——这个坑我在三套环境里反复踩过,最终确认是 com.sun.jmx.remote.util.ClassLoaderRepository 类加载器变更引发的。

2. 环境准备的七个致命细节——Ubuntu 18.04特有的系统级约束

很多教程一上来就让你 sudo apt install zookeeperd ,这恰恰是最大误区。Ubuntu 18.04官方仓库中的 zookeeperd 包版本为3.4.10-4,它被硬编码为绑定 /etc/zookeeper/conf 路径且强制使用 init.d 启动脚本,而现代ZK集群要求每个节点有独立的 dataDir clientPort server.x 配置。更麻烦的是,该包的 postinst 脚本会自动创建 /var/lib/zookeeper/version-2 目录并赋予 zookeeper:zookeeper 权限,但后续手动修改配置时若未同步调整目录所有权,ZK进程会因权限拒绝写入快照文件而静默退出——日志里只有一行 WARN Cannot open channel to X at election address Y ,根本不会报错。

所以第一步必须彻底卸载官方包,并建立纯净的手动安装路径:

sudo apt remove --purge zookeeperd zookeeper-bin
sudo apt autoremove
sudo rm -rf /etc/zookeeper /var/lib/zookeeper /var/log/zookeeper

接下来是Ubuntu 18.04独有的系统级约束,这些细节在CentOS或Docker环境中不存在,但在这里会直接导致集群脑裂:

2.1 时间同步必须用chrony而非ntpd

Ubuntu 18.04默认启用 systemd-timesyncd ,但它精度仅±100ms,而ZooKeeper法定票数选举要求节点间时钟偏差≤10ms。我实测过:当三台机器时间差达15ms时, zkCli.sh -server ip1:2181,ip2:2181,ip3:2181 ls / 命令会随机返回 Connection refused Session expired 。解决方案是强制切换至chrony:

sudo apt install chrony
sudo systemctl stop systemd-timesyncd
sudo systemctl disable systemd-timesyncd
sudo systemctl enable chrony
sudo systemctl start chrony
# 验证:chronyc tracking | grep "System time"

2.2 内核参数调优不可跳过

ZooKeeper大量使用epoll和内存映射文件,Ubuntu 18.04默认的 net.core.somaxconn=128 会导致连接队列溢出。当客户端并发连接数>100时, zkServer.sh status 会显示 Client port not bound 。需永久修改 /etc/sysctl.conf

# ZooKeeper专用内核参数
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
vm.swappiness = 1
fs.file-max = 2097152

执行 sudo sysctl -p 后,还需验证 ulimit -n 是否生效——Ubuntu 18.04的 /etc/security/limits.conf 对systemd服务无效,必须在 /etc/systemd/system.conf 中添加:

DefaultLimitNOFILE=1048576
DefaultLimitNPROC=1048576

然后重启systemd: sudo systemctl daemon-reload

2.3 Java环境必须锁定JDK 8u292

OpenJDK 8u292是最后一个完整支持ZooKeeper 3.4.x JMX远程监控的版本。更高版本因JEP 248(G1为默认GC)引入的GC日志格式变更,会导致ZK自带的 zkEnv.sh 脚本解析失败。安装命令必须精确:

sudo apt install openjdk-8-jdk-headless
java -version # 必须输出 openjdk version "1.8.0_292"
# 设置JAVA_HOME(关键!)
echo 'export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64' | sudo tee -a /etc/profile
source /etc/profile

注意: java-8-openjdk-amd64 路径在ARM64架构(如树莓派)上为 java-8-openjdk-arm64 ,需根据 dpkg --print-architecture 结果动态判断。这是“arm ubuntu 18.04”相关热搜词背后的真实痛点——很多人在树莓派集群上部署ZK失败,根源就是JAVA_HOME路径写错。

3. 集群配置的核心逻辑——quorum机制如何决定你的生死线

ZooKeeper集群不是简单地把多个实例连起来,而是通过 法定票数(quorum)算法 实现强一致性。这个算法决定了:当节点故障时,剩余节点能否继续提供服务?数据会不会丢失?客户端会不会收到脏读?理解 tickTime initLimit syncLimit 这三个参数的本质,比记住配置步骤重要十倍。

3.1 tickTime:心跳节拍器的物理意义

tickTime 不是“心跳间隔”,而是ZK内部所有超时计算的 最小时间单位 。它被用作:

  • initLimit = tickTime × N(初始连接超时)
  • syncLimit = tickTime × M(同步超时)
  • minSessionTimeout = tickTime × 2(会话最小超时)
  • maxSessionTimeout = tickTime × 20(会话最大超时)

在Ubuntu 18.04上,由于内核调度延迟较高, tickTime 设为2000ms(2秒)是安全下限。若设为500ms,当系统负载突增时,leader节点可能因GC暂停无法在1个tick内响应follower心跳,导致follower主动发起新选举——这就是脑裂的起点。我见过最惨烈的案例:某电商大促期间, tickTime=500 的集群在流量峰值时每3分钟自动分裂一次,所有临时节点(ephemeral node)批量消失。

3.2 initLimit与syncLimit的黄金比例

initLimit 控制follower首次连接leader完成全量同步的最大时间, syncLimit 控制增量同步的窗口。二者关系必须满足: initLimit > syncLimit × 2 。原因在于:follower首次同步需下载整个快照(snapshot)+ 增量事务日志(txn log),而后续同步只需拉取最新txn log。在千兆内网环境下,我实测三台Ubuntu 18.04虚拟机(4C8G)的典型值:

场景 snapshot大小 全量同步耗时 推荐initLimit 推荐syncLimit
新集群空数据 <1MB <1s 10s (tickTime×5) 5s (tickTime×2.5)
生产集群(日均10万写) ~200MB 8-12s 30s (tickTime×15) 10s (tickTime×5)

initLimit 设得太小,follower会因超时退出同步状态,日志出现 FollowerHandler: Unexpected exception causing shutdown while sock still open ;若 syncLimit 太小,则网络抖动时follower频繁断连重连,CPU占用率飙升。

3.3 server.x配置的拓扑陷阱

server.1=192.168.1.101:2888:3888 这行配置中, 2888 是follower与leader通信端口(Follower→Leader), 3888 是选举端口(Peer→Peer)。关键陷阱在于: 所有节点的electionPort(3888)必须能互相直连,且不能被ufw防火墙拦截 。Ubuntu 18.04默认启用ufw,而 sudo ufw allow 2181 只开放客户端端口, 2888/3888 端口仍被封锁。必须显式放行:

sudo ufw allow 2181
sudo ufw allow 2888
sudo ufw allow 3888
sudo ufw reload

更隐蔽的问题是:当集群跨子网部署时(如192.168.1.0/24与10.0.0.0/24), server.1 的IP地址必须填写 对方网络可路由的地址 。我曾在一个混合云环境里,因在 server.1 中填写了内网IP(10.0.0.101),而另一节点在公网VPC中(192.168.1.101),导致选举端口始终无法建立TCP连接, zkServer.sh start 后日志循环打印 Unable to initiate connection to server.1

4. 手动部署全流程——从解压到集群验证的23个关键动作

现在进入实操环节。以下步骤在三台Ubuntu 18.04服务器(host1/host2/host3)上逐台执行, 顺序不可颠倒 。我将用host1为例,host2/host3仅需替换IP和myid值。

4.1 创建标准化部署目录结构

ZooKeeper要求 dataDir dataLogDir 严格分离(避免IO争抢),且路径不能含空格或特殊字符:

# 创建主目录
sudo mkdir -p /opt/zookeeper/{data,logs,conf}
sudo chown -R $USER:$USER /opt/zookeeper
# 下载ZooKeeper 3.4.14(Ubuntu 18.04最稳定版本)
cd /tmp
wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz
tar -xzf zookeeper-3.4.14.tar.gz
sudo cp -r zookeeper-3.4.14/* /opt/zookeeper/
# 清理冗余文件
sudo rm -rf /opt/zookeeper/{docs,contrib,recipes}

4.2 编写zoo.cfg核心配置文件

/opt/zookeeper/conf/zoo.cfg 内容如下(注意:所有路径必须用绝对路径,相对路径会导致ZK静默失败):

# 基础参数
tickTime=2000
initLimit=15
syncLimit=5
# 数据目录(必须独立于日志目录)
dataDir=/opt/zookeeper/data
dataLogDir=/opt/zookeeper/logs
# 客户端端口(生产环境建议改2182避免冲突)
clientPort=2181
# 四字命令端口(用于运维诊断)
fourLetterWordWhiteList=*
# 集群节点列表(按实际IP修改)
server.1=192.168.1.101:2888:3888
server.2=192.168.1.102:2888:3888
server.3=192.168.1.103:2888:3888
# 高级参数(解决Ubuntu 18.04特有问题)
autopurge.snapRetainCount=3
autopurge.purgeInterval=1
# 关键!禁用JMX远程(避免JDK 8u292兼容问题)
jmxremote.auth=false
jmxremote.ssl=false

注意: fourLetterWordWhiteList=* 在生产环境应限制为具体命令(如 stat,ruok,mntr ),但调试阶段设为 * 可避免因白名单缺失导致 echo stat | nc localhost 2181 无响应。

4.3 配置myid文件并验证唯一性

这是集群识别身份的核心。每台机器必须有唯一数字ID(1-255),且必须与 server.x 中的x一致:

# host1执行
echo "1" | sudo tee /opt/zookeeper/data/myid
# host2执行
echo "2" | sudo tee /opt/zookeeper/data/myid
# host3执行
echo "3" | sudo tee /opt/zookeeper/data/myid
# 验证:所有节点执行
cat /opt/zookeeper/data/myid  # 必须输出对应数字
ls -l /opt/zookeeper/data/    # myid文件权限应为-rw-r--r--

4.4 创建systemd服务单元文件

Ubuntu 18.04的systemd对Java进程管理有特殊要求,必须指定 Type=forking 并设置 PIDFile

sudo tee /etc/systemd/system/zookeeper.service << 'EOF'
[Unit]
Description=Apache ZooKeeper
Documentation=http://zookeeper.apache.org
Requires=network.target
After=network.target

[Service]
Type=forking
Restart=on-failure
RestartSec=10
User=$USER
Group=$USER
Environment=JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64
Environment=ZOO_LOG_DIR=/opt/zookeeper/logs
Environment=ZOO_LOG4J_PROP="INFO,ROLLINGFILE"
ExecStart=/opt/zookeeper/bin/zkServer.sh start /opt/zookeeper/conf/zoo.cfg
ExecStop=/opt/zookeeper/bin/zkServer.sh stop /opt/zookeeper/conf/zoo.cfg
PIDFile=/opt/zookeeper/zookeeper_server.pid
TimeoutSec=120
RestartPreventExitStatus=1

[Install]
WantedBy=multi-user.target
EOF

4.5 启动集群并逐层验证

必须严格按顺序启动 :先启host1,等其进入 leader 状态,再启host2,最后启host3。任意节点启动前,先检查端口占用:

# 检查端口(三台机器均执行)
sudo ss -tlnp | grep -E ':2181|:2888|:3888'
# 若被占用,杀掉进程:sudo lsof -i :2181 | awk 'NR>1 {print $2}' | xargs kill -9

# 启动host1(等待15秒)
sudo systemctl daemon-reload
sudo systemctl enable zookeeper
sudo systemctl start zookeeper
sleep 15
# 验证host1状态
echo "srvr" | nc 127.0.0.1 2181 | grep "Mode"
# 应输出 Mode: leader

# 启动host2(等待10秒)
sudo systemctl start zookeeper
sleep 10
echo "srvr" | nc 127.0.0.1 2181 | grep "Mode"
# 应输出 Mode: follower

# 启动host3(等待10秒)
sudo systemctl start zookeeper
sleep 10
echo "srvr" | nc 127.0.0.1 2181 | grep "Mode"
# 应输出 Mode: follower

4.6 终极验证:模拟故障与自动恢复

真正的集群健壮性体现在故障转移能力。我们手动杀死leader节点,观察是否在30秒内选出新leader:

# 在host1上获取leader进程PID
ps aux | grep QuorumPeerMain | grep -v grep | awk '{print $2}'
# 杀死leader(host1)
sudo kill -9 <PID>
# 等待30秒,检查host2是否升为leader
echo "srvr" | nc 127.0.0.1 2181 | grep "Mode"  # host2应输出leader
# 检查客户端连接是否中断
echo "ls /" | nc 192.168.1.102 2181  # 应正常返回空列表

实测经验:若故障转移耗时>45秒,大概率是 initLimit syncLimit 设置过小,或网络延迟>15ms。此时需用 mtr 192.168.1.102 检测双向丢包率。

5. 运维诊断工具箱——那些官方文档没写的救命命令

ZooKeeper集群一旦上线,日常运维远不止 zkServer.sh status 。Ubuntu 18.04环境下,我整理了一套经过27次线上事故锤炼的诊断工具箱,所有命令均可直接复制粘贴:

5.1 四字命令深度解析

ZooKeeper内置的四字命令是无需客户端库的终极诊断手段。在Ubuntu 18.04上,必须配合 nc telnet 使用( echo 管道有时会失效):

# 查看完整状态(重点关注Zxid、Mode、Latency)
printf "stat" | nc localhost 2181

# 检查节点健康(返回imok表示存活)
printf "ruok" | nc localhost 2181

# 监控指标(重点关注Packets received/sent、Outstanding requests)
printf "mntr" | nc localhost 2181 | grep -E "(zk_avg_latency|zk_num_alive_connections|zk_outstanding_requests)"

# 查看最近10个事务(定位数据异常)
printf "srst" | nc localhost 2181

注意: mntr 命令在ZK 3.4.14中返回字段名含下划线(如 zk_avg_latency ),而3.5.x改为驼峰( zkAvgLatency ),脚本化监控时务必注意版本差异。

5.2 日志分析黄金组合

ZooKeeper日志分散在 /opt/zookeeper/logs/ 目录,关键文件有三个:

文件名 作用 分析命令
zookeeper-$USER-server-host1.out JVM标准输出(含GC日志) grep -i "gc|oom" zookeeper*.out | tail -20
zookeeper-$USER-server-host1.log ZK业务日志(含会话事件) `grep -E "(Created
zookeeper-$USER-server-host1-gc.log 专用GC日志(需在zkEnv.sh中开启) tail -100 zookeeper*-gc.log | grep "Total time"

要开启GC日志,需修改 /opt/zookeeper/bin/zkEnv.sh ,在 ZOOMAIN 变量前插入:

# Ubuntu 18.04专用GC参数(避免G1GC兼容问题)
ZOO_LOG4J_PROP="INFO,ROLLINGFILE"
JVMFLAGS="$JVMFLAGS -Xloggc:/opt/zookeeper/logs/zookeeper-gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps"

5.3 网络连通性自检脚本

针对Ubuntu 18.04常见的ufw/iptables干扰,编写一键检测脚本 zk-net-check.sh

#!/bin/bash
NODES=("192.168.1.101" "192.168.1.102" "192.168.1.103")
PORTS=(2181 2888 3888)

for node in "${NODES[@]}"; do
  echo "=== Testing $node ==="
  for port in "${PORTS[@]}"; do
    if timeout 2 bash -c "echo > /dev/tcp/$node/$port" 2>/dev/null; then
      echo "  Port $port: OK"
    else
      echo "  Port $port: FAILED (check ufw/iptables)"
      sudo ufw status | grep "$port"
    fi
  done
done

赋予执行权限后运行: chmod +x zk-net-check.sh && ./zk-net-check.sh

5.4 数据一致性校验方案

当怀疑集群数据不一致时(如 ls / 在不同节点返回不同结果),用 zkCli.sh 导出快照比对:

# 在每台节点执行(替换IP为本机IP)
/opt/zookeeper/bin/zkCli.sh -server 127.0.0.1:2181 ls / > /tmp/zk_nodes_$(hostname).txt
# 比较三台节点的输出
diff /tmp/zk_nodes_host1.txt /tmp/zk_nodes_host2.txt
diff /tmp/zk_nodes_host2.txt /tmp/zk_nodes_host3.txt

若发现差异,说明集群已分裂,需立即停止写入,用 zkSnapShotToolkit 工具分析快照文件(需单独编译)。

最后分享一个血泪教训:某次升级ZK版本后, zkCli.sh 连接时提示 Authentication is not valid 。排查三天才发现是 /opt/zookeeper/conf/zoo.cfg 中多了一个空行,导致ZK解析配置时将空行后的 server.1 误认为新配置项。从此我养成了每次修改配置后执行 grep -n "^server\|^$" /opt/zookeeper/conf/zoo.cfg 的习惯——空行和注释符号 # 的位置,就是Ubuntu 18.04上最危险的语法雷区。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值