
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同时对外服务的脑裂情况。举个具体的写入流程客户端把写请求发给LeaderLeader生成事务并写入本地事务日志同时把事务广播给所有Follower。Follower各自写日志然后返回ACK。当Leader收到的ACK数量超过一半事务才算提交成功再通知所有Follower应用这个变更。这中间任何一个环节都依赖quorum计算。用生活类比就像开会表决不是所有人都在场才能做决定但超过半数人认可后决议就有效哪怕剩下的人网络不通也影响不了已经达成的共识。1.3 单机、双机和三机的本质区别单机就不说了挂在哪儿哪儿瘫痪。双机也不是大家想的少一台而已。双机没有过半概念任意一台挂掉剩余节点都凑不出2票的多数集群直接不可用。如果硬要做主备镜像一旦网络隔离两台机器各自认为自己是主等网络恢复时数据已经分成两套没法收场。生产环境最小配置就是三台这不是拍脑袋的规矩而是经历过无数故障后验证过的底线。你去看各种分布式产品的官方HA部署文档ZooKeeper的最小推荐集群全部是奇数节点原因都指向同一个机制quorum。2. 集群规划3台、5台还是混搭Observer搭建哪件事都不难难的是开始动手之前那一串决策到底买几台机器每台的规格给多少要不要引入Observer角色这节把决定做的过程完整还原一遍。2.1 奇数节点数量的数学账很多文章只说要奇数但没讲透底层逻辑。核心就是过半机制。三节点容错一台四节点容错还是只能一台。五节点容错两台六节点容错仍然是两台。多买的一台偶数机器并没有提升容错能力反而让每次投票要多等一台机器回包增加网络开销。所以四台和五台面临同样的故障风险五台支出更多而三台能解决的问题四台并不会做得更好。换个更严谨的说法集群最多能容忍的故障数是 floor(N/2)。想容忍F台机器故障就需要至少 2F1 台机器。容忍一台故障就得三台容忍两台故障就得五台。这就是 2F1 公式的由来。很多刚入门的人觉得五台比三台更稳实际上五台的稳定来自两台冗余而不是多两台机器所以概率上更抗造。规划时根据业务的重要程度选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贴出来然后逐行解释tickTime2000 initLimit10 syncLimit5 dataDir/data/zookeeper/data dataLogDir/data/zookeeper/logs clientPort2181 maxClientCnxns60 autopurge.snapRetainCount3 autopurge.purgeInterval12 server.1zk-node-01:2888:3888 server.2zk-node-02:2888:3888 server.3zk-node-03:2888:3888这些参数逐个讲一遍tickTime2000。基础时间单元单位毫秒。ZooKeeper里很多超时都是它的整数倍比如下面两个Limit。initLimit10。Follower启动时连接Leader并完成数据同步的最大时间10个tick等于20秒。如果数据中心内网不太稳这个值要适当调大否则新节点会一直卡在CONNECTING状态。syncLimit5。Follower和Leader之间心跳超时上限5个tick等于10秒。设太短网络抖动会被误判为节点故障导致重选设太长故障感知变慢。建议根据机房内网的实际ping延迟调整。dataDir和dataLogDir。快照目录和事务日志目录记得分开。clientPort2181。客户端连接端口应用配置里的连接串都指向它。maxClientCnxns60。单个IP能建立的连接数上限。如果上层有大量客户端集中部署在少数机器上这个值按需调大默认60容易触发Too many connections。autopurge.snapRetainCount3。保留最近3份快照其余旧文件自动清理。autopurge.purgeInterval12。清理周期单位小时。server.Xhost: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.cfgmyid在各节点手动创建。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.whitelistruok,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:MaxGCPauseMillis200设置堆内存固定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 nameha.zookeeper.quorum/name valuezk-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 HAZooKeeper集群就是整个大数据底座最底层的依赖它的一举一动直接决定上层全链路的稳定性。5.2 会话超时与会话重连被忽略的客户端参数以Java客户端为例ZooKeeper允许配置sessionTimeout但很多新人不清楚的是这个超时时间不是客户端单方面说了算而是客户端和服务端协商后的结果。服务端有minSessionTimeout和maxSessionTimeout两个参数默认分别是2个tick和20个tick也就是4秒到40秒。客户端把sessionTimeout设成60秒服务端会强制截断到40秒设成1秒也会被拉高到4秒。所以只看客户端配置容易产生错觉。如果需要更灵活的会话超时范围建议在zoo.cfg里显式调整minSessionTimeout4000 maxSessionTimeout60000改完重启节点才生效。另一个实战经验是客户端会话重连策略。我在项目里常用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参数、监控脚本全部落到团队知识库。搭建过程可能只花两个小时但在后来某个故障夜晚你翻着文档快速定位到问题的那十分钟就是这些记录最好的回报。