1. 为什么Storm离不开ZooKeeper——分布式协调的底层逻辑
1.1 先搞清楚ZooKeeper在分布式系统里到底管什么
很多刚接触Storm的人,第一反应是把ZooKeeper当成一个“配置中心”或者“注册中心”,这个理解不能说错,但太片面了。ZooKeeper在分布式系统里扮演的角色,更像是一个“分布式协调内核”,它提供的是分布式环境下多节点之间的一致性共识能力。
你可以把它理解成班级里的“班长记录本”。班里几十个同学(节点)各自干活,但谁在做什么、谁掉队了、谁临时接手别人的任务,这些信息必须有一个大家公认的地方来记录和同步。ZooKeeper就是那个记录本,它的核心能力是:让所有节点对同一个状态变化,在同一个时间点上达成一致的认知。
这个能力在单机环境下毫无意义,但在分布式环境里是刚需。Storm如果想跨多台机器协同工作,就必须解决以下几个问题:
- 谁是这个集群的“领导”(Nimbus Leader),大家听谁的?
- 哪些Worker节点还活着,哪些已经挂了?
- 当前提交的拓扑任务,分配给哪台机器执行?
- 某个节点心跳超时了,接下来怎么处理?
这些问题没有统一答案,每个节点各说各话,集群就会立刻乱套。ZooKeeper就是用来给这些问题提供“唯一标准答案”的设施。
1.2 Storm里哪些核心机制离不开ZooKeeper
Storm在架构上分为Nimbus(调度控制节点)、Supervisor(工作节点)、Worker(实际执行任务的进程)三层。这三层之间的协调,几乎全部依赖ZooKeeper。
先说集群元数据存储。Storm把拓扑的运行时状态、任务分配信息、Executor的心跳信息,都存放在ZooKeeper的znode节点里。这样任何一个Nimbus宕机了,换上新的Nimbus,只要还能连上同一个ZooKeeper集群,就能读取之前的全部状态,继续调度任务。
其次是Leader选举。Storm从0.10.0版本开始支持Nimbus高可用,也就是同时部署多个Nimbus节点。但同一时刻只有一个Nimbus是Active状态,负责接收任务分配和调度,其他Nimbus处于Standby状态。这个“谁是Leader”的决策,就是通过ZooKeeper的临时节点和顺序节点配合完成的。具体机制后面在实战部分细讲。
第三是心跳与故障检测。每个Supervisor节点会定期向ZooKeeper写入自己的心跳信息,说明“我还活着”。Nimbus通过检查这些心跳节点的存活时间,判断哪些Supervisor已经失联,进而决定是否重新分配任务。这一套机制不依赖任何中心化的“监控服务”,完全是靠ZooKeeper的临时节点特性实现的——节点进程挂了,临时节点自动消失,所有观察者立刻感知。
第四是任务分配信息的存储与同步。拓扑提交后,Nimbus会计算出一份“谁在哪个机器上跑哪个任务的分配方案”,这个方案存放在ZooKeeper里。所有Supervisor监听这个分配节点的变化,一旦发现有新的任务分配给自己,就拉起对应的Worker进程执行。
所以你看,Storm对ZooKeeper的依赖是全方位的。它不是“可选组件”,而是整个集群协调工作的核心。
提示:ZooKeeper不是用来传输流式业务数据的。Storm的流式数据(Spout发射、Bolt处理的那些tuple)是直接通过Netty在Worker之间传输的,根本不经过ZooKeeper。很多初学者误以为消息数据流经ZooKeeper,导致对性能产生误解——实际上ZooKeeper承载的是控制面和协调面流量,数据量很小,但对实时性要求很高。
2. 环境准备与版本选型——少踩版本坑的实战建议
2.1 版本兼容性有多重要
我见过太多人栽在版本兼容性上。Storm、ZooKeeper、Java三个版本排列组合,稍有不慎就出现各种诡异问题。比如Thrift版本冲突、ZooKeeper客户端协议不匹配、Java序列化异常等等。
以我实际使用比较多的两个版本组合为例做个参考:
| Storm版本 | 推荐ZooKeeper版本 | 推荐Java版本 | 说明 |
|---|---|---|---|
| Storm 1.2.x | ZooKeeper 3.4.x | Java 8 | 最经典稳定组合,生产环境大量验证 |
| Storm 2.2.x | ZooKeeper 3.6.x | Java 8/11 | 新版特性多,对ZooKeeper的依赖结构有调整 |
| Storm 2.4.x | ZooKeeper 3.7.x | Java 11 | 最新版,适合新项目 |
这里有个大原则:优先使用ZooKeeper 3.4.x系列,除非你有必须使用新版的理由。为什么?因为Storm框架内置的ZooKeeper客户端库,长期基于3.4.x协议开发测试,兼容性最成熟。我并不是说新版ZooKeeper不能配合Storm用,而是说如果你追求稳定,老牌组合最保险。
另外注意,ZooKeeper 3.5.0之后引入了动态重新配置等新特性,但同时也修改了一些默认参数(比如默认端口并没有变,但配置项名称有调整),这些变化在集成Storm时可能引发不必要的麻烦。
2.2 ZooKeeper集群搭建——基础却关键的步骤
ZooKeeper的部署模式分为单机版、伪集群版、集群版。生产环境至少部署3台,因为ZooKeeper需要过半选举(后面细说)。这里给出3台集群的搭建核心步骤。
第一步,下载解压并创建数据目录和myid文件:
# 假设三台机器分别为zk01/zk02/zk03 wget https://archive.apache.org/dist/zookeeper/zookeeper-3.4.14/zookeeper-3.4.14.tar.gz tar -zxvf zookeeper-3.4.14.tar.gz -C /opt/ cd /opt/zookeeper-3.4.14 mkdir -p /data/zookeeper # 每台机器写入不同的myid,zk01写1,zk02写2,zk03写3 echo "1" > /data/zookeeper/myid第二步,修改conf/zoo.cfg:
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper clientPort=2181 server.1=zk01:2888:3888 server.2=zk02:2888:3888 server.3=zk03:2888:3888这里的参数看着简单,但每个都有说道:
- tickTime:ZooKeeper的基本时间单元,单位毫秒。心跳、超时的时间计算都基于它。2000毫秒是默认值,一般不需要改。
- initLimit:Follower节点启动时,与Leader节点完成同步的最大时间(以tickTime为单位)。设置为10,表示最多等待10×2000=20秒。
- syncLimit:Follower与Leader之间心跳通信的最大延迟时间。设置为5,表示最多5×2000=10秒。如果超过这个时间Leader还没收到Follower的心跳,就会判定该Follower失效。
- server.X:X是集群节点编号,必须与myid文件里的数字一致。后面的两个端口,第一个(2888)用于Leader与Follower之间的通信,第二个(3888)用于Leader选举投票。
第三步,启动集群。三台机器都要启动ZooKeeper服务:
/opt/zookeeper-3.4.14/bin/zkServer.sh start验证集群状态,这个命令非常重要,必须熟练使用:
/opt/zookeeper-3.4.14/bin/zkServer.sh status正常情况下,三台机器中会有一台显示Mode: Leader,另外两台显示Mode: Follower。如果你看到三台全是Standalone,说明它们互相没连上,检查防火墙和server.X配置的hostname是否解析正确。
2.3 为什么推荐奇数节点——过半选举机制
ZooKeeper集群的选举机制要求超过半数的节点存活才能对外提供服务。这个设计是为了避免脑裂问题——即网络分区导致集群分裂成两个各自决策的小团体。
3台机器的集群,允许挂1台;5台机器的集群,允许挂2台。挂掉太多节点,整个集群就停止服务了。为什么不是“多数通过”只要达到一半就行?因为过半和一半有本质区别,比如2台集群,如果各自占一票,出现网络分区时两台机器都会认为自己是Leader,形成脑裂。而3台集群,必须得到至少2台投票才能当选Leader,即使一台失联,剩下2台能达成一致,继续对外工作。
所以在规划时,优先选择3、5、7这样的奇数节点数量。Storm的元数据是核心中的核心,我这里建议至少3台ZooKeeper,ZooKeeper不要和Nimbus放在同一台机器上,以免单点故障时,Nimbus和ZooKeeper同时不可用,整个集群就彻底停摆了。
3. Storm与ZooKeeper的关键配置——storm.yaml逐项拆解
3.1 配置文件核心参数
Storm的配置文件是conf/storm.yaml,所有与ZooKeeper相关的配置都集中在这里。下面是一份我在生产环境常用的配置片段,逐行解释每个参数的含义:
########### Storm ZooKeeper 相关配置 ########### storm.zookeeper.servers: - "zk01" - "zk02" - "zk03" storm.zookeeper.port: 2181 storm.zookeeper.root: "/storm" storm.zookeeper.session.timeout: 20000 storm.zookeeper.connection.timeout: 15000 storm.zookeeper.retry.times: 5 storm.zookeeper.retry.interval: 1000 storm.zookeeper.retry.intervalceiling: 30000 nimbus.seeds: ["nimbus01", "nimbus02"]storm.zookeeper.servers:ZooKeeper集群的地址列表。这里填主机名或者IP都可以,但建议用主机名并配置好/etc/hosts解析,避免后续扩容或迁移时IP变动导致大面积配置修改。
storm.zookeeper.port:ZooKeeper的客户端端口。默认2181,一般不用改,除非你部署ZooKeeper时自定义了端口。
storm.zookeeper.root:Storm在ZooKeeper上的根节点路径。这个参数很多人忽略,但它非常重要。如果多个Storm集群共享同一个ZooKeeper集群,必须通过设置不同的root来隔离数据,否则集群之间会互相覆盖元数据。默认值就是"/storm",生产环境建议带上集群标识,比如"/storm-prod"、"/storm-test"。
storm.zookeeper.session.timeout:Storm与ZooKeeper之间的会话超时时间,单位毫秒。这个参数的设置很有讲究。设置太短(比如5秒),网络稍微抖动一下,ZooKeeper就会认为Storm节点挂了,触发任务重新分配,导致大量无谓的迁移开销;设置太长(比如60秒),节点真正宕机后,ZooKeeper要等很久才能感知,故障恢复时间被拉长。我一般建议设置在15秒到30秒之间,网络环境好的内网集群用20秒比较均衡。
storm.zookeeper.connection.timeout:连接ZooKeeper的超时时间。这个值需要比session.timeout小,否则还没建立连接,会话已经超时了。
storm.zookeeper.retry.times:连接ZooKeeper失败后的重试次数。默认5次,一般够用。如果你遇到过“Connection loss”之类的异常,可以适当加大这个值,比如8次。
storm.zookeeper.retry.interval:重试间隔,单位毫秒。默认1000毫秒。这里的逻辑是:如果连接ZooKeeper失败,Storm会每隔1秒重试一次,最多重试5次。如果5次都失败,就会抛出异常。
storm.zookeeper.retry.intervalceiling:这个参数容易被忽略,其实也很关键。它的意思是重试间隔的上限。ZooKeeper客户端有个指数退避策略,重试间隔会逐渐加大,但最大不超过这个值。默认30000毫秒,一般不用特殊调整。
nimbus.seeds:Nimbus节点地址列表。这是一个高可用相关的配置,在ZooKeeper协调机制中扮演关键角色(后面Leader选举部分会讲)。注意在低版本Storm里,这个配置项叫nimbus.host,只支持单Nimbus;从高版本开始才改名为nimbus.seeds,支持多Nimbus。
3.2 集群模式与本地模式的ZooKeeper区别
这里专门说一下Storm的两种运行模式,因为很多新手在这个问题上迷糊。
本地模式(Local Mode):Storm会在JVM内启动一个Embedded ZooKeeper实例,你用不着自己部署ZooKeeper集群。适合开发调试、单元测试。但要注意,本地模式的ZooKeeper是临时性的,只要进程退出数据就没了,千万别拿它跑正式任务。
集群模式(Cluster Mode):Nimbus和Supervisor都作为独立进程运行,需要外部ZooKeeper集群。提交拓扑时,客户端会连接ZooKeeper集群,把拓扑的JAR包和相关配置上传到Nimbus节点,Nimbus再把任务分配方案写入ZooKeeper。
判断一个Storm进程跑在什么模式,有个简单的特征:如果Log里出现“Starting ZK Server”,说明是本地模式;如果出现“Connecting to ZooKeeper at zk01:2181”之类的日志,说明是集群模式。
3.3 配置验证——启动前必做的检查步骤
配置完成后,不要急着启动Storm,先做几个验证:
第一步,确认ZooKeeper集群本身健康:
/opt/zookeeper-3.4.14/bin/zkCli.sh -server zk01:2181 # 进入ZooKeeper客户端后执行 ls / # 正常情况下会看到至少包含zookeeper节点第二步,确认Storm节点能连通ZooKeeper端口:
telnet zk01 2181 # 如果能通,会显示Connected to zk01第三步,启动Nimbus后,立刻检查ZooKeeper上是否出现了Storm目录:
# 在ZooKeeper客户端里执行 ls /storm # 正常情况下能看到一堆子节点,比如assignments、supervisors、topologies等如果执行ls /storm提示Node does not exist,说明Nimbus还没成功连接上ZooKeeper,先去查Nimbus日志,基本都是配置问题或网络不通。
4. 从提交拓扑到任务调度——ZooKeeper在背后的完整工作流程
4.1 提交一个拓扑,后台到底发生了什么
这一节我们完整串一遍:当你执行storm jar mytopology.jar com.example.MyTopology时,系统内部到底发生了什么。搞懂了这条链路,你对Storm和ZooKeeper集成的理解会直接上一个台阶。
第一步,客户端连接Nimbus并上传JAR。Storm客户端通过Thrift协议连接Nimbus,将拓扑的代码运行所需JAR包上传到Nimbus的本地目录(默认在/nimbus/inbox/)。
第二步,Nimbus将拓扑信息写入ZooKeeper。Nimbus把拓扑的结构信息(Spout/Bolt的定义、并行度参数等)转换成Storm内部的数据结构,写入ZooKeeper的/storm/topologies/{topology-id}节点。这个节点保存了拓扑的“静态定义”,是集群任务调度的依据。
第三步,Nimbus计算任务分配方案。Nimbus根据拓扑的并行度参数和当前存活的Supervisor列表,计算出一份“哪个Supervisor的哪个端口运行哪个任务的哪个Executor”的分配方案,写入/storm/assignments/{topology-id}节点。
第四步,Supervisor监听并执行。每个Supervisor进程都在ZooKeeper的/storm/assignments目录上注册了Watcher监听器。一旦发现自己的hostname出现在新的分配方案中,就会根据分配的端口启动对应的Worker进程(JVM)。
第五步,Worker启动后,通过Netty建立通信。Worker进程之间通过Netty建立直接的TCP连接,传输tuple数据。这一步完全不经过ZooKeeper,ZooKeeper只负责“告诉Worker们彼此该连接谁”。
4.2 心跳维持与Supervisor的存活管理
每个Supervisor节点启动后,会向ZooKeeper的/storm/supervisors/{supervisor-id}节点写入一条心跳记录,内容包含当前时间戳、主机名、可用端口数、总CPU/内存信息等。心跳是自动周期的,通过supervisor.heartbeat.frequency.secs参数控制,默认5秒写一次。
Nimbus会周期性地检查所有Supervisor的心跳节点,这个检查周期由nimbus.monitor.freq.secs参数控制,默认10秒。如果某个Supervisor有nimbus.supervisor.timeout.secs(默认60秒)时间没有更新心跳,Nimbus就会判定该Supervisor已失效,把它从可用节点列表里移除,同时重新分配原本运行在这台机器上的任务。
这里的核心机制是ZooKeeper的临时节点(Ephemeral Node)特性。Supervisor进程在ZooKeeper上创建的会话如果断开(进程崩溃或者网络失联),ZooKeeper会自动删除该会话创建的所有临时节点。所以即使Supervisor进程异常终止,来不及写任何“我要下线”的标记,ZooKeeper也能感知到,并通知Nimbus。
4.3 Nimbus的高可用与Leader选举机制
刚才提到nimbus.seeds可以配置多个Nimbus节点。Storm高可用模式下,多台Nimbus之间的调度逻辑,正是通过ZooKeeper的临时顺序节点实现的。
具体过程是这样的:
- 每台Nimbus进程启动时,尝试在ZooKeeper的
/storm/nimbus目录下创建一个临时顺序节点,如/storm/nimbus/1、/storm/nimbus/2。 - 创建成功后,检查自己创建的节点序号是不是当前最小的。如果是最小,则声明自己是Leader,对外提供调度服务;如果不是最小,设置Watcher监听比自己序号小的最后一个节点。
- 如果当前Leader(最小序号节点)的会话超时,对应的临时节点自动消失,编号最小的Standby Nimbus会收到监听事件通知,重新检查序号并接管Leader职责。
这套机制的本质就是ZooKeeper经典的“Fair Lock”选举模式,在HBase、Kafka等分布式系统里也是一样的套路。核心价值在于:所有Nimbus节点共享同一个“裁决机构”ZooKeeper,不需要额外部署独立的选举服务。
这里有一个实操经验:在配置nimbus.seeds时,各Nimbus节点都要把列表配全,而且要包含自己。比如有三台Nimbus(nimbus01、nimbus02、nimbus03),那么三台机器的storm.yaml里nimbus.seeds都要写成["nimbus01","nimbus02","nimbus03"],而不是只写别人不写自己。
4.4 Supervisor重启后如何恢复任务
再讲一个实战中一定会遇到的情景:某台Supervisor机器需要运维重启,重启完成后它是怎么恢复任务的?
Supervisor重启后,会重新连接ZooKeeper,读取/storm/assignments下所有拓扑的分配信息。如果发现某个拓扑的部分任务分配给了自己,Supervisor会自动拉起对应的Worker进程,不需要用户重新提交拓扑。这就是Storm“故障自动恢复”的基础能力,完全依赖ZooKeeper保存的分配快照。
但这里有个前提:拓扑元数据没有丢失。如果在Supervisor重启期间,Nimbus已经把分配给这台机器的任务重新安排给了其他节点,那么Supervisor恢复后读取到的是“旧分配方案”,Nimbus会根据心跳时间戳判断冲突,把旧Worker清理掉,保留新分配方案。整个过程也是自动化完成。
5. 实操过程:部署一套Storm+ZooKeeper集群并跑通拓扑
5.1 集群规划参考
以一套小型生产集群为例,我给出一个经过验证的部署规划:
| 角色 | 节点 | 配置建议 |
|---|---|---|
| ZooKeeper | zk01/zk02/zk03 | 4核8GB,独立部署,不混部 |
| Nimbus | nimbus01/nimbus02 | 8核16GB,磁盘≥200GB |
| Supervisor | sup01/sup02/sup03 | 16核32GB,磁盘≥500GB |
| Storm客户端 | client01 | 用于提交拓扑和运维操作 |
这个规划的考量是:ZooKeeper对磁盘IO比较敏感,因为它要频繁写事务日志(write-ahead log),建议ZooKeeper节点使用SSD;Nimbus负责JAR包存储和任务调度,磁盘空间要给足;Supervisor是真正跑业务逻辑的节点,CPU和内存是重点。
5.2 部署全套步骤
假设ZooKeeper集群已经按第2节的方法部署完成,这里重点讲Storm部分的部署。
第一步,下载并解压Storm:
wget https://archive.apache.org/dist/storm/apache-storm-1.2.3/apache-storm-1.2.3.tar.gz tar -zxvf apache-storm-1.2.3.tar.gz -C /opt/ mv /opt/apache-storm-1.2.3 /opt/storm第二步,配置storm.yaml。核心配置项在前面已经列出,这里补充几个生产环境常用的非ZooKeeper配置:
storm.local.dir: "/data/storm" nimbus.supervisor.timeout.secs: 60 supervisor.slots.ports: - 6700 - 6701 - 6702 - 6703 worker.heap.memory.mb: 2048storm.local.dir:Storm本地文件存储目录,用于存放JAR包、拓扑元数据等。这个目录需要一定磁盘空间,建议单独挂载。
supervisor.slots.ports:每台Supervisor可用的Worker端口列表。每个端口对应一个Worker进程。端口个数决定了这台机器最多能同时运行多少个Worker,具体数量根据机器内存和Worker堆大小来定。
第三步,启动Nimbus和Supervisor:
# 在nimbus01上启动 /opt/storm/bin/storm nimbus & # 在sup01上启动 /opt/storm/bin/storm supervisor & # 在client01上启动UI(可选) /opt/storm/bin/storm ui &启动后观察日志和ZooKeeper状态:
# 查看ZooKeeper上的Supervisor注册信息 /opt/zookeeper-3.4.14/bin/zkCli.sh -server zk01:2181 ls /storm/supervisors如果能看到一个或多个以UUID命名的节点,说明Supervisor已经成功注册到ZooKeeper。
5.3 提交拓扑并观察ZooKeeper上的数据变化
这里用一个最简单的WordCount拓扑示例,展示提交后ZooKeeper上数据结构的变化。假设你已经有编译好的拓扑JAR包,执行提交命令:
/opt/storm/bin/storm jar mywordcount.jar com.example.WordCountTopology wordcount-topology提交成功后,立刻到ZooKeeper上查看:
# 查看所有拓扑 ls /storm/topologies # 查看某个拓扑的任务分配 ls /storm/assignments get /storm/assignments/{topology-id}get命令的输出是一长串二进制序列化数据,不用纠结每个字节的含义,只要看到有内容返回,就说明Nimbus已经把拓扑和分配信息写进了ZooKeeper。
接下来验证任务是否真的跑起来:
# 在Supervisor机器上查看Worker进程 ps -ef | grep worker如果能看到“storm.worker”相关的Java进程,说明Supervisor通过ZooKeeper感知到了任务分配,已经启动Worker执行。
5.4 验证ZooKeeper在故障转移中的角色
最后做个故障演练,亲眼看看ZooKeeper的协调能力。
在运行过程中直接kill掉一个Supervisor的Worker进程:
# 找一个Worker的PID,kill掉 kill -9 {worker-pid}观察现象:
- 几秒后Nimbus通过ZooKeeper的心跳检测感知到Worker失效。
- Nimbus重新计算分配方案,将失效Worker的任务分配到同一台Supervisor的其他可用端口,或者分配到其他存活的Supervisor,具体取决于可用资源。
- 新Worker启动后,数据流从上游节点继续传输,拓扑整体不会停止。
这个过程中,你可以通过Storm UI界面观察拓扑的Executors数量变化——短暂的抖动后,Executor会恢复到配置的并行度。
注意:kill -9 Worker进程属于模拟故障的破坏性操作,只在测试环境做。生产环境不要随便这么干,正确做法是通过
storm kill {topology-name}优雅停止拓扑,或者通过重新平衡(rebalance)调整并行度。
6. 常见问题与排查技巧实录
6.1 典型问题速查表
根据我这些年维护Storm集群的实际经历,把集成ZooKeeper过程中最常见的问题整理成一个速查表:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Nimbus启动后立刻退出 | storm.zookeeper.servers配置错误或端口不通 | 检查ZooKeeper集群状态,用telnet验证端口连通性 |
| Supervisor注册不上 | myid配置冲突或ZooKeeper节点目录残留 | 清空ZooKeeper的/storm目录,重新启动Supervisor |
| 频繁出现Connection loss异常 | 网络不稳定或session.timeout设置过短 | 适当调大session.timeout到20000-30000 |
| 提交拓扑报“Failed to assign” | Nimbus无法连接ZooKeeper,或ZooKeeper磁盘已满 | 检查ZooKeeper磁盘空间,清理事务日志 |
| Worker进程反复崩溃重启 | Worker堆内存配置过小,或集群资源不足 | 调大worker.heap.memory.mb,确认Supervisor可用端口数 |
| UI界面显示Supervisor离线 | Supervisor心跳超时,或Supervisor进程已挂 | 检查Supervisor进程,确认网络连通性 |
| 重启Nimbus后拓扑状态丢失 | 拓扑元数据在ZooKeeper里被误删 | 重新提交拓扑,确认根节点路径未冲突 |
6.2 排查思路与方法论
排查Storm与ZooKeeper的问题,我的经验是遵循“从底层到上层”的顺序:
第一步,确认ZooKeeper集群本身状态。运行zkServer.sh status检查Leader选举是否正常,运行zkCli.sh ls /确认ZooKeeper能正常响应客户端请求。如果ZooKeeper本身挂了或者处于只读模式,再往下排查毫无意义。
第二步,检查Storm进程与ZooKeeper的连接。在Storm的Nimbus或Supervisor日志目录下找类似worker.log、nimbus.log的文件。搜索关键字“ZooKeeper”或“Connecting”,看有没有异常堆栈。
第三步,检查ZooKeeper上的数据节点是否正常。这是很多人忽略的。通过zkCli.sh进入ZooKeeper,逐层查看/storm目录下的节点结构。如果supervisors目录为空,说明Supervisor注册失败;如果assignments目录为空,说明任务分配还没发生。
另一个很重要的排查工具是开启ZooKeeper的详细日志。在ZooKeeper的conf/log4j.properties里把zookeeper的日志级别调整为DEBUG,可以看到包括连接建立、会话创建、Watcher触发在内的所有细节。生产环境注意排查完成后调回原级别,避免日志量过大。
6.3 一些独家的避坑技巧
分享几个常规文档里不会写、但实战价值极高的经验:
技巧一:定期清理ZooKeeper的/storm目录残留数据。Storm集群长期运行后,如果频繁提交和删除拓扑,ZooKeeper的/storm目录下会积累大量历史节点。虽然ZooKeeper会自动清理不再被引用的znode,但在Tomcat过期拓扑(被kill掉的拓扑)时偶尔会有残留。残留数据多了会影响ZooKeeper性能。可以用zkCli.sh手动清理,也可以写个定时脚本清理已经被kill且ZooKeeper上不再活跃的拓扑节点。注意一定是确认拓扑已经彻底停止再清理。
技巧二:ZooKeeper的JVM堆不要盲目调大。ZooKeeper的数据模型是纯内存的,所有znode都常驻内存。但ZooKeeper节点数通常不会太多,真正吃内存的是连接会话管理和Watcher机制。默认的1GB堆一般够用,盲目调到4GB反而会导致GC停顿拉长,影响会话心跳的及时性。
技巧三:观察/storm/nimbus目录下的选举信息。如果配置了多Nimbus高可用,可以通过检查get /storm/nimbus里的内容,确认当前哪个节点是Active Leader。这是排查Nimbus高可用问题最直接的入口。
技巧四:用四字命令快速检查ZooKeeper运行状态。在命令行执行:
echo mntr | nc zk01 2181输出里如果有zk_server_state leader,说明这台是Leader。另外还有ruok(检查健康)、stat(查看连接数等统计)、wchs(查看Watcher数量)等四字命令,排查时非常高效。
注意:ZooKeeper的四字命令默认是开启的,但可以通过配置
4lw.commands.whitelist来限定允许执行的命令列表,这是安全加固的常用做法。生产环境建议只保留mntr、ruok、stat这几个。
7. 写在后面:我对分布式协调的一些体会
做分布式系统这几年,最深刻的一个体会是:真正复杂的从来不是业务逻辑,而是多节点之间的状态一致性。Storm和ZooKeeper的集成,表面上看只是配置几个IP地址和端口,但真正理解它背后那套“临时节点、顺序节点、Watcher通知、Leader选举”的协调机制之后,你会发现这些底层模式在几乎所有的分布式系统里都是相通的。
我遇过不少人觉得ZooKeeper笨重,总觉得是不是可以自己搞一个简单的数据库表来替代。每次我都会反问:你怎么保证多台机器同时读写同一张表时,不会出现脏读和覆盖?你怎么保证一台机器挂掉后,其他机器能在几秒内感知并接管?这些问题自己实现一遍,你会发现ZooKeeper的每一层设计都不是多余的。
最后分享一个小技巧:日常运维时,我习惯在客户端机器上写一个简单的辅助脚本,封装常用的ZooKeeper检查命令,包括集群状态检查、节点目录查看、连接数统计等。每次排查问题时,一条脚本跑下来,10秒钟就能定位是ZooKeeper的问题还是Storm的问题,省下大量手动敲命令的时间。这套方法不仅在Storm项目里管用,在维护其他依赖ZooKeeper的Kafka、HBase集群时,一样直接复用。