
最近在赫兹威客平台接了个ZooKeeper相关的测试任务需要在本地快速搭一套伪分布式ZooKeeper环境。这套环境要能模拟真实集群的选举、数据同步和故障转移又不能用多台服务器去折腾。折腾了两个小时后我发现网上很多教程只给了一堆配置参数却没有说明每一步为什么这样做遇到报错也不知道去哪里查。这篇教程就把我实际的搭建过程、配置思路、验证方法和踩过的坑完整还原一遍给刚入门ZooKeeper、准备测试Hadoop分布式组件、以及想低成本验证集群方案的朋友作个参考。伪分布式ZooKeeper简单说就是在同一台机器上启动多个ZooKeeper进程用不同端口和数据目录让它们“假装”是不同节点。它解决的问题很明确没有那么多机器、不想用虚拟机、只是要验证配置和功能。需要提醒的是伪分布式能模拟分布式协调的大部分行为但不能完全替代多机部署这个边界我后面会专门讲。文中的配置以Linux环境为例ZooKeeper版本用当前稳定版3.7.x。整个流程会包含为什么需要伪分布式、环境准备、核心配置、启动验证、问题排查最后补一段和Hadoop整合的实战思路。1. 为什么要做伪分布式ZooKeeper测试1.1 单机模拟集群的核心价值ZooKeeper在分布式系统里的地位可以理解成整个大楼的物业中心谁当领导、哪个服务挂了、配置项有哪些变化都得经过它来协调。生产环境里ZooKeeper集群通常至少三台机器而且必须是奇数节点因为选主需要“多数派”点头。但对大多数开发者和学习者来说手头可能只有一台电脑或者一台低配云主机总不能为了跑个demo就去开三台服务器。伪分布式的价值就在这里。它仍然是一个真正的ZooKeeper集群有leader、有follower、有数据同步、有故障转移只是这些角色都跑在同一台物理机的不同进程里。通过不同的客户端端口、不同的数据目录和不同的内部通信端口让三个进程互相认为对方是独立节点。这样一来你可以在一台机器上完整体验集群的启动过程、选举机制和读写行为成本几乎为零。我最初接触伪分布式是在学习HDFS高可用的时候。当时需要验证HA配置是否正确但是实验室只有一台可以随便折腾的机器于是就用伪分布式ZooKeeper加伪分布式Hadoop把整个高可用流程跑通了。那种感觉就像在一个人住的三居室里同时扮演物业经理、保安和维修工所有岗位都存在只是都在一个屋檐下。虽然有点“自导自演”的意思但该走的流程一步不少。1.2 哪些场景建议先用伪分布式根据我这些年测试分布式组件的经验伪分布式特别适合下面这几类场景。第一类是学习ZooKeeper本身。想搞懂节点类型、临时节点和持久节点的区别、Watch机制怎么触发直接用伪分布式集群练习比看文档高效得多。第二类是验证Hadoop高可用配置。HDFS NameNode的自动故障转移、YARN ResourceManager的高可用都依赖ZooKeeper提供协调服务。第三类是CI/CD流水线里的集成测试。测试环境往往资源有限用伪分布式集群跑一遍分布式场景比每次都申请独立测试集群要快。第四类是演示和课程设计。需要向别人展示ZooKeeper集群怎么工作又不想被网络环境限制伪分布式是最稳妥的选择。不过要清醒一点伪分布式不能模拟所有真实分布式环境的问题。比如网络分区、多机之间的延迟抖动、单点硬件故障导致的消息乱序这些在伪分布式环境下很难复现。另外伪分布式把多个进程压在同一台机器上内存和CPU是共享的生产级别的压力测试不能这么干。所以我的建议是学习和功能验证用伪分布式上线前必须跑到多机环境做完整测试。2. 环境准备与基础配置2.1 下载与安装ZooKeeperZooKeeper本身是免费的Apache开源项目直接到Apache官网下载稳定版二进制压缩包就行。这里注意一点不要图新鲜去下载Beta或者SNAPSHOT版本分布式组件出问题很难排查稳定版能省掉很多非预期坑。当前稳定版是3.7.x系列下载文件名一般是zookeeper-3.7.1-bin.tar.gz注意要认准带bin的那个包不带bin的是源码包解压后还得自己编译。下载完成后解压到指定目录例如cd /opt tar -zxvf zookeeper-3.7.1-bin.tar.gz mv zookeeper-3.7.1-bin zookeeper-3.7.1ZooKeeper是基于Java开发的需要提前装好JDK。建议使用JDK 1.8或者JDK 11这两个版本是ZooKeeper社区测试最充分的。如果机器上已经有Java环境可以用java -version确认版本没有的话先装OpenJDK然后设置JAVA_HOME环境变量。安装完ZooKeeper后把bin目录加到PATH里也可以但我个人习惯不设置全局环境变量而是每次用绝对路径去调用脚本这样机器上同时存在多个ZooKeeper版本时不至于混乱。2.2 配置文件与基本参数说明ZooKeeper所有的核心配置都写在配置文件里。独立模式只需要一份zoo.cfg而伪分布式集群需要为每个实例准备独立的配置文件也就是每个节点一份。这一点很多教程都没有说透导致很多人把三份配置文件写成同样的内容启动后端口互相冲突全起不来。先看一份最简单的独立模式配置理解每个参数的作用tickTime2000 initLimit10 syncLimit5 dataDir/tmp/zookeeper/data clientPort2181各参数的含义可以参考这张表参数含义说明tickTime基本时间单元单位毫秒ZooKeeper内部很多超时时间都是它的整数倍initLimit初始化连接超时时间follower启动后与leader同步数据的超时值为tickTime的倍数syncLimit心跳超时时间follower与leader之间发送心跳的超时值为tickTime的倍数dataDir数据存储目录事务日志和快照文件的存放位置必须有写权限clientPort客户端连接端口应用程序通过这个端口连接ZooKeeper这里有个容易被忽略但很实用的点dataDir不能放在/tmp目录下。因为很多Linux发行版会定期清理/tmp下不活跃的文件一旦ZooKeeper的数据快照被清掉恢复会很头疼。建议单独建一个目录比如/opt/zookeeper-cluster/node1/data权限设置成当前运行用户可读写。关于clientPort每个节点的这个端口必须各不相同否则后启动的进程会报端口占用这是伪分布式配置的第一个关键点。3. 伪分布式核心配置一个机器模拟三个节点3.1 端口规划与目录规划伪分布式要模拟三个节点首先得把节点编号规划好。我在本地经常用node1、node2、node3三个目录来区分实例。每个目录下有一个独立的data目录用来放对应节点的事务日志和快照。还要有一个myid文件用来标识当前节点在整个集群中的编号。目录结构大致如下/opt/zookeeper-cluster/ ├── node1/ │ ├── data/ │ └── zoo.cfg ├── node2/ │ ├── data/ │ └── zoo.cfg └── node3/ │ ├── data/ │ └── zoo.cfg端口规划是重点。三个节点需要三套端口参数clientPort是客户端连接端口分别用2181、2182、2183。集群内部通信需要两个端口格式是server.NIP:port1:port2。port1是follower连接leader同步数据的端口port2是选举投票端口。为了好记node1用2888和3888node2用2889和3889node3用2890和3890。在3.7.x版本中还需要注意adminServerPort参数。ZooKeeper 3.5版本以后内置了一个Admin Server默认监听8080端口提供一些监控和四字命令的HTTP接口。如果三个伪分布式节点共用默认配置那么三个进程都会去抢8080端口必然有一个或两个启动失败。解决方法是把admin.serverPort改成不同的值比如8081、8082、8083或者直接设置admin.enableServerfalse关掉它。这个坑特别隐蔽因为错误日志不会直接告诉你“8080被占用”而是报一堆类似“Address already in use”的信息第一次遇到时很容易绕进去。3.2 三份配置文件参考node1的zoo.cfg配置如下tickTime2000 initLimit10 syncLimit5 dataDir/opt/zookeeper-cluster/node1/data clientPort2181 admin.serverPort8081 server.1127.0.0.1:2888:3888 server.2127.0.0.1:2889:3889 server.3127.0.0.1:2890:3890node2的zoo.cfg配置如下tickTime2000 initLimit10 syncLimit5 dataDir/opt/zookeeper-cluster/node2/data clientPort2182 admin.serverPort8082 server.1127.0.0.1:2888:3888 server.2127.0.0.1:2889:3889 server.3127.0.0.1:2890:3890node3的zoo.cfg配置如下tickTime2000 initLimit10 syncLimit5 dataDir/opt/zookeeper-cluster/node3/data clientPort2183 admin.serverPort8083 server.1127.0.0.1:2888:3888 server.2127.0.0.1:2889:3889 server.3127.0.0.1:2890:3890观察这三份配置会发现只有dataDir、clientPort、admin.serverPort不同server.N那段是完全相同的。原因很简单伪分布式集群中每一个节点的配置都需要描述整个集群有哪些节点所有节点的server列表必须保持一致这样它们才能互相识别。有人会问能不能把server.N里的IP改成localhost可以但127.0.0.1在绝大多数情况下更稳定因为localhost在某些网络配置下可能被解析成IPv6地址导致端口通信失败。3.3 myid文件与server编号的对应关系ZooKeeper通过myid文件来确认当前进程对应server.N中的哪个编号。这个文件必须放在dataDir目录下内容就是节点编号。比如node1的data目录下需要创建myid文件内容为数字1echo 1 /opt/zookeeper-cluster/node1/data/myid echo 2 /opt/zookeeper-cluster/node2/data/myid echo 3 /opt/zookeeper-cluster/node3/data/myid这里有几个非常容易踩的细节。第一myid文件的内容不要带空格不要有多余的换行有些编辑器会在文件末尾自动加换行一般没影响但如果你用了Windows的记事本编辑再传到Linux上可能带上\r字符ZooKeeper可能解析失败。第二myid的内容必须和配置里的server.N对应不能node1的配置里写server.1...但myid文件存的是2那就跟集群里另一个节点“撞ID”了。第三myid文件所在的目录必须是dataDir对应的目录不能配到其他目录去否则ZooKeeper会认为这是一个全新节点状态完全对不上。创建完目录和myid后建议用ls -l检查一下文件权限。如果dataDir目录的所有者是root而你是用普通用户启动ZooKeeper启动时会提示目录无法写入。我在第一次搭建时就是忘记改属主结果日志里报Permission denied排查了好一会儿。4. 启动与验证从单机到集群的伪装4.1 启动三个实例的完整流程启动伪分布式集群时需要分别指定三个配置文件来启动三个进程。ZooKeeper的启动脚本zkServer.sh支持把配置文件路径作为参数传入这一点很实用。假如把三个配置文件都放在各自的目录下可以用下面的命令依次启动/opt/zookeeper-3.7.1/bin/zkServer.sh start /opt/zookeeper-cluster/node1/zoo.cfg /opt/zookeeper-3.7.1/bin/zkServer.sh start /opt/zookeeper-cluster/node2/zoo.cfg /opt/zookeeper-3.7.1/bin/zkServer.sh start /opt/zookeeper-cluster/node3/zoo.cfg启动顺序没有严格要求但我的习惯是从node1开始依次启动这样日志更好排查。如果一切正常启动命令会输出类似STARTED的消息。启动后先用jps看一下Java进程正常情况下能看到三个QuorumPeerMain进程这就是三个ZooKeeper实例。如果只看到一个或者两个说明后面启动的节点可能因为端口冲突或者配置错误失败了。顺便提一句ZooKeeper 3.7.x启动脚本对配置文件路径的识别逻辑如果传的是相对路径脚本可能会到conf目录下找结果找不到文件直接报错。所以强烈建议用绝对路径一劳永逸。这个过程完成后三个进程只是各自启动了但它们之间是否成功组成集群还要看状态输出。4.2 状态检查与Leader选举验证启动完成后检查集群状态最直接的方式是用zkServer.sh status同样需要指定配置文件/opt/zookeeper-3.7.1/bin/zkServer.sh status /opt/zookeeper-cluster/node1/zoo.cfg如果集群已经正常选举出leader输出中会出现Mode: follower或者Mode: leader。ZooKeeper集群没有固定的leader启动过程中所有节点先处于LOOKING状态然后通过选举算法投票。3.7.x版本默认的选举实现是Fast Leader Election所有节点把票投给自己再根据节点编号等信息相互协商最终超过半数的节点达成一致后leader就产生了。这里就体现出奇数节点的重要性了。三节点集群中一个节点获得两票就能成为leader即使有一个节点挂了剩下的两个节点仍然能选出leader集群可以继续提供服务。如果只有两个节点其中任何一个宕机剩下的节点无法形成多数派整个集群就会卡在LOOKING状态出现“服务不可用”的情况。这就是为什么生产上不推荐部署两节点ZooKeeper。在伪分布式测试中你可以尝试停掉一个节点来模拟这种场景观察剩余节点是否还能正常工作这也是判断集群是否“真”集群的最好办法。4.3 用zkCli.sh验证读写与故障转移状态检查只能说明进程活着且角色正常要验证数据同步是否生效还得靠客户端实际读写。ZooKeeper自带的zkCli.sh客户端工具可以完成这件事。连接node1/opt/zookeeper-3.7.1/bin/zkCli.sh -server 127.0.0.1:2181连接成功后执行创建节点操作create /test hello再连接node2的端口2182执行get /test如果能读出hello这个值说明node1创建的数据已经同步到了node2。这就是ZooKeeper数据一致性的基本表现。接下来可以验证故障转移。先找到当前leader是哪个节点用status命令逐个查看然后停掉leader节点/opt/zookeeper-3.7.1/bin/zkServer.sh stop /opt/zookeeper-cluster/node2/zoo.cfg再查看剩下的节点正常情况下剩下的两个节点中会有一个变成leader同时客户端连接其他端口仍然可以读写数据。我在本地测试时经常开着三四个终端窗口一个连2181一个连2182一个连2183然后停掉其中一个节点观察客户端会话是否受影响。只要配置没有问题数据读写基本不受影响只是短暂会有几百毫秒的阻塞因为要重新选举leader。如果等了很久还是不可写就要检查是不是出现了“脑裂”或者节点数不够形成多数派。5. 实操中常见问题与排查技巧5.1 端口冲突与地址占用端口冲突是伪分布式搭建中最常见的坑。症状也很典型某个节点启动时输出一大堆异常日志里面包含Address already in use或Socket bind failed。排查思路很简单先用命令确认端口被谁占用netstat -tlnp | grep 2182如果看到某个进程占用2182端口要么是之前残留的ZooKeeper进程没被杀掉要么是其他服务占用了这个端口。我之前遇到过一次三个节点启动后第一个节点正常第二个节点报了端口占用原因是node1和node2的zoo.cfg里clientPort都写成了2181典型的复制粘贴之后忘改。解决办法就是逐一核对三份配置文件的clientPort、admin.serverPort、server.N端口是否互不相同。建议在启动之前先写一个简单的端口规划表像这样节点clientPort数据同步端口选举端口adminServerPortnode12181288838888081node22182288938898082node32183289038908083每次改配置前对照这张表检查一次能避免大部分低级错误。另外如果系统里还有其他基于Java的服务占用了端口也会导致冲突所以最好在搭建前把端口占用情况摸清楚。5.2 启动失败与日志排查ZooKeeper的日志信息是排查问题的第一手资料。默认情况下日志写在启动脚本所在目录的logs子目录下文件名类似zookeeper- -server- .out。也可能在zookeeper.out或nohup.out里。如果启动失败不要只盯着控制台输出要打开日志文件看完整的堆栈信息。以下几种报错是伪分布式搭建过程中出现频率较高的报错线索常见原因解决思路Unable to load databasedataDir下已有损坏快照或事务日志或目录权限不足确认目录权限测试环境可备份后清空data目录的version-2子目录Connection refused某个server端口无法连接通常是IP配置错误或端口被防火墙拦截检查server.N里的IP是否为127.0.0.1检查防火墙FileNotFoundExceptionmyid文件不存在或路径不对在对应dataDir下创建myid文件Address already in use端口冲突按5.1的方式排查端口占用日志里最让人困惑的是Unable to load database。这个一般是dataDir下面残留了不完整的快照数据。解决办法是把data目录备份后清空让ZooKeeper重新初始化。但要记住这只适用于测试环境生产环境直接删数据是严重事故。另外还有一个排查技巧如果三个节点都启动了但status一直显示Mode: standalone而不是leader/follower说明三个节点并没有组成集群。最常见的原因是server.N配置里的IP写成了自己的机器名但机器名解析不到127.0.0.1节点之间互相找不到。全部改成127.0.0.1就能解决。5.3 myid与目录配置不一致的坑myid这个文件不大但出问题的时候很隐蔽。有一次我遇到一个场景三个节点都能启动但死活选不出leader日志里一直循环广播投票看起来就像是网络问题。排查到最后才发现node2的dataDir配置成了node1的data目录然后myid文件也是复制过来的1导致集群里出现了两个node1node3成了node2ID直接错乱了。另外创建myid文件时如果用了echo命令默认会带一个换行符这个一般没问题。但如果你是在Windows环境下用记事本创建文件再上传到Linux文件末尾可能会多一个\r符号ZooKeeper读取myid时会解析失败。检查方法是用cat -A查看myid文件如果看到结尾有^M字符说明有\r需要去掉。简单处理就是直接在Linux上重新创建。还有一个容易被忽略的细节是目录权限。如果dataDir配置的是/opt/zookeeper-cluster/node1/data但这个目录的所有者是root普通用户启动ZooKeeper时会报权限不足。解决方案是使用chown把目录归属改成启动用户chown -R user:user /opt/zookeeper-cluster在伪分布式场景下三个dataDir目录都必须有写权限一个都不能少。建议搭建完成后用下面的命令把所有关键信息一次性列出来检查cat /opt/zookeeper-cluster/node1/zoo.cfg cat /opt/zookeeper-cluster/node1/data/myid ls -l /opt/zookeeper-cluster/node1/data养成这个习惯后大部分问题都能在启动前发现不用等进程报错再回头翻配置。6. 与Hadoop整合实战补充6.1 为什么Hadoop要配ZooKeeper知道了ZooKeeper伪分布式怎么搭很多人会问和Hadoop整合到底怎么用我最初搭伪分布式ZooKeeper就是为了测试Hadoop的高可用。Hadoop生态里ZooKeeper最典型的作用有两个一个是HDFS的NameNode高可用另一个是YARN的ResourceManager高可用。在HDFS高可用架构中有两个NameNode一个Active、一个Standby。Active节点负责处理客户端读写请求Standby节点同步元数据状态等待故障时接替。如果没有ZooKeeper两个NameNode怎么确定谁是Active、Standby节点什么时候切换都需要手工干预。有了ZooKeeper两个NameNode会同时向ZooKeeper注册争抢一个Active锁。谁抢到了谁就是Active。当Active节点心跳丢失时ZooKeeper会自动触发故障转移让Standby节点升为Active。这个过程就叫自动故障转移核心依赖就是ZooKeeper的临时节点和Watch机制。ResourceManager的HA原理也类似本质上就是让多个候选节点通过ZooKeeper协调避免出现多个Active同时工作导致状态不一致。理解了这层关系你就会发现ZooKeeper伪分布式集群的价值在一台机器上就能把整个HA流程演练一遍不需要真的开两台NameNode虚拟机。6.2 整合的关键配置思路在Hadoop中整合ZooKeeper关键是把ZooKeeper连接字符串配置正确。比如HDFS开启自动故障转移后hdfs-site.xml里需要配置这样几个核心参数property namedfs.ha.automatic-failover.enabled/name valuetrue/value /property property nameha.zookeeper.quorum/name value127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183/value /property这里ha.zookeeper.quorum的值就是你三份zoo.cfg里配置的clientPort把三个端口都写上用逗号分隔。Hadoop的NameNode会通过这个连接字符串访问ZooKeeper集群。注意这里不能只写一个2181因为Hadoop也需要通过ZooKeeper集群完成高可用如果某个节点挂了它还能连其他节点。当你在伪分布式环境下把HDFS HA跑通后迁移到真实集群时只需要把ha.zookeeper.quorum里的127.0.0.1换成各个节点的真实IP同时把ZooKeeper配置里的server.N对应的IP改成真实IP。ZooKeeper端的数据目录和myid保持不变但配置文件里的端口和IP需要重新规划。这也体现了伪分布式的最大价值用最低成本把分布式组件的逻辑理清楚之后上生产环境时才不会手忙脚乱。写在最后的一点实操体会我后来搭ZooKeeper伪分布式环境不会把这个过程当成“一次性操作”而是会把它沉淀成一个可重复使用的工具包。我会在/opt/zookeeper-cluster下维护好三个节点的配置文件和myid再写一个简单的启停脚本把start、status、stop三个操作封装起来。这样后续测试Hadoop HA或者别的分布式组件时直接跑脚本就能拉起一个完整的ZooKeeper集群省掉重复配置的时间。还有一个小技巧想分享伪分布式里三个节点的日志输出默认是分开的排查问题时可以给每个节点设置独立的日志路径或者用一个终端同时tail三个日志文件这样能实时看到选举和同步过程。ZooKeeper的调试信息非常详细多看看日志记录很快就能建立对集群行为的直觉。说实话能在一台机器上把ZooKeeper集群玩明白再去操作多机集群时心里会踏实很多。