做数据的人都知道,ETL管道表面上是SQL脚本、Spark任务、Kafka流在来回跑,但真正让一套ETL系统能稳定跑上几个月不翻车的,往往不是那些花哨的清洗逻辑,而是底层负责协调的“管家”。这个管家,在很多工程里就是ZooKeeper。
不少新手第一次在ETL工具的配置里看到zookeeper.connect这个参数时,会愣一下:ETL不是抽数、清洗、写数吗?关ZooKeeper什么事?其实在分布式ETL场景里,ZooKeeper负责的是那些最容易被忽视、又最容易引起大事故的事情:多个调度节点同时抢任务怎么办、某个执行Worker挂了怎么感知、同一个跑批任务会不会被重复执行、HDFS的NameNode切换时ETL任务能不能自动恢复。
这篇文章我想从自己在生产环境里维护ETL链路的角度,把ZooKeeper在大数据ETL工具里的真实角色拆开讲透。不光讲概念,还会把集群搭建、与Hadoop/调度平台整合、分布式锁的实现思路、以及我踩过的坑全部列出来。适合正在做数据平台、离线数仓、实时数仓的工程师,也适合刚接触ZooKeeper但想知道“这东西到底用在哪”的读者。
1. ETL里的ZooKeeper,究竟在解决什么问题
1.1 从单机脚本到分布式执行,多出来的三个难题
早期做ETL很简单,一台机器上写个Shell脚本,定时调度跑数,抽数、清洗、写目标库,流水线清清爽爽。但数据量一上来,单机扛不住,任务就得分散到多台机器上执行;调度系统也不能只部署一份,否则调度器一挂,整个跑批全停。
这一演变,带来了三个单机时代没有的麻烦。
第一个是“谁说了算”。多台调度节点同时在线,必须保证同一时刻只有一个节点能真正派发任务,其他节点待命。这个机制专业叫Leader选举。如果没有一个可靠的外部组件,两个节点可能同时认为自己是主节点,任务就会重复下发。
第二个是“谁能干活”。ETL任务执行器散布在不同机器上,调度节点需要动态知道哪些Worker是活的。机器宕机、网络抖动、重启服务,都会导致Worker状态变化。如果只靠配置文件维护一个静态列表,扩缩容和故障感知都跟不上。
第三个是“不能重复执行”。凌晨两点跑批高峰期,同一个数据抽取任务如果被两个Worker同时执行,上游业务库可能直接被打垮,目标表也会出现重复数据。想要保证“同一时间只有一个实例在处理某个任务”,就需要一个分布式环境下的互斥锁。
这三个问题,本质上都是分布式系统里的“协调问题”。ZooKeeper不是计算引擎,也不是存储引擎,它专门解决的就是这一类协调问题。
1.2 ZooKeeper的四个看家本领,正好对上ETL的痛点
ZooKeeper对外提供的是一个类似文件系统的树形结构,节点叫ZNode。它身上四个能力,在ETL工程中各有对应场景。
临时节点和顺序节点的组合,天然适合做Leader选举。多个节点同时去创建一个临时节点,谁创建成功谁就是Leader,其他节点监听这个节点的存在状态。
临时节点也可以直接当“存活探针”。Worker启动后注册一个临时节点,进程挂掉、网络断开、会话超时,这个节点会自动消失,不需要额外写心跳上报逻辑。
持久节点加版本号,可以实现分布式锁。多个Worker同时去创建同一个路径的节点,创建成功的人拿到锁,任务结束后删除节点释放锁。
Watcher机制则用来做配置下发和状态通知。客户端监听某个路径,节点数据一变,所有订阅者马上收到通知。ETL的清洗规则、连接池参数、任务开关,都可以用这种方式热更新,不用重启服务。
你仔细品一下就会发现,ZooKeeper解决的并不是“数据怎么计算”,而是“多个节点怎么配合才不打架”。而ETL工具一旦做成分布式架构,这个问题就绕不开。
1.3 现实中的ETL技术栈,早就在用ZooKeeper
说几个大家比较熟悉的。
如果你的ETL链路里用了Kafka,那实际上已经在依赖ZooKeeper了。Kafka的broker元数据、Topic分区信息、消费者组的offset(老版本),这些都可以存在ZooKeeper里。Kafka节点挂掉时,控制器(Controller)的切换也是靠ZooKeeper完成的。
如果你的计算引擎跑在Hadoop集群上,HDFS的NameNode高可用、YARN的ResourceManager高可用,同样依赖ZooKeeper。NameNode主节点宕机后,Standby自动升主,ETL任务读HDFS不会中断,这些都是ZooKeeper在幕后做的。
如果是用DolphinScheduler这类分布式调度平台,ZooKeeper的角色更直接。Master节点的选举、Worker节点的注册与发现、任务队列的容错分发,全部构建在ZooKeeper之上。Azkaban的Multi Executor模式也一样,多个执行器通过ZooKeeper注册,Web端动态调度。
所以你会发现一个规律:越是需要高可用、多实例、自动容错的ETL底座,越离不开ZooKeeper。它不是某一个工具的功能,而是很多大数据组件共同的“协调底座”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搞懂ZooKeeper四个关键机制,ETL集成不再踩坑
2.1 ZNode的数据模型:更像目录树上的“消息牌”,不是数据库表
很多人一开始会把ZooKeeper当成一个“KV存储”来用,这是一个很普遍的误解。它虽然可以存数据,但设计上并不适合存大量业务数据,单节点默认不能超过1MB。
我更喜欢把它理解成“目录树上的一块消息牌”。路径从根开始,比如/etl/locks/order_etl、/etl/workers/node-01、/dolphinscheduler/master。每个节点可以有一点数据,但核心价值在于节点的存在与否、路径的结构。
节点类型有几种,在ETL场景里的用途完全不同。
持久节点只要不删就一直在,适合存任务配置、黑白名单、规则版本这类不随进程状态变化的信息。临时节点和创建它的客户端会话绑定,会话结束节点就消失,适合标记“某个Worker正在运行”“某个实例持有锁”。顺序节点在创建时自动加序号,比如/etl/locks/job_000000001,适合做公平排队,先到先得。
ETL工程里最常见的一个组合是:临时节点加顺序节点实现Leader选举。多个候选人同时创建同一个路径的临时顺序节点,序号最小的那个就是Leader。Leader挂了,它的临时节点消失,下一个顺位的节点接替。
2.2 Watcher机制:它不是持续订阅,而是“一次性闹钟”
Watcher是ZooKeeper里非常有特色、也非常容易用错的一个机制。
你可以对某个节点或某个路径注册一个Watcher,当这个节点发生数据变化、删除,或者子节点列表变化时,ZooKeeper会给你推送一条通知。但这个通知是一次性的,回调完就自动失效,如果还要继续监听,必须在回调里重新注册。
在ETL场景里,这个机制可以这样用:自研的任务执行器启动后,在/etl/workers/下创建一个临时节点,然后监听这个父节点的子节点列表。如果有新的Worker上线或已有Worker下线,客户端会收到通知,调度器就能实时刷新可用的执行节点列表。
我刚开始用的时候踩过一个坑:只注册了一次Watcher,结果某个Worker宕机后,调度器完全没有感知,任务继续往失效节点上派。查了半天才发现是Watcher已经失效了,回调跑完就再也不触发。后来在回调末尾重新注册,问题才解决。
记住一点,Watcher适合做“状态变化的点火信号”,不适合做“数据内容的分发管道”。收到通知后,业务逻辑应该自己去获取最新数据,不要在回调里跑几十秒的重任务。
2.3 会话与心跳:临时节点是死是活,全靠这套机制
临时节点为什么会自动消失?关键在于Session机制。
客户端连接ZooKeeper服务端以后,就建立了一个Session。这个Session通过心跳包维持,客户端会按一定间隔发送心跳,服务端如果超过sessionTimeout还没收到心跳,就判定会话失效,把该会话创建的临时节点全部清除。
sessionTimeout这个参数,在ETL生产环境里特别值得讲究。太短,网络稍微抖一下,任务锁节点就丢了,可能导致同名任务被多个Worker同时执行;太长,Worker真正宕机时,调度系统要过很久才能感知,任务恢复变慢。
生产环境一般建议设置为20到40秒之间。如果你的机房网络经常有抖动,可以适当调大一点。但要注意,ZooKeeper服务端会对客户端设置的sessionTimeout做限制,默认范围是2倍到20倍tickTime。如果tickTime是2000毫秒,那客户端能设置的范围就是4秒到40秒,超出范围会被强制截断。
我自己在维护ETL平台时,会把核心任务的锁路径单独放在一个ZooKeeper集群上,避免其他业务的Session波动影响到锁的稳定性。
2.4 一致性模型:为什么不会出现“两个主节点都以为自己是主”
这一点是ZooKeeper能扛起协调重任的核心原因。
ZooKeeper集群内部用ZAB协议保证一致性。写请求必须提交到集群中的大多数节点(超过一半),确认成功后才算真正生效。这样做有一个直接好处:当网络发生分区时,只有包含多数节点的那一侧才能继续选主和服务,另一侧即使进程活着,也无法形成“法定人数”。
所以ZooKeeper天然具备“少数服从多数”的约束。在ETL调度场景里,这意味着:两个Master节点同时去竞选,只要它们之间网络断了,最终只可能有一个节点能拿到ZooKeeper里的锁节点,不可能出现两个节点都创建成功的情况。
我之前遇到过一种误判,以为“两个节点同时在ZooKeeper里注册了同一个Master路径”就是脑裂了。后来排查发现,是其中一个节点注册到了不同的根路径上,路径不统一导致的假象。所以在设计路径命名空间时,整个平台一定要约好根路径,不能各写各的。
3. 实操:从零搭一套带ZooKeeper的ETL协调环境
3.1 集群规划和基础参数选择
动手以前,先把集群规划清楚。
生产环境ZooKeeper至少3个节点,阿里云、自建机房都行。节点数量最好是奇数,因为选主需要过半票数,3节点容忍挂1个,5节点容忍挂2个。我见过有些团队图省事搭2个节点,结果其中一个宕机后剩下1个永远凑不够过半数,集群直接不可用,这比单节点还惨。
三个端口要提前规划好:
- 2181:客户端连接端口
- 2888:Follower节点连接Leader节点的内部通信端口
- 3888:Leader选举端口
三台机器之间要双向放通这几个端口。如果部署在公司内网,注意不要把所有ZooKeeper节点放在同一个物理机柜或者同一个交换机下面,避免电源或网络设备故障时整体挂掉。
数据目录建议单独挂一块磁盘或至少独立分区。ZooKeeper会把事务日志和快照落盘,磁盘满了会导致集群写失败,进而影响所有依赖它的ETL任务。给数据和日志分配的空间,至少预留20GB以上,具体取决于节点更新频率。
3.2 一步步搭建ZooKeeper集群
我这里用Apache ZooKeeper 3.8.x版本举例,下载解压后,进入conf目录,把zoo_sample.cfg复制为zoo.cfg。
配置项主要看这几个:
bash复制tickTime=2000
initLimit=10
syncLimit=5
dataDir=/data/zookeeper
dataLogDir=/data/zookeeper/logs
clientPort=2181
maxClientCnxns=300
autopurge.snapRetainCount=3
autopurge.purgeInterval=1
server.1=zk01:2888:3888
server.2=zk02:2888:3888
server.3=zk03:2888:3888
解释一下关键参数。tickTime是基础时间单位,单位毫秒,2000表示2秒。initLimit=10表示Follower在启动时连接Leader最多能容忍10个tickTime,也就是20秒。syncLimit=5表示Leader与Follower之间正常同步时最多容忍5个tickTime,也就是10秒。这两个参数如果网络较慢可以适度调大,但不要无脑放大,否则故障切换会变慢。
maxClientCnxns默认是60,也就是单个IP最多建立60个连接。如果ETL并发任务特别多,到处创建客户端连接,很容易触发这个限制,建议生产环境调到300左右。注意,这只是单IP连接数上限,不是全局连接数。
每个节点上还要创建dataDir目录,然后在里面写一个myid文件。三台机器分别写入1、2、3:
bash复制mkdir -p /data/zookeeper/logs
echo 1 > /data/zookeeper/myid
这里很容易踩一个坑:dataDir目录在ZooKeeper启动时会自动创建,但myid文件如果没写或者写错,启动直接报错。而且myid必须跟zoo.cfg里的server序号对应,不能乱写。
启动集群时,最好按顺序启动一台、确认成功后再启动下一台。启动命令非常简单:
bash复制cd /usr/local/zookeeper/bin
./zkServer.sh start
查看状态:
bash复制./zkServer.sh status
3个节点正常时,会看到其中一个显示Mode: leader,其余显示Mode: follower。另外用jps能看到QuorumPeerMain进程,说明进程常驻。这一步最容易出现的问题是防火墙没放通2888和3888端口,导致节点之间连不上,一直停留在LOOKING状态。
3.3 和Hadoop HA整合,给ETL的数据底座加一层保险
很多ETL任务要直接读HDFS。如果HDFS的NameNode是单点,一旦它挂了,所有读HDFS的ETL任务都会失败。这时候ZooKeeper和ZKFC(ZooKeeper Failover Controller)就能派上用场。
这里说的是HDFS NameNode的自动故障转移。配置的核心逻辑是:两个NameNode节点共享同一份命名空间编辑日志,通过ZKFC在ZooKeeper里注册一个临时节点,谁抢到这个节点谁就是Active,另一个保持Standby。
需要在core-site.xml里配置ZooKeeper地址:
xml复制<property>
<name>ha.zookeeper.quorum</name>
<value>zk01:2181,zk02:2181,zk03:2181</value>
</property>
在hdfs-site.xml里启用自动故障转移:
xml复制<property>
<name>dfs.ha.automatic-failover.enabled</name>
<value>true</value>
</property>
配置好之后,在集群上执行一下ZKFC格式化:
bash复制hdfs zkfc -formatZK
这个命令会在ZooKeeper里创建HDFS HA相关的节点。这里有个很关键的坑:只要格式化过一次,后面如果手滑把ZooKeeper里的节点删了,两个NameNode可能同时尝试变成Active,所以不要随便去手动删HDFS HA的ZK节点。
格式化完成后,分别启动两个NameNode,再启动ZKFC进程(通常随NameNode一起启动)。之后可以观察ZooKeeper里的节点:
bash复制zkCli.sh -server zk01:2181
ls /hadoop-ha/nameservice1
正常情况下会看到类似ActiveStandbyElectorLock的节点。哪个NameNode是Active,看这个临时锁节点在谁的Session下创建的。
这套架构的价值在ETL里体现得很直接:曾经有一次凌晨跑批,某个NameNode节点因为内存故障重启,如果是单NameNode,所有读HDFS的任务会排队失败。配了自动故障转移后,几秒内Standby自动上位,整个跑批只是轻微延迟,没有任务级失败。
3.4 把分布式调度器和ZooKeeper串起来
如果你用的ETL调度平台是DolphinScheduler,那ZooKeeper基本是标配。安装目录的conf/application.yaml(或application.properties)里会有一组ZooKeeper配置项。不同版本可能写法不同,核心是告诉调度器连接哪个ZK集群。
比如这样的配置:
yaml复制registry:
type: zookeeper
zookeeper:
connect-string: zk01:2181,zk02:2181,zk03:2181
配置完之后,启动Master服务和Worker服务。启动成功后,用ZooKeeper客户端看一眼调度器的节点注册情况:
bash复制zkCli.sh -server zk01:2181
ls /dolphinscheduler/nodes/master
ls /dolphinscheduler/nodes/worker
如果能看到Master节点列表和Worker节点列表,说明Master选举和Worker注册都正常。
DolphinScheduler里,Master节点不是通过配置文件指定谁是主的,而是多个Master启动后去ZK里竞选,谁成功谁是主。主Master挂掉后,其他Master会抢到临时节点,继续调度。Worker节点的临时节点同样起着存活感知的作用,某个Worker宕机后,它下面正在执行的任务会被其他Worker接手。
如果你用的是Azkaban,逻辑类似。在azkaban.properties里开启多执行器模式,并配置ZK地址。执行器启动时会在ZK中注册自己的地址,Web Server从ZK获取可用的执行器列表,这样新增执行器就不用改配置文件了。
3.5 自己写一个基于ZooKeeper的任务互斥锁
有时候不想引入完整的调度平台,只想给自己写的ETL小框架加一个“避免重复执行”的保险。用ZooKeeper实现互斥锁其实很直接。
先用命令行理解过程。假设两台Worker都要跑凌晨2点的order_etl任务,大家约定锁路径是/etl/locks/order_etl:
bash复制# Worker A 尝试创建锁节点,-e 表示临时节点
create -e /etl/locks/order_etl
# 如果返回路径,说明A拿到锁
# Worker B 尝试创建同样的节点
create -e /etl/locks/order_etl
# 返回 NodeExists,说明B拿不到锁,只能等待
# A任务执行完后删除节点
delete /etl/locks/order_etl
# B继续抢锁
这段命令背后依赖一个事实:ZooKeeper的节点创建操作是原子的,同一个路径下同一个时刻只能有一个客户端创建成功。
生产代码里不推荐直接操作原生ZooKeeper API,重复轮子容易出问题。我用的是Curator框架,它对选主、锁、监听都封装得很完善。用起来大概是这样的:
java复制CuratorFramework client = CuratorFrameworkFactory.newClient(
"zk01:2181,zk02:2181,zk03:2181",
new ExponentialBackoffRetry(1000, 3));
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/etl/locks/order_etl");
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 拉取数据、清洗、写入目标库
} finally {
lock.release();
}
}
client.close();
用Curator的InterProcessMutex时,最需要注意的是acquire的超时时间。不要设成无限等待,否则一旦锁释放异常,所有等待任务都会卡在那边。10秒或30秒比较合适,超时后返回失败,由上层调度决定重试还是告警。
另外,锁节点路径要包含任务标识。如果所有任务共用同一个锁路径,那整个ETL平台就变成单任务串行执行了,性能完全不可接受。可以按业务线、任务名、调度日期做层级划分,比如/etl/locks/ods/order/20250101。
4. 生产环境常见故障与排查速查表
4.1 一张表看懂常见问题
我整理了一份自己在维护ZooKeeper和ETL链路时最常遇到的问题,每一条都是真实踩过的坑,可以先收藏。
| 现象 | 常见原因 | 排查命令/方法 | 解决建议 |
|---|---|---|---|
| 任务被重复执行 | 临时锁节点因会话超时丢失,锁误释放 | 查看ZK日志和客户端连接日志,确认sessionTimeout设置 | 调大sessionTimeout,检查网络抖动,换用Curator重试机制 |
| ZooKeeper集群一直LOOKING | 2888/3888端口不通,无法选主 | 检查防火墙、用telnet测试节点端口 | 放通端口,确认所有server节点配置完整 |
| 客户端连接被拒绝 | maxClientCnxns太小,单个IP连接数超限 | ZK日志中出现Too many connections from... | 调大maxClientCnxns,优化客户端连接复用 |
| Watcher不触发 | Watcher是一次性的,过期后未重新注册 | 在回调里打日志,确认执行路径 | 回调末尾重新注册Watcher |
| 节点数据写不进去 | 数据超过1MB限制 | 尝试创建大节点时会报错 | ZooKeeper不适合存大对象,改存数据库或缓存 |
| 磁盘被快照日志占满 | 没启用自动清理 | du -sh /data/zookeeper/version-2 | 配置autopurge.snapRetainCount和purgeInterval |
| NameNode无法自动切换 | ZKFC的锁节点被误删,或者格式化信息不匹配 | ls /hadoop-ha/nameservice1 | 执行hdfs zkfc -formatZK重新初始化 |
4.2 会话超时导致的任务锁丢失,是最隐蔽的坑
这一类问题最难排查,因为它不是直接报错,而是“逻辑上出错”。现象是:同一个ETL任务在同一分钟内被两个节点执行,但所有ZooKeeper节点状态都正常,没有异常日志。
根因通常出在sessionTimeout设置上。客户端与ZooKeeper建立Session后,如果因为网络抖动、GC停顿、机器负载过高,心跳发送不及时,超过服务端容忍时间,ZooKeeper就会判定Session过期,自动清理这个Session创建的临时节点。
临时节点一删,任务锁就丢了。第二个Worker原本在等待,此时尝试创建锁节点会发现能创建成功,于是开始执行同一个任务。
我之前碰到过一起事故,一个凌晨跑批任务把上游业务数据库拖到CPU飙高,排查下来就是某台Worker机器的GC暂停了接近30秒,Session过期,锁被释放,另一个Worker补位执行。两个Worker同时拿着同一个任务在跑。
解决思路是:把sessionTimeout适当调大到25到40秒,同时监控客户端机器的GC和负载。另外,任务执行逻辑里最好做一次幂等设计,比如写入目标表时按日期分区覆盖,这样即使锁失效出现重复执行,至少不会产生重复数据。
4.3 连接数被打满,不一定是ZooKeeper自身的问题
ZooKeeper默认maxClientCnxns=60,这个限制是“每IP连接数”,不是“总连接数”。ETL任务并发一高,如果每个任务都临时创建一个ZooKeeper客户端而且不关闭,很快会触到这个限制。
日志里会出现类似Too many connections from /192.168.x.x的报错。此时并不是ZooKeeper性能不够,而是客户端使用方式太粗暴。
解决办法有两个方向。一是调大maxClientCnxns到300或更高;二是优化客户端连接管理。Curator本身自带连接管理机制,一个客户端实例可以复用,不要每次都new一个。整个ETL平台按服务维度维护一个全局CuratorFramework实例即可。
我在自研调度器里就是这样处理的:启动时初始化一个全局客户端,所有锁、注册、监听都复用这个连接。即使几百个任务并发,底层也只有几路连接,ZooKeeper侧很轻松。
4.4 脑裂这个词被用烂了,真正的脑裂是什么样的
很多人在群里一看到ZooKeeper集群状态不对就喊“脑裂”。其实ZooKeeper的ZAB协议对脑裂有很强的防护,网络分区时,只有包含多数节点的那一侧能继续对外提供服务。少数派节点即使还在运行,也无法选举出Leader,更不会接受写请求。
真正需要警惕的场景是:应用客户端自己出了问题,导致两个Master进程都认为自己持有锁,而不是ZooKeeper本身脑裂。比如有些系统和ZooKeeper的Session断了,但没有正确退出业务逻辑,继续在跑任务;等它重连后又抢到了锁,这时候就可能出现两个业务主同时工作。
排查这类问题时,不要只盯着ZooKeeper的status输出,还要看业务进程的完整日志。确认业务主进程在失去ZooKeeper会话时,是否真的把本地状态降级为Standby,停止了任务下发。这是比ZooKeeper配置更值得关注的“业务级脑裂”问题。
4.5 快照日志清理,是磁盘告警的必修课
ZooKeeper运行时间长了,dataDir的version-2目录下会积累大量事务日志和快照文件。如果不清理,磁盘迟早撑爆。
最稳妥的方式不是手动删文件,而是启用自动清理:
bash复制autopurge.snapRetainCount=3
autopurge.purgeInterval=1
snapRetainCount=3表示保留最近3份快照,purgeInterval=1表示每1小时清理一次。这个配置加在zoo.cfg里后重启集群生效。
需要注意,自动清理只对合法的事务日志和快照文件生效。如果之前已经出现磁盘写满,要小心先停掉某个节点,清理出足够空间后再启动,避免对正在运行的集群做危险的删除操作。
5. 用了这么久ZooKeeper,几个最值得记住的经验
5.1 不要把ZooKeeper当数据库用
这是我在团队里反复强调的一句话。ZooKeeper擅长的是“存状态、管协调、发通知”,不是“存数据”。ETL任务的任务详情、清洗规则、血缘关系这些数据,老老实实放到MySQL或者HBase里。ZooKeeper路径上只放节点标识、锁标记、版本号这类轻量信息。
有一次新同事想用ZooKeeper存一批日切表的字段映射关系,我看了一眼那个JSON的大小,接近几百KB,立刻让他改方案。真要这么干,集群性能和稳定性都会出问题。
5.2 路径设计要提前规范化,不然后期全是泪
多个团队共用一个ZooKeeper集群时,路径规划不统一会带来连锁问题。比如调度平台、HDFS HA、Kafka、自研锁,各自的根路径必须清晰:
text复制/dolphinscheduler
/hadoop-ha
/kafka
/etl/locks
/etl/workers
每个业务团队约定好自己的命名空间,不要在根目录下乱建节点。否则排查问题的时候,根本分不清某个节点是谁创建的。路径一旦成型,后期想迁移非常痛苦,因为所有客户端代码里的监听路径和锁路径都要跟着改。
5.3 锁、选主这类能力,尽量用现成框架
ZooKeeper原生API功能底层、异常处理多,直接写业务代码容易出问题。选主用Curator的LeaderLatch或LeaderSelector,锁用InterProcessMutex,监听用PathChildrenCache。这些封装经过大量线上验证,边界情况处理得比手工代码可靠。
尤其是InterProcessMutex,它内部处理了锁重入、异常恢复、会话过期后的锁清理。如果自己实现锁,遇到Session过期还容易留下永久节点,导致锁永远释放不了。
5.4 监控ZooKeeper,不能只看进程在不在
进程在,不代表ZooKeeper服务健康。我建议至少监控这几个指标:客户端连接数、znode数量、磁盘使用率、请求延迟(特别是sync和create操作延迟)、Leader是否发生过切换。
连接数突然飙升,往往意味着某些客户端没复用连接;znode数量异常增长,说明有节点只创建不删除,多半是临时节点逻辑有Bug;请求延迟变大,需要关注GC和磁盘IO。把这些指标接入现有的Prometheus监控体系之后,很多ETL问题可以在任务失败前就被发现,不用等到第二天早上起来看告警。
6. 最后再分享两个实际运维里的小技巧
第一个是版本升级的时候,先看一眼兼容性再动手。ZooKeeper 3.5之后,客户端和服务端的协议有过调整,有些老版本客户端连接新版服务端会报异常。我们当时从3.4升3.7,主要改了配置项名称,比如autopurge相关参数,还有部分客户端库要跟着升级。生产环境升级前,先在测试环境把Hadoop、Kafka、调度平台都完整跑一遍ETL用例,确认没问题再动线上。
第二个是ZooKeeper的status输出里,有个Mode字段很有用。它的值是leader或follower。如果某个节点输出standalone,说明它没有按集群模式启动,和另外两个节点不在同一个集群里。ETL调度链路中,如果某个组件配置了错误的ZK地址,连上一个standalone节点,而其他组件连的是集群,就会出现“部分正常、部分异常”的诡异状态。排查问题时,先确认所有配置里的ZK地址写的是同一个集群。
我在实际项目中,最深的体会是:ZooKeeper的价值不在它本身多复杂,而在于它能给一套分布式ETL系统提供确定性的协调规则。凌晨跑批最怕的不是单条数据写错,而是同一个任务在好几个节点上同时跑了好几遍。把这个底线守住,ETL平台才能从“能跑”变成“稳跑”。如果你正在设计一套新的ETL调度框架,我的建议很简单:把ZooKeeper当成基础设施规划进去,越早考虑,后面越省心。
