1. 单机也能跑,为什么项目里一定要组集群
我见过太多项目初始阶段就部署一个单机ZooKeeper,理由也都差不多——"现阶段就验证个功能,不需要上集群"。但只要后续接上HDFS HA或者Kafka,单机ZooKeeper就变成一颗隐形炸弹。之前线上HDFS集群突然写入超时,排查半天才发现是ZooKeeper所在机器磁盘IO被打满,NameNode的Active/Standby状态反复抖动,整个分布式系统就像丢了大脑。从那之后,我给自己定了一条规矩:任何涉及分布式存储或消息队列的技术底座中,ZooKeeper必须三台起步,没有例外。这篇文章就是围绕ZooKeeper集群搭建,把从规划、配置、启动到运维以及和Hadoop/Spark生态整合的完整经验梳理一遍。
1.1 ZooKeeper到底在分布式系统里扮演什么角色
很多人刚接触时觉得ZooKeeper就是一个带树形结构的小型数据库。这个理解不算错,但不够准确。它真正解决的是分布式系统里的协调问题:多个节点之间谁当主、谁做备,哪些节点还活着,分布式锁怎么拿,配置信息怎么实时同步。HDFS的NameNode HA、Kafka的broker注册和controller选举、Spark的调度器协调、Dubbo的服务发现,底层都依赖ZooKeeper这份"共识服务"。
它内部维护的是一棵znode树,支持持久节点、临时节点、顺序节点。这个设计配合Watcher机制,让客户端可以"订阅"某个节点的变化——数据变了立刻收到通知,而不是反复轮询。临时节点尤其关键:客户端与会话绑定,会话一断,节点就自动消失,分布式系统里判断"某个节点是否还活着"全靠这一招。你在ZooKeeper集群里看到大量临时节点,其实就说明有不少应用正通过它做心跳检测和服务状态管理。
1.2 过半存活与Leader选举:集群可用性的底层逻辑
ZooKeeper集群的可用性依赖一个核心机制——quorum,也就是法定人数。以三节点集群为例,一个写请求必须被超过半数的节点确认后才算提交成功。三节点允许挂一台,五节点允许挂两台。选举Leader也是同样的逻辑:一个节点要成为Leader,必须获得超过半数的投票,所以任意时刻集群内只能有一个逻辑上的"大脑",不会出现两个Leader同时对外服务的脑裂情况。
举个具体的写入流程:客户端把写请求发给Leader,Leader生成事务并写入本地事务日志,同时把事务广播给所有Follower。Follower各自写日志,然后返回ACK。当Leader收到的ACK数量超过一半,事务才算提交成功,再通知所有Follower应用这个变更。这中间任何一个环节都依赖quorum计算。用生活类比就像开会表决:不是所有人都在场才能做决定,但超过半数人认可后决议就有效,哪怕剩下的人网络不通也影响不了已经达成的共识。
1.3 单机、双机和三机的本质区别
单机就不说了,挂在哪儿哪儿瘫痪。双机也不是大家想的"少一台而已"。双机没有过半概念:任意一台挂掉,剩余节点都凑不出2票的多数,集群直接不可用。如果硬要做主备镜像,一旦网络隔离,两台机器各自认为自己是主,等网络恢复时数据已经分成两套,没法收场。生产环境最小配置就是三台,这不是拍脑袋的规矩,而是经历过无数故障后验证过的底线。你去看各种分布式产品的官方HA部署文档,ZooKeeper的最小推荐集群全部是奇数节点,原因都指向同一个机制:quorum。
2. 集群规划:3台、5台还是混搭Observer
搭建哪件事都不难,难的是开始动手之前那一串决策:到底买几台机器?每台的规格给多少?要不要引入Observer角色?这节把决定做的过程完整还原一遍。
2.1 奇数节点数量的数学账
很多文章只说"要奇数",但没讲透底层逻辑。核心就是过半机制。三节点容错一台,四节点容错还是只能一台。五节点容错两台,六节点容错仍然是两台。多买的一台偶数机器,并没有提升容错能力,反而让每次投票要多等一台机器回包,增加网络开销。所以四台和五台面临同样的故障风险,五台支出更多;而三台能解决的问题,四台并不会做得更好。
换个更严谨的说法:集群最多能容忍的故障数是 floor(N/2)。想容忍F台机器故障,就需要至少 2F+1 台机器。容忍一台故障就得三台,容忍两台故障就得五台。这就是 2F+1 公式的由来。很多刚入门的人觉得五台比三台"更稳",实际上五台的稳定来自两台冗余,而不是"多两台机器所以概率上更抗造"。规划时根据业务的重要程度选3或5就够了,很少有场景需要一次性铺七台。
2.2 机器选型:配置不是越高越好
ZooKeeper本身的数据量其实很小——它存储的是协调元数据,不是业务数据。snapshot快照加事务日志,正常情况下也就几个GB。所以CPU和内存不是瓶颈,真正需要认真考虑的是三样东西:
- 磁盘I/O。事务日志必须顺序写,如果磁盘性能差,写延迟会直接传导到所有依赖ZooKeeper的上层应用。日志盘建议用SSD,至少也要选性能稳定的云盘类型。
- 网络稳定性。节点之间靠心跳维持连接,网络抖动会误判节点死亡,触发不必要的选举,而每次选举都有短暂不可写窗口。
- 时钟偏差。集群节点的时钟要尽量同步。ZooKeeper并不完全依赖物理时钟做决策,但明显的时间漂移会影响会话超时判断和日志时间戳排查。
实际生产中,2核4G的虚机跑ZooKeeper集群非常常见。真正要花心思的是目录规划:dataDir和dataLogDir一定要分到不同磁盘上,日志盘优先SSD。千万不要两个目录放同一块系统盘,否则系统日志涨起来,ZooKeeper的写性能先遭殃。
2.3 Observer角色:什么时候必须考虑
如果投票节点超过五台,每次写操作都要等更多节点确认,选举时间也会变长。这时可以把只提供读服务、不参与投票的节点设成Observer。Observer不参与Leader选举,也不参与写入确认,只跟在Leader后面同步数据,专门给客户端提供横向扩展的读能力。
典型部署形态是:五台投票节点保障容错和写入,再挂若干Observer对外承接大量读写请求。需要注意,Observer适合读多写少的场景。如果业务里写比例特别高,Observer带来的读扩展意义有限,不如直接优化上层应用的ZooKeeper访问模式。
3. 从下载到三机全绿:ZooKeeper集群搭建实录
规划做完了,开始实际操作。这套步骤我在测试环境和生产环境反复执行过很多次,每一步都可能踩坑,我会把容易忽略的细节单独标出来。
3.1 环境准备:JDK版本和目录结构
ZooKeeper是Java写的,依赖JDK,建议用JDK 8或11。版本太新反而可能遇到一些老组件兼容问题,没必要追新。操作系统CentOS 7、Ubuntu 18.04以上都可以,并没有特殊要求。从Apache官网镜像站下载稳定版,3.7.x或3.8.x均可,核心配置逻辑一致。解压后我习惯统一放到/opt/zookeeper下,再做一个软链指向具体版本目录,后期升级版本只需要切换软链地址。比如:
tar -zxvf apache-zookeeper-3.8.4-bin.tar.gz ln -s /opt/apache-zookeeper-3.8.4-bin /opt/zookeeper需要注意,解压出来的目录带-bin后缀才是免编译版。还有人会用一个很老的习惯,把conf目录下的zoo_sample.cfg复制成zoo.cfg,这个动作没问题,但要立刻把里面的内容全替换掉,不要留样例配置。
数据目录不能选系统盘。第一套生产环境我就吃过这个亏,dataDir和系统日志共用一块盘,系统日志一膨胀,ZooKeeper写事务日志直接变慢,整个集群写延迟飙升。之后我固定用两块独立盘:一块给dataDir,一块给dataLogDir,环境再紧张也要保证物理隔离。
3.2 zoo.cfg的逐行拆解
先把最小可用的zoo.cfg贴出来,然后逐行解释:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper/data dataLogDir=/data/zookeeper/logs clientPort=2181 maxClientCnxns=60 autopurge.snapRetainCount=3 autopurge.purgeInterval=12 server.1=zk-node-01:2888:3888 server.2=zk-node-02:2888:3888 server.3=zk-node-03:2888:3888这些参数逐个讲一遍:
- tickTime=2000。基础时间单元,单位毫秒。ZooKeeper里很多超时都是它的整数倍,比如下面两个Limit。
- initLimit=10。Follower启动时连接Leader并完成数据同步的最大时间,10个tick等于20秒。如果数据中心内网不太稳,这个值要适当调大,否则新节点会一直卡在CONNECTING状态。
- syncLimit=5。Follower和Leader之间心跳超时上限,5个tick等于10秒。设太短,网络抖动会被误判为节点故障导致重选;设太长,故障感知变慢。建议根据机房内网的实际ping延迟调整。
- dataDir和dataLogDir。快照目录和事务日志目录,记得分开。
- clientPort=2181。客户端连接端口,应用配置里的连接串都指向它。
- maxClientCnxns=60。单个IP能建立的连接数上限。如果上层有大量客户端集中部署在少数机器上,这个值按需调大,默认60容易触发"Too many connections"。
- autopurge.snapRetainCount=3。保留最近3份快照,其余旧文件自动清理。
- autopurge.purgeInterval=12。清理周期,单位小时。
- server.X=host:2888:3888。X是节点编号,后面2888是节点间数据同步端口,3888是选举端口。三行的X分别是1、2、3。
这里最容易被忽视的是initLimit。第一次搭集群时我在模拟弱网环境里启动第三节点,结果等了五分钟还是LOOKING,查日志发现就是initLimit不够,Follower同步历史数据超过了20秒的上限。调成30秒后恢复正常。如果网络条件复杂,建议initLimit直接给15甚至20,付出的代价只是启动时多等几秒,收益是避免莫名其妙的加入失败。
3.3 myid:就一个数字,却卡住了不少人
每个节点的dataDir目录下必须创建一个myid文件,内容就是该节点在server.X里的编号。没有扩展名,不要写别的字符,保持一个纯数字文件最省心。比如node-01上执行:
mkdir -p /data/zookeeper/data /data/zookeeper/logs echo "1" > /data/zookeeper/data/myid这个文件是ZooKeeper启动时识别集群身份的关键。myid和zoo.cfg里的server编号对不上,或者myid被放到了错误目录下,节点启动会直接报类似"Invalid myid"的错误。还有一个高频坑:很多团队用统一的脚本把配置和data目录一起从第一台机器分发到其他节点,结果把第一台的myid也复制过去了,三台机器全部以为自己是"1",自然组不成集群。正确的做法是只分发zoo.cfg,myid在各节点手动创建。
3.4 启动、验证与角色确认
三台机器都准备好后,分别执行启动命令:
/opt/zookeeper/bin/zkServer.sh start启动顺序上我习惯一台一台来。第一台启动时它在独自等待,日志里能看到节点状态为LOOKING;第二台启动后,两台通过3888端口开始选主;第三台加入后形成过半法定人数,Leader产生。整个过程通常几秒到十几秒。启动完成后别急着收工,用status命令验证:
/opt/zookeeper/bin/zkServer.sh status正常输出会显示Mode: leader或Mode: follower。三台机器上应该恰好一台Leader、两台Follower,这代表选举完成。再用客户端连一下,测试读写:
/opt/zookeeper/bin/zkCli.sh -server zk-node-01:2181 create /test version-1 get /test能创建也能读取,说明集群对外服务正常。如果发现所有节点全是Leader或者全是Follower,基本就是配置、网络或myid出了问题,去对应章节排查即可。
4. Leader挂掉的那十分钟:运维、监控与故障恢复
集群搭好只是起点。日常运维里,真正考验人的是Leader节点故障之后谁能快速顶上,以及我们能不能第一时间发现异常。下面这些内容都是实际运维经验,建议搭完集群后照着做一遍。
4.1 故意杀掉Leader:一次故障演练
集群刚搭完,最值得做的一次测试就是主动把Leader杀掉。先确认当前Leader在哪台:
/opt/zookeeper/bin/zkServer.sh status假设Leader是zk-node-02,登录那台机器杀掉进程:
kill -9 <zk_pid>此时另外两台Follower会感知到Leader丢失,进入新一轮选举。由于ZAB协议要求新Leader必须在过半节点中达成一致,所以三节点集群里剩下的两台可以凑出法定人数,几秒内就会选出新Leader。实测过程中,从Leader被kill到新Leader产生,通常只需要2到5秒。期间的ZooKeeper写请求会短暂失败,但读请求不受影响,因为Follower仍然能处理读。
这个测试的真正价值不是看集群能不能活,而是验证你的客户端能否安然度过这几秒。很多应用的ZooKeeper客户端没有配置合理的重试策略,在Leader切换的几秒内直接抛异常退出。等这个风险暴露在生产环境就晚了,所以集群搭完第一件事就是做故障演练,顺便把上层应用的异常处理链路验一遍。
4.2 四字命令和监控指标:哪些必须盯
ZooKeeper自带四字命令,通过nc工具就能查询状态。新版默认只开放少数命令,需要在zoo.cfg里显式加白名单:
4lw.commands.whitelist=ruok,stat,mntr,conf,cons改完配置后重启节点,再执行:
echo mntr | nc zk-node-01 2181输出里会列出大量运行时指标。我平时重点盯这几个:
- zk_server_state。当前角色,leader还是follower,随时确认只有一台Leader。
- zk_znode_count。znode总数。如果发现它在持续快速增长,多半是某个客户端在疯狂创建临时节点,很可能是应用逻辑出问题了。
- zk_watch_count。Watcher数量。数量过大说明客户端设计不够合理,watch风暴会把集群拖垮。
- zk_pending_syncs。处于pending状态的同步请求数,长期不为0说明Follower的数据同步速度跟不上。
- zk_outstanding_requests。堆积中的未处理请求数,一旦常驻在100以上,读写性能就已经告警了。
这些指标最好接入Prometheus和Grafana,让ZooKeeper的JMX信息自动汇总。监控面板上除了业务指标,还需要盯每台机器的CPU、内存、磁盘IO和网络延迟,ZooKeeper对IO的敏感性在前面已经说过,不再重复。
4.3 JVM与GC调优:小服务也有大学问
ZooKeeper默认堆内存是1G,大多数场景够用。但如果你在跑HBase、Kafka这种重度依赖ZooKeeper的系统,建议把堆内存调到2G到4G。要注意,堆不是越大越好。JVM的GC停顿是ZooKeeper最怕的事情——一次Full GC造成的秒级停顿,可能让心跳超时,触发一轮无谓的Leader选举。
生产环境我用下面这组JVM参数:
export JVMFLAGS="-Xmx4g -Xms4g -XX:+UseG1GC -XX:MaxGCPauseMillis=200"设置堆内存固定4G,使用G1收集器并限制最大GC暂停时间在200毫秒内。GC日志也建议打开,单独放到一个目录,方便事后排查。这里要特别提醒一句:修改JVM参数后按节点顺序逐个重启,不要一次全部重启,否则整个集群会有一段很长的不可用窗口。真正的滚动顺序是:先重启一台,等它恢复Follower状态并同步完数据,再重启下一台。
4.4 常见故障排查链路:从现象找根因
故障排查讲究的是从现象倒推根因,按步骤来。
第一个高频故障:节点一直处于LOOKING状态。先测试这台机器与其他节点之间的2888和3888端口通不通:
nc -vz zk-node-02 2888 nc -vz zk-node-02 3888不通就检查防火墙和安全组。通的话再看看myid是否匹配、zoo.cfg里的server列表是否写全。
第二个高频故障:客户端连接总是超时。先看客户端连接串有没有把三台机器都写上,只写一台会导致那台故障时客户端没有备选。再看maxClientCnxns是不是被某个集中部署的应用耗尽了。最后把时间段拉长,看看有没有周期性GC长暂停的迹象。
第三个高频故障:Leader频繁切换。优先怀疑网络延迟和抖动,再看磁盘IO。这两者都排除后,去查每台机器的JVM Full GC次数。三个排查方向按顺序做,基本能覆盖绝大多数切换原因。千万不要一上来就重启节点,重启只能临时掩盖问题,根因不除,过两天还会再犯。
5. ZooKeeper和Hadoop/Spark生态整合的实战衔接
热搜词里有大量关于Hadoop和ZooKeeper整合、Spark集群搭建的需求,说明很多人搭ZooKeeper集群的目的就是为了撑起大数据底座。这章讲几个整合过程中实际会遇到的问题。
5.1 HDFS HA模式下ZooKeeper承担的关键职责
HDFS高可用架构里,两个NameNode一主一备,由ZooKeeper协调谁真正对外提供服务。具体配合过程是:JournalNode负责同步edits日志,ZKFC进程监控NameNode状态并向ZooKeeper注册临时节点。Active NameNode出现故障后,对应的临时节点消失,ZooKeeper通知备用NameNode重新选主,完成秒级切换。
和ZooKeeper集群相关的最典型配置在hdfs-site.xml里:
<property> <name>ha.zookeeper.quorum</name> <value>zk-node-01:2181,zk-node-02:2181,zk-node-03:2181</value> </property>这里建议把三台机器的地址都写上,让客户端自己去选节点连接,不要在前面加负载均衡器。ZooKeeper客户端本身就内置了多地址轮换机制,硬加负载均衡反而会让会话管理变复杂。
Spark的情况简单一些。Spark早期Standalone模式用ZooKeeper做Master高可用,指定ZooKeeper地址即可。Spark on YARN模式下,YARN的ResourceManager HA同样依赖ZooKeeper。换句话说,一旦你的技术栈里启用HDFS HA或YARN HA,ZooKeeper集群就是整个大数据底座最底层的依赖,它的一举一动直接决定上层全链路的稳定性。
5.2 会话超时与会话重连:被忽略的客户端参数
以Java客户端为例,ZooKeeper允许配置sessionTimeout,但很多新人不清楚的是,这个超时时间不是客户端单方面说了算,而是客户端和服务端协商后的结果。服务端有minSessionTimeout和maxSessionTimeout两个参数,默认分别是2个tick和20个tick,也就是4秒到40秒。客户端把sessionTimeout设成60秒,服务端会强制截断到40秒;设成1秒,也会被拉高到4秒。所以只看客户端配置容易产生错觉。
如果需要更灵活的会话超时范围,建议在zoo.cfg里显式调整:
minSessionTimeout=4000 maxSessionTimeout=60000改完重启节点才生效。另一个实战经验是客户端会话重连策略。我在项目里常用CuratorFramework,它对ZooKeeper会话管理封装得比较完善,重试策略配置如下:
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 30, 30000); CuratorFramework client = CuratorFrameworkFactory.newClient( "zk-node-01:2181,zk-node-02:2181,zk-node-03:2181", 30000, 30000, retryPolicy); client.start();ExponentialBackoffRetry的三个参数分别是初始间隔1秒、最大重试次数30、最大间隔30秒。这样的策略保证Leader切换或者单节点故障时,客户端会自动退避重连,而不是第一次失败就彻底退出。
5.3 集群扩容与缩容:提前规划才不慌
集群不是搭完就结束了,业务量增长后扩容是必然动作。新节点沿用同样的安装方式,把zoo.cfg配好,myid写对应编号,再启动节点。老版本的ZooKeeper只能改配置后滚动重启;新版本支持reconfig动态配置,可以在不停服的情况下增删节点。使用动态配置时,需要在zoo.cfg里开启:
dynamicConfigFile=/data/zookeeper/conf/zoo.cfg.dynamic缩容要更加谨慎。如果缩掉的是一台参与投票的节点,集群的过半门槛会改变。5台缩到3台,容错能力从2台变成1台,这是容量规划层面的变化,不是简单删除一台机器的问题。所以优先缩Observer节点,对整体投票能力和容错能力影响最小。
最后再分享一个实际体会:把ZooKeeper集群的所有信息文档化,包括每台机器的IP、角色、数据目录、JVM参数、监控脚本,全部落到团队知识库。搭建过程可能只花两个小时,但在后来某个故障夜晚,你翻着文档快速定位到问题的那十分钟,就是这些记录最好的回报。