1. “not connected”不是一条错误日志那么简单
你是不是也在某天凌晨被一条ZooKeeper not connected的告警折腾醒过?日志里反复出现连接断开、重连失败、会话超时,服务状态看起来又好像没挂。这个问题在 Zookeeper 3.7.0 上会出现,在 3.4、3.5、3.6 上同样会出现,区别只是报错上下文略有差异。很多开发第一反应是“ZooKeeper 集群挂了”,赶紧往服务器上冲,实际上大多数时候服务端活得好好的,问题出在客户端、网络或者配置参数上。
先理清一个概念:not connected是客户端会话状态,不是服务端状态。ZooKeeper 客户端进程内部维护着一个状态机,刚启动时处于CONNECTING,这时还没跟任意一台服务器建立有效连接,对外表现就是“连不上”;连接成功后会变成CONNECTED,如果中途网络抖动、服务端选主、客户端 GC 停顿,状态又会退回CONNECTING。如果超过会话超时时间仍没有恢复,服务端会判定会话过期,客户端收到SessionExpiredException。所以“not connected”和“session expired”是两个层面的问题,前者是连接通道问题,后者是会话生命周期问题。
1.1 会话超时机制和 tickTime 的关系
ZooKeeper 以tickTime作为基础时间单位,默认是 3000ms,也就是 3 秒。服务端对客户端设置的sessionTimeout并不是原样接受,而是会做一次边界修正:会话超时最小不能低于2 * tickTime,最大不能高于20 * tickTime。如果没显式配置maxSessionTimeout,服务端上限就是 20 倍 tickTime。比如默认 tickTime=3000,客户端想设置 60000ms 超时,服务端实际只会给你 60000ms 吗?不会,它会把你的会话超时截断到 60000 与上限中的最小值,最终生效值远小于你的预期。这就是为什么有些人明明在构造ZooKeeper对象时把超时写得很大,却依然频繁出现 session expired 的根本原因。
1.2 低版本为什么也适用同一套排查思路
从 3.4 到 3.7,ZooKeeper 的客户端连接状态机、会话机制、四字命令、zoo.cfg核心参数几乎没变过。3.7.0 在安全、可观测性、IPv6 支持方面做了增强,但底层两阶段提交、leader 选举、会话管理这些模型依然稳定。所以只要你能理解“服务端状态 → 客户端配置 → 网络连通性”这条排查链,不管面前跑的是哪个版本,都能快速收敛问题。下面所有方案我都按 3.7.0 场景写,同时会在关键点上标注低版本差异。
2. 服务端排查:先用三分钟确定它是否真的活着
看到 not connected,第一步永远是确认服务端是否可用,而不是急着调客户端参数。服务端如果挂了,客户端再怎么折腾都没用。
2.1 四字命令和状态查询
ZooKeeper 一直保留着四字命令,最常用的是srvr、stat、mntr。在服务器本机执行:
echo srvr | nc 127.0.0.1 2181正常输出会包含Zookeeper version: 3.7.0、Mode: standalone或Mode: leader/follower,以及延迟、连接数、节点数等信息。如果输出空,先看端口是不是被换过,再看进程是否还活着。也可以直接执行安装目录下的脚本:
bin/zkServer.sh status集群模式下,这条命令会告诉你当前节点是 leader 还是 follower,还是处于unknown。出现unknown说明节点没有成功加入集群多数派,客户端自然连不上。
2.2 进程、端口和启动日志
优先检查进程和监听端口:
jps | grep QuorumPeerMain ss -lntp | grep 2181如果进程在但端口没监听,多半是启动过程中报错退出了,典型原因有三个。一是磁盘写满,ZooKeeper 依赖事务日志落盘,磁盘满会导致写入异常;二是dataDir权限不对,进程没有写权限;三是 3.5 以后默认开启 AdminServer,它占用 8080 端口,如果你的业务程序也占用了这个端口,ZooKeeper 会启动失败。检查启动日志:
tail -200 logs/zookeeper-root-server-*.out看到Exception while listening或者Address already in use就能定位。
2.3 集群模式下最容易翻车的几个点
服务端是多机部署时,还要检查myid文件和zoo.cfg里的server.x配置是否一一对应。myid写的是数字,必须和server.1=zk1:2888:3888中的编号一致。如果有两个节点写了同样的 myid,选主就会出现脑裂;如果只有两个节点,其中一个宕机,剩余节点无法形成多数派,整个集群对外表现为不可用,客户端疯狂报 not connected。
另外注意initLimit和syncLimit。initLimit是 follower 从 leader 同步初始数据的时间上限,单位是 tickCount,默认 10。如果这个值太小,节点启动时数据量很大,同步无法在限定时间内完成,节点会被判定连接超时,从而一直处于 looking 状态。这类问题在客户端日志里表现为“找不到可用服务器”,而不是传统的连接拒绝。
提示:四字命令在新版本里默认有白名单,如果执行
echo ruok | nc没反应,不代表服务端挂了。优先用srvr和stat,或者通过zkServer.sh status确认。
3. 客户端配置:经常被忽视的三个关键点
服务端确认无恙后,就要回头审视客户端了。根据我自己排障经验,not connected 在客户端侧的根因,集中在连接字符串、会话超时和错误处理方式。
3.1 connectString 的隐藏规则
构造 ZooKeeper 客户端时,连接字符串格式是host1:port1,host2:port2,host3:port3/chroot。这个字符串有三个容易出错的地方。
第一,多个地址用英文逗号分隔,不是分号,不能有空格。"zk1:2181, zk2:2181"这种写法里多了一个空格,解析时会尝试连接一个带空格的地址,导致连接失败。
第二,如果末尾带了/chroot,那么这个 znode 必须已经存在,客户端会自动把整个会话限制在这个命名空间下。很多人创建连接时顺手写了一层 chroot,但忘记了先在服务端create /chroot创建它,结果客户端一直报ConnectionLoss或SessionExpired。
第三,客户端机器必须能解析配置里的主机名。如果你的connectString写的是zk1:2181,但应用服务器上/etc/hosts没有zk1的映射,那么 TCP 连接永远无法建立。常规做法是写 IP,或者保证所有节点主机名都能正常解析。
3.2 sessionTimeout 到底设多少才算合理
很多业务系统为了不丢数据,喜欢把 sessionTimeout 调大,比如 60 秒,但往往忽略服务端的边界修正。建议先确认服务端tickTime,再结合应用场景设置。如果 tickTime=3000,客户端设置在 15 秒到 30 秒之间是比较稳妥的区间。太小的话,一次正常的 leader 选举切换(可能持续几秒)就会导致会话过期;太大又会让服务端迟迟不清理故障客户端的临时节点,影响锁释放速度。
还有一个容易忽略的坑:应用进程发生长 GC。当客户端 JVM 出现 Full GC,暂停时间可能长达十几秒,即使网络正常,ZooKeeper 客户端也来不及发送心跳,服务端会判定会话超时。这种场景下单纯调大 sessionTimeout 能缓和,但不能根治,必须从应用 JVM 参数和 GC 优化入手。
3.3 客户端不能光创建连接就万事大吉
原生new ZooKeeper(connectString, sessionTimeout, watcher)是异步连接,构造方法返回后连接可能还没建立。如果马上调用getData(),通常会抛ConnectionLossException。正确做法是注册一个Watcher,在Event.EventType.None且getState()为SyncConnected后再执行业务操作。更工程化的做法是使用 Curator 框架,它内部已经把重试、状态监听、路径校验这些脏活封装好了。
下面是用 Curator 创建客户端的标准姿势:
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 5); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("zk1:2181,zk2:2181,zk3:2181") .sessionTimeoutMs(30000) .connectionTimeoutMs(10000) .retryPolicy(retryPolicy) .build(); client.start(); client.getConnectionStateListenable().addListener((curator, state) -> { if (state == ConnectionState.CONNECTED) { // 连接建立,可以执行业务操作 } else if (state == ConnectionState.LOST) { // 会话可能丢失,需要重新构建上层状态 } });Curator 自带的ExponentialBackoffRetry会在连接失败后按指数退避重试,避免造成“重试风暴”。低版本 ZooKeeper 同样适用,因为 Curator 依赖的是客户端协议,而协议接口是稳定的。
4. 网络与运行环境:那些概率小但一踩一个准的坑
服务端活着,客户端代码也没问题,可还是 not connected,问题往往就躲在网络和主机环境里。
4.1 防火墙、安全组和端口可达性
2181 端口看起来是开放的,但很多云环境里不仅有安全组限制,还有主机内部 iptables、SELinux 策略。客户端连不上,第一时间用telnet或nc验证:
nc -vz zk1 2181如果不通,从两端同时排查。先看服务端监听0.0.0.0:2181还是只监听了本机回环地址,再看安全组是否放行了客户端 IP。我曾遇到过服务端没任何异常,但是客户端所在的容器网络没有放行出方向到 ZK 端口的策略,导致所有连接超时。这种问题用tcpdump抓包最容易确认,服务端能收到 SYN,但回包被丢弃。
4.2 主机名解析和时钟漂移
ZooKeeper 集群节点之间通信依赖zoo.cfg中配置的主机名或 IP。如果每个节点配置的是主机名,就要确保所有节点(包括客户端应用服务器)都能解析这些主机名。不然客户端找到服务器列表后,发起连接时解析失败,状态一直停在 CONNECTING。
时钟漂移也是隐藏杀手。ZooKeeper 会话超时依赖服务器系统时间,集群节点之间时钟偏差过大,leader 判断 follower 心跳就会出错。客户端机器时钟如果被业务手动改动,也可能造成服务端时间戳跳变,导致连接状态异常。生产环境必须统一用时间同步服务校准。
4.3 GC 长停顿和线程资源竞争
之前提到 GC 停顿会导致心跳中断,但很多团队直到定位线上问题才会把它纳入排查范围。看客户端 JVM 日志,如果一次 Full GC 超过 sessionTimeout 的三分之一,就必须重视。可以启用 GC 日志:
-Xlog:gc*:file=/data/logs/gc.log:time,uptime,level,tags另外还要关注客户端所在容器是否限制了 CPU 和内存。如果 cgroup 限额过小,CPU 争抢严重,ZooKeeper 客户端的发送线程可能迟迟得不到调度,表现就是连接被服务端空闲超时断开。
5. 一次完整排障复盘:从乱象到根因
光说排查步骤太抽象,分享一个典型的排查过程。某个业务模块升级到 Zookeeper 3.7.0 后,应用日志里开始频繁出现KeeperException$ConnectionLossException,紧接着是ZooKeeper not connected,服务间调用成功率掉到 80%。第一反应是怀疑新版本有问题,甚至考虑回滚到旧版本。但我没有急着换版本,而是按下面的顺序走了一遍。
5.1 排障过程记录
先在三台 ZooKeeper 服务器上执行echo srvr | nc 127.0.0.1 2181,状态分别是 leader 和 follower,集群健康。再执行bash bin/zkServer.sh status,确认 leader 已正常选出。随后在应用服务器上执行nc -vz zk1 2181,网络层通,端口能访问。
接着用 ZooKeeper 自带命令行工具测试:
bin/zkCli.sh -server zk1:2181命令行可以正常连接,也能执行ls /。这说明服务端口、鉴权配置都没问题。问题范围缩小到应用代码或 JVM 环境。查看应用的 GC 日志,发现每次报错前后都伴随一次老年代 Full GC,暂停时间最长达到 18 秒,而应用配置的 sessionTimeout 只有 10 秒。真相大白,不是 3.7.0 的问题,是会话超时跑不过长 GC。
把 sessionTimeout 调整到 30 秒,并且优化了堆内存分配,减少了 Full GC 频率,问题消失。这个案例特别典型,因为它几乎集合了所有干扰项:新版本、多个节点、中间网络代理。但如果一开始就被版本表象带走,很容易浪费时间。
5.2 常见问题速查表
这里整理一份高频原因对照表,方便你排障时直接查阅。
| 现象 | 可能原因 | 快速验证 | 处理方式 |
|---|---|---|---|
| 所有客户端都连不上 | 集群多数节点宕机或选主失败 | zkServer.sh status | 检查 myid、server.x、存活节点数 |
| 部分客户端连不上 | 安全组/防火墙/IP 白名单 | nc -vz 目标端口 | 放通对应来源 IP 端口 |
| 启动一段时间后断连 | 客户端 GC 停顿过长 | 查看 Full GC 频率 | 调大 sessionTimeout,优化 JVM |
| 偶发 ConnectionLoss | 网络抖动/leader 切换 | 观察集群选主日志 | 客户端增加重试策略 |
| 设置了超时不生效 | 超过服务端上限被截断 | 查看 tickTime 配置 | 按 2~20 倍 tickTime 重新设置 |
| 带 chroot 连不上 | chroot 路径不存在 | 检查 / 下 znode | 先创建对应路径 |
6. 从根上减少 not connected:配置与高可用优化
每次都靠救火式排障太被动,不如把基础配置和客户端侧机制一次到位。
6.1 zoo.cfg 参数参考
以下是一份经过实战验证的配置模板,3.7.0 和低版本都可以直接参考:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper dataLogDir=/data/zookeeper/log clientPort=2181 maxClientCnxns=1000 autopurge.snapRetainCount=3 autopurge.purgeInterval=1 admin.enableServer=truetickTime=2000意味着客户端会话超时下限 4 秒,上限 40 秒,应用侧 sessionTimeout 建议设置在 10 到 30 秒之间。autopurge.purgeInterval=1让服务端每小时自动清理旧快照和事务日志,避免磁盘被历史数据撑爆。maxClientCnxns根据业务连接数调整,默认 60 在某些高并发场景下明显不够用,但只要服务端连接数达到上限,新客户端就会直接被拒绝,表现为 not connected。
6.2 客户端用 Curator 管理连接状态
原生客户端适合学习,但生产环境我更推荐 Curator。它能维护连接状态监听,自动重连,并提供分布式锁、Leader 选举等高级组件。更重要的是,Curator 对会话恢复有明确的语义:CONNECTED表示会话可用,SUSPENDED表示连接暂时断开但会话未过期,LOST表示必须重新创建整个会话。这些状态能帮业务在上层实现优雅降级,而不是一脸懵地看到 not connected。
6.3 监控要盯准四个指标
除了被动处理告警,还要主动监控服务端健康指标。四字命令mntr会输出大量监控项,重点盯这四个:
zk_avg_latency:平均响应延迟,持续上涨说明队列积压或磁盘慢。zk_outstanding_requests:堆积请求数,长时间居高不下说明处理不过来。zk_num_alive_connections:活跃连接数,突然暴跌可能代表客户端集体重连。zk_synced_followers:同步中的 follower 数量,低于正常值说明集群存在同步风险。
把这些指标接入现有监控系统,配置合适阈值,很多连接问题会在造成大面积影响之前暴露出来。
7. 最后再分享一点个人习惯
排查 not connected 时,我习惯先在纸上写一条时间线:客户端第一次报错是什么时候,服务端有没有选主,网络有没有变更,最近发布过什么配置。把时间线对齐,基本能排除掉 80% 的干扰项。另一个习惯是每次调参只改一个变量,很多人为了快速恢复,同时调大了 sessionTimeout、加了 retry 次数、改了连接字符串,结果问题解决了也不知道是谁的功劳,下次踩坑还要再排一遍。
如果你现在正在被 Zookeeper 3.7.0 的 not connected 困扰,先憋住重启集群的冲动。按服务端状态、客户端配置、网络环境这条线走一遍,大概率能在五分钟内找到答案。这个思路在 3.4 到 3.7 的各个版本里同样有效。