☰
ZooKeeper在Hadoop高可用中的选主机制与故障排查
2026/11/2 8:50:53 网站建设 项目流程

1. 先把ZK在Hadoop里的角色摆正

很多人第一次接触ZooKeeper,是在一套已经跑起来的Hadoop集群上看到进程列表里多出来的QuorumPeerMain。它就那么安静地趴在三台机器上,CPU占用常年个位数,日志也翻不出几行,看起来像个可有可无的配角。直到某天ZK集群的某一台机器磁盘写满、进程掉线,然后NameNode开始不停地切换主备,HDFS客户端疯狂报RetriableException,你才会突然意识到:这个"配角"其实是整条链路里唯一握着裁判哨的人。

ZooKeeper在Hadoop生态里的应用,说穿了就是三个字——协调者。它不存储业务数据,不参与数据块读写,也不做计算调度,它只负责回答一类问题:在分布式系统里,谁现在是主、谁还活着、大家有没有达成一致。放到HDFS里,它决定了两个NameNode中哪个是Active;放到YARN里,它决定了哪个ResourceManager对外提供服务;放到HBase里,它是所有RegionServer报到和心跳的中转站。这些组件把"谁说了算"这种最容易出岔子的事情外包给了ZK,自己专心做存储和计算。

这篇文章面向的是已经能搭起单机Hadoop、但对高可用部分还比较模糊的工程师,也适合那些正在被"NN频繁切换""RM起不来""ZK session expired"这类报错折磨的运维同学。我会把ZooKeeper在Hadoop各组件里到底承担了什么、配置怎么写、故障怎么查,一层一层拆开讲。涉及具体参数的会给出计算过程,涉及操作的会给出可以直接抄的命令,踩过的坑也一并写出来。

1.1 它不存数据,只负责"谁说了算"

理解ZK的定位,最有效的方式是把它看成一个带监听功能的分布式小文件系统。它的数据结构是一棵树,每个节点叫znode,节点上能挂一小段字节数据,官方建议单个节点数据不要超过1MB,实际生产中几百字节就够了。常用的znode只有两类:持久节点和临时节点。临时节点的生命周期绑在客户端会话上,客户端断连、会话超时,节点自动消失——这一个特性就撑起了整个Hadoop高可用的半边天。

主备选举的本质,就是多个候选者同时去ZK上抢着创建一个同名临时节点。ZK保证同一路径下创建操作是原子的,谁先创建成功谁就是主,剩下的只能注册监听,等着这个节点被删除的通知。主节点进程挂掉之后,会话超时,临时节点被ZK自动清理,备节点收到通知,立刻发起新一轮抢占。整个过程不需要任何人工干预,也不需要主备之间直接通信,ZK充当了那个绝对中立的第三方。

这种设计的精妙之处在于把"存活判定"交给了会话机制。主备之间互相ping是不可靠的,网络分区的时候双方都觉得自己没死、对方死了,这就是脑裂的根源。而ZK的会话是有超时时间的,主节点必须持续续约才能保住临时节点,一旦续不上,裁判直接判负。判定的依据不是"我觉得",而是"ZK觉得",这就把主观判断变成了客观事实。

1.2 三个绕不开的核心用途

在Hadoop生态里,ZK承担的职责可以归成三类,几乎所有组件的用法都是这三类的变体。

用途具体表现典型组件
主备选举抢临时节点决定谁是ActiveHDFS NameNode、YARN RM、HBase Master
状态存储持久znode记录集群元信息YARN RMStateStore、HBase 集群状态
成员管理临时节点注册 + 监听感知上下线HBase RegionServer、HiveServer2 实例

主备选举是大家最熟悉的场景。HDFS的两个NameNode通过ZKFC组件在ZK上抢锁,YARN的两个ResourceManager通过内嵌的elector抢锁,HBase的多个Master同样如此。抢到锁的那个对外提供服务,另一个进入Standby状态持续监听。

状态存储用得最多的是YARN。RM重启之后需要恢复应用状态,包括有哪些Application、每个应用跑到哪个阶段、Container分配情况。这些信息存在ZK的/yarn-leader-election和/RMStateStore路径下,RM重启后从ZK读回来,正在运行的作业不至于全部丢失。这个能力叫RM Restart,是生产环境必开的。

成员管理在HBase里体现得最明显。每个RegionServer启动时会在/hbase/rs下创建一个临时节点,Master监听这个目录,任何一个子节点消失就说明对应的RS下线了,需要把它上面的Region重新分配。整个过程就是标准的"注册-监听-回调"模型,HBase自己不用维护任何心跳表。

1.3 为什么Hadoop不自己造一个

这个问题我问过自己很多次,答案其实不难理解。Hadoop的各个组件是分阶段演进的,HDFS最早是单NameNode,YARN从MRv1脱胎而来,HBase是另一个团队独立演化的项目。如果每个组件都自己实现一套选主和状态同步机制,那就是重复造三轮轮子,而且每一轮的可靠性都要重新验证。用ZK等于复用一个已经被大量系统验证过的协调层,组件只需要实现"抢锁"和"监听"这两个简单接口。

另一个现实原因是ZK的语义足够简单,简单到不容易用错。它没有提供租约、没有提供复杂的分布式事务,只有节点、会话、监听、版本号这几样东西。功能少意味着边界清晰,工程师不用去纠结各种边界条件。相比之下,自己实现的选主方案往往在"网络抖动""GC停顿""时钟漂移"这些场景上栽跟头,而这些恰恰是ZK已经处理好的部分。

代价当然也有。引入ZK意味着多了一套需要独立运维的集群,多了一个故障点。ZK集群整体不可用时,已经选出的主还能继续工作(因为主备切换逻辑只在需要切换时才访问ZK),但如果此时发生主节点故障,就没法自动切换了。这个取舍在生产上是可以接受的:ZK的可用性通常比Hadoop组件本身还高,因为它的负载极轻。

2. HDFS NameNode HA:ZK最经典的战场

HDFS的高可用是ZooKeeper在Hadoop里最标准、也最值得深挖的应用。理解了这一套,其他组件的用法基本都是它的简化版或者变体。

2.1 双NameNode的脑裂隐患从哪来

单NameNode时代,元数据服务是明确的单点。上了双NameNode之后,问题从"机器挂了怎么办"变成了"两台都以为自己是主怎么办"。脑裂的危害比单点故障更严重:两个NameNode同时接受写请求,各自维护一份edits日志,元数据会彻底分叉,恢复起来极其痛苦。

Hadoop解决这个问题的思路是共享存储 + 排他锁。两个NameNode都往一组JournalNode上写edits,JournalNode保证同一时刻只有一个写入者。但JournalNode本身不负责判断谁是主,它只是被动接受写入。真正做裁判的还是ZK:ZKFC(ZooKeeper Failover Controller)作为独立进程部署在每台NameNode机器上,负责在ZK上抢锁,抢到锁的那个ZKFC才有资格把自己的NameNode提升为Active。

这里有个关键点容易被忽略:ZKFC和NameNode是两个进程。ZKFC负责健康检查和选主,NameNode只管干活。设计成两个进程是有意为之的,因为ZKFC需要在NameNode失去响应(比如长时间Full GC)时能够果断地发fencing命令把它干掉,如果两者在同一个进程里,ZKFC自己也被卡住了,就什么都做不了。

注意:ZKFC进程挂掉不会导致NameNode立刻降级,但会让这台机器失去参与选主的能力。如果你的监控只盯NameNode进程,ZKFC静默退出可能几天都发现不了。这一点后面排查章节还会展开。

2.2 ZKFC + ActiveStandbyElector 的完整握手流程

把整个选主过程拆成时间线,大致是这样的:

  1. 两个ZKFC同时启动,各自与ZK集群建立会话,会话超时由ha.zookeeper.session-timeout.ms控制,默认10000毫秒。
  2. 双方同时尝试创建临时节点/hadoop-ha/<nameservice>/ActiveStandbyElectorLock。ZK保证原子性,只有一个能成功。
  3. 创建成功的ZKFC调用本地NameNode的transitionToActiveRPC,把自己的NameNode提升为Active,同时向JournalNode开始写edits。
  4. 创建失败的ZKFC在锁节点上注册watch,然后调用本地NameNode的transitionToStandby,进入待命状态。
  5. 一旦Active的ZKFC会话失效(进程崩溃、网络中断、机器宕机),锁节点被ZK删除,Standby侧的watch被触发。
  6. Standby的ZKFC立即尝试创建锁节点,成功后执行fencing,确认老的主确实已经停止服务,再提升自己。

第6步的fencing是整套机制里最关键也最容易被跳过的一环。ZK只知道会话断了,它无法确认老NameNode是不是真的死了——有可能只是ZKFC和ZK之间的网络断了,NameNode本身还在好好地对外提供服务。这时候如果直接提升新的主,脑裂就发生了。fencing的作用就是用强制手段(SSH过去kill进程、或者调用带外管理接口断电)确保老的主彻底停止。

配置上通过dfs.ha.fencing.methods指定,常见写法:

<property> <name>dfs.ha.fencing.methods</name> <value>sshfence</value> </property> <property> <name>dfs.ha.fencing.ssh.private-key-files</name> <value>/home/hadoop/.ssh/id_rsa</value> </property> <property> <name>dfs.ha.fencing.ssh.connect-timeout</name> <value>30000</value> </property>

sshfence的原理是SSH登录到对端,用fuser命令查出谁占着8020端口,然后kill掉。这要求两台机器之间配置好免密登录,并且fuser命令存在。有些精简过的系统镜像里没有fuser,导致fencing静默失败,最后报一个很含糊的"Unable to fence"错误,我在一个容器化环境里就遇到过这种坑。

2.3 关键配置逐条拆开看

HDFS HA涉及的配置项不少,但真正和ZK相关的其实就几个。下面这张表是我自己整理的核心清单:

配置项作用建议值
ha.zookeeper.quorumZK集群地址列表三节点全列出,带端口
ha.zookeeper.session-timeout.msZKFC与ZK的会话超时10000,与ZK服务端上限对齐
ha.zookeeper.parent-znodeHA节点在ZK上的根路径默认/hadoop-ha
dfs.ha.automatic-failover.enabled是否启用自动切换true
dfs.ha.fencing.methods隔离方式生产必须配,测试可临时用shell(/bin/true)

ha.zookeeper.quorum的写法是逗号分隔的host:port列表,端口默认2181。这里有个细节:不要把端口写错成2888或3888,那是ZK集群内部通信和数据同步用的端口,客户端连上去会一直卡住直到超时,报错信息还特别误导人。

ha.zookeeper.session-timeout.ms和ZK服务端的tickTime是联动的。ZK服务端允许的最大会话超时是20 * tickTime,默认tickTime=2000ms,也就是40秒。如果客户端请求10000毫秒,在允许范围内,就按10000算。但如果哪天有人把客户端的值调到了60000,ZK会把它截断成40000,实际生效的是40秒而不是你以为的60秒。这个截断不会报错,只会让故障切换的实际耗时和你预期的不一致。

自动切换开关dfs.ha.automatic-failover.enabled打开之后,fs.defaultFS里配置的地址就要用nameservice的别名而不是具体主机名,同时必须配置dfs.client.failover.proxy.provider.<nameservice>,否则客户端不知道该怎么切。

<property> <name>dfs.nameservices</name> <value>mycluster</value> </property> <property> <name>dfs.ha.namenodes.mycluster</name> <value>nn1,nn2</value> </property> <property> <name>dfs.namenode.rpc-address.mycluster.nn1</name> <value>node01:8020</value> </property> <property> <name>dfs.namenode.rpc-address.mycluster.nn2</name> <value>node02:8020</value> </property> <property> <name>dfs.client.failover.proxy.provider.mycluster</name> <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value> </property>

2.4 znode结构与你该看的现场

配好之后,用zkCli.sh连上去看一眼,能直观理解ZK里到底存了什么:

zkCli.sh -server node01:2181 [zk: node01:2181(CONNECTED) 0] ls /hadoop-ha [mycluster] [zk: node01:2181(CONNECTED) 1] ls /hadoop-ha/mycluster [ActiveBreadCrumb, ActiveStandbyElectorLock]

get /hadoop-ha/mycluster/ActiveBreadCrumb能看到当前(或者最后一任)Active节点的信息,里面记录了主机名和端口,是持久节点,不会随会话消失。而ActiveStandbyElectorLock是临时节点,只有Active活着的时候才存在,Standby机器上看这个节点是空目录。

这个区分非常实用:如果你发现ActiveBreadCrumb存在但ActiveStandbyElectorLock不存在,说明之前有过一个Active,现在它下线了,正在进行切换或者切换失败卡住了。这时候去看ZKFC日志里的fencing环节,八成能定位到问题。

3. YARN ResourceManager HA里的ZK

YARN的高可用和HDFS思路同源,但细节上差得挺多,尤其是状态存储这部分,是YARN独有的。

3.1 RM的选举和状态恢复是两件事

很多人以为RM的HA就是简单的选主,其实它包含两个独立的能力:选主解决"谁对外服务",状态恢复解决"新上任的主知不知道之前发生了什么"。前者靠ZK的临时节点,后者靠ZK的持久节点。

只有选主没有状态恢复的话,主备切换之后,所有正在跑的MapReduce或Spark作业在RM眼里都不存在了,Container还在NodeManager上跑着但没人管理,最后要么资源泄漏要么作业失败。生产环境这两个必须一起开。

状态恢复的实现是ZKRMStateStore,它把应用状态序列化后写到/yarn-leader-election和/RMStateStore路径下。每次有应用状态变更(提交、运行中、完成),RM都会同步写一次ZK。这带来一个副作用:ZK的写入压力会比纯HA场景大不少,在作业提交密集的集群上要留意ZK的磁盘IO。

配置项如下:

<property> <name>yarn.resourcemanager.ha.enabled</name> <value>true</value> </property> <property> <name>yarn.resourcemanager.cluster-id</name> <value>yarn-cluster</value> </property> <property> <name>yarn.resourcemanager.ha.rm-ids</name> <value>rm1,rm2</value> </property> <property> <name>yarn.resourcemanager.hostname.rm1</name> <value>node03</value> </property> <property> <name>yarn.resourcemanager.hostname.rm2</name> <value>node04</value> </property> <property> <name>yarn.resourcemanager.zk-address</name> <value>node01:2181,node02:2181,node03:2181</value> </property> <property> <name>yarn.resourcemanager.recovery.enabled</name> <value>true</value> </property> <property> <name>yarn.resourcemanager.store.class</name> <value>org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore</value> </property>

yarn.resourcemanager.cluster-id是ZK上的路径分隔符,多个YARN集群共用一套ZK时靠它隔离。这个值一旦上线就不要改,改了之后原来的状态全读不到了,等于把恢复能力废掉。

3.2 配置清单与嵌入式选主

YARN的选主默认是嵌入式的,也就是elector跑在RM进程内部,不像HDFS那样有独立的ZKFC进程。对应配置是yarn.resourcemanager.ha.automatic-failover.embedded,默认true。

嵌入式的好处是部署简单,少一个进程要管;坏处是RM进程本身卡住(比如长时间GC)时,elector也跟着卡住,没法主动放弃锁。不过YARN对这种情况有兜底:Standby的RM会持续监控Active的health,通过内部RPC探测,超时就自己发起切换。这个机制叫yarn.resourcemanager.ha.automatic-failover.zk-retry-interval-ms配合健康检查,实际表现比HDFS的纯ZK会话超时判定要稍微积极一些。

另外还有一个yarn.resourcemanager.ha.id,需要每个RM配成自己的id。这个值不配的话,启动时会报一个很清楚的错,但错误信息里只说要设置,不说为什么。原因是RM需要知道"我是谁",才能判断自己在ZK上抢到的锁是不是属于自己的。

启动顺序上,建议先起ZK,再起两个RM。如果RM先于ZK启动,会不断重试连接,日志刷得很快但不影响最终结果,只是看着糟心。

3.3 与HDFS HA的差异对照

放在一起对比会更清楚两者设计的取舍:

维度HDFS NameNode HAYARN ResourceManager HA
选主组件独立进程ZKFCRM进程内嵌elector
共享存储JournalNode(独立部署)ZK状态存储(复用ZK)
状态持久化edits日志ZK znode
fencing必须配置,SSH或脚本依赖RM自身退出,一般不单独配
切换耗时受fencing影响,通常30-60秒通常10-30秒

HDFS为什么坚持用JournalNode而不用ZK存edits?因为edits的量级和写入频率远超ZK能承受的范围,ZK设计上就不适合存大块数据。YARN的状态数据量小得多,一个集群几千个应用也就几百KB,放ZK里完全没问题。这个差异背后是对两者数据规模的准确判断。

4. HBase、Hive等组件的ZK依赖

Hadoop生态里依赖ZK的组件远不止HDFS和YARN,HBase对ZK的依赖程度甚至更高——它是把ZK当成集群的"中枢神经"在用。

4.1 HBase:ZK是集群的"花名册"

HBase在ZK上的znode结构比HDFS复杂得多,默认根路径是/hbase。几个关键节点:

  • /hbase/master:临时节点,当前Active Master持有,抢到即为主。
  • /hbase/backup-masters:备用Master注册在这里,等着接手。
  • /hbase/rs:所有RegionServer的临时节点都在这个目录下,一个子节点对应一台RS。
  • /hbase/hbaseid:集群的唯一标识,持久节点,写入后一般不改。
  • /hbase/replication、/hbase/meta-region-server:分别存复制状态和meta表所在的RegionServer地址。

/hbase/meta-region-server这个节点特别关键。客户端要读写数据,第一步得知道meta表在哪儿,这个信息就存在ZK里。所以ZK不可用时,老客户端能继续读写(地址已经缓存了),新客户端连都连不上。这也解释了为什么HBase运维里"ZK挂了"往往表现为"客户端连不上集群"而不是"数据写不进去"。

HBase还支持多集群共用一套ZK,靠zookeeper.znode.parent隔离,比如/hbase-cluster1、/hbase-cluster2。这个配置上线后不能改,改了等于换了个集群,原有数据全部读不到。我在一个项目里见过有人为了"清理一下ZK里的垃圾节点"直接改了parent,结果整个集群的客户端全部失联,最后靠回滚配置才恢复。

RegionServer的上下线感知完全依赖ZK的临时节点和watch机制。RS进程正常退出时会主动删除自己的节点,异常宕机则等会话超时后ZK自动清理。Master在这两种情况下收到的通知是一样的,后续处理也一样,不需要区分。

4.2 HiveServer2 的HA注册

HiveServer2的ZK用法稍微不同,它不涉及选主,只做服务发现。多个HS2实例启动时各自在ZK上注册一个临时节点,节点里写入自己的host:port。客户端(比如JDBC客户端或者BI工具)连ZK读这个目录,拿到一个可用的HS2地址,然后建立连接。第一个实例挂了,节点消失,客户端下次拉取时自动拿到其他实例的地址。

<property> <name>hive.zookeeper.quorum</name> <value>node01,node02,node03</value> </property> <property> <name>hive.zookeeper.client.port</name> <value>2181</value> </property> <property> <name>hive.server2.support.dynamic.service.discovery</name> <value>true</value> </property> <property> <name>hive.server2.zookeeper.namespace</name> <value>hiveserver2</value> </property>

这里要提醒一句:HS2的动态服务发现要求JDBC连接串用jdbc:hive2://<zookeeper-quorum>/;serviceDiscoveryMode=zooKeeper;zooKeeperNamespace=hiveserver2这种格式,而不是直接写主机名。很多团队配了ZK发现但连接串还是老格式,结果等于白配。

4.3 共用ZK集群时如何隔离

中小规模集群通常会共用一套ZK,HDFS、YARN、HBase、Hive都连同一个quorum。这时候路径隔离和容量规划就很重要了。

路径隔离靠各自的根节点参数:HDFS用ha.zookeeper.parent-znode,YARN用yarn.resourcemanager.cluster-id,HBase用zookeeper.znode.parent,Hive用hive.server2.zookeeper.namespace。只要这几个名字不冲突,各组件在ZK上是互不干扰的。

但要留意ZK的容量和连接数。单个znode数据建议控制在1MB以内,整个ZK的节点数不宜过多(几万个是常见量级,几十万就要审视一下设计)。连接数方面,每个RS、每个HS2、每个ZKFC都会占用至少一个会话,上百个节点的集群里ZK的连接数上千是常态,要确保maxClientCnxns(默认60,按IP限制)够用。这个参数是按IP算的,如果多台服务部署在同一台机器上,很容易撞上限,表现为连接被拒绝。

还有一个隐蔽的坑:ZK的watch是一次性的。客户端在某个节点上注册watch,触发一次后就失效了,需要重新注册。Hadoop和HBase的客户端都做了自动重注册,但如果版本不一致(比如ZK客户端版本低于服务端太多),可能出现重注册失败的边缘情况,表现为"集群状态变化后,某台机器上的客户端一直看不到更新"。这类问题排查起来很费劲,稳妥的做法是把Hadoop依赖的ZK客户端版本和服务端版本控制在同一个大版本内。

5. 搭一套能跑的最小实战环境

理论说再多,不如自己搭一遍。下面这套流程是我在虚拟机上反复验证过的,三台机器即可,配置精简但功能完整。

5.1 部署前的前置检查清单

动手之前把这几个点确认一遍,能省掉后面大量的排查时间:

  • 主机名和hosts:三台机器互相能ping通主机名,/etc/hosts里写死,不要依赖DNS。ZK的配置文件和Hadoop的配置里都用主机名,用IP会导致证书和fencing出问题。
  • 时钟同步:三台机器的时间偏差控制在几十毫秒内。ZK的会话判定对时间敏感,偏差大了会出现"莫名其妙被踢下线"。
  • JDK版本:ZK和Hadoop用同一个JDK,避免版本混用。
  • 防火墙:2181(客户端)、2888(集群内部通信)、3888(选举)三个端口必须互通。8020、8485(JournalNode)、8088这些Hadoop端口也要放开。
  • 目录规划:ZK的dataDir和Hadoop的元数据目录分开,不要放在同一个磁盘分区上,避免互相影响。

5.2 ZK集群配置与myid

在三台机器上分别创建dataDir,然后写zoo.cfg:

tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper/data clientPort=2181 maxClientCnxns=200 autopurge.snapRetainCount=5 autopurge.purgeInterval=24 server.1=node01:2888:3888 server.2=node02:2888:3888 server.3=node03:2888:3888 4lw.commands.whitelist=*

几个参数的含义需要说清楚。tickTime=2000是ZK的时间单位,毫秒。initLimit=10表示Follower启动时与Leader建立连接的最长等待是10个tick,即20秒。syncLimit=5表示Follower与Leader之间心跳超时是5个tick,即10秒。这两个值在跨机房部署或者磁盘IO很慢的环境里要适当调大,否则会频繁触发重选举。

autopurge.purgeInterval=24表示每24小时清理一次历史快照和事务日志,配合snapRetainCount=5只保留最近5份。这两个参数强烈建议打开,否则磁盘迟早被写满——ZK的日志增长虽然慢,但架不住它从来不删。

每台机器的dataDir下要放一个myid文件,内容就是server.N里的N:

echo 1 > /data/zookeeper/data/myid # node01 echo 2 > /data/zookeeper/data/myid # node02 echo 3 > /data/zookeeper/data/myid # node03

启动后验证集群状态:

# 确认角色 echo stat | nc node01 2181 | grep Mode # 输出 Mode: follower 或 Mode: leader # 确认集群内其他节点都看到了 echo mntr | nc node01 2181 | grep zk_synced_followers

三台机器全部启动后,会有一台选举成Leader,另外两台是Follower。如果只有一台起来,它自己是无法形成Quorum的,会一直处于LOOKING状态,日志里不断刷Cannot open channel to ...,这是正常的,等第二台起来就好了。

5.3 Hadoop HA 配置落地

ZK集群就绪后,配置HDFS HA。core-site.xml里改fs.defaultFS为nameservice别名,hdfs-site.xml里把前面表格里的配置项写全,另外还要配JournalNode的地址:

<property> <name>dfs.namenode.shared.edits.dir</name> <value>qjournal://node01:8485;node02:8485;node03:8485/mycluster</value> </property> <property> <name>dfs.journalnode.edits.dir</name> <value>/data/hadoop/journal</value> </property>

JournalNode的数量建议是奇数,三台或五台。它不需要和NameNode在同一批机器上,独立部署更好,但小集群凑合用也没问题。

配置写完后,第一件事是在两个NameNode上分别执行格式化:

# 在第一个NN上格式化,会同时初始化JournalNode hdfs namenode -format -clusterId CID-xxx # 在第二个NN上同步元数据 hdfs namenode -bootstrapStandby # 初始化ZK上的HA状态(只做一次,在任意一台NN上) hdfs zkfc -formatZK

hdfs zkfc -formatZK这一步会创建/hadoop-ha/mycluster路径和ActiveBreadCrumb节点。这个命令只能执行一次,重复执行会报节点已存在,而且如果你在集群运行中执行,可能把现有的锁节点情况搞乱。真出问题了,先用deleteall /hadoop-ha/mycluster清掉再重新format,但前提是集群已经全部停下来。

第二个NN的-bootstrapStandby要求第一个NN已经启动,因为它要通过HTTP从第一个NN拉取元数据快照。

5.4 启动顺序与故障切换演练

完整的启动顺序是:ZK → JournalNode → NameNode(两台)→ ZKFC(两台)→ DataNode。ZKFC可以随NameNode一起用hdfs --daemon start zkfc单独启动,也可以由启动脚本一并拉起。

启动完成后验证:

# 两台NN上分别执行,看到 Active 和 Standby hdfs haadmin -getServiceState nn1 hdfs haadmin -getServiceState nn2 # 查看ZK上的锁节点,临时节点在Active那边 zkCli.sh -server node01:2181 ls /hadoop-ha/mycluster

现在做故障演练。最真实的方式是直接kill -9掉Active的NameNode进程:

# 在Active节点上 jps | grep NameNode kill -9 <namenode_pid>

然后盯着Standby的ZKFC日志:

tail -f $HADOOP_HOME/logs/hadoop-hadoop-zkfc-*.log

正常情况下几十秒内能看到日志依次出现:检测到锁节点被删除、尝试创建新锁、执行fencing、调用transitionToActive、成功。之后hdfs haadmin -getServiceState在新Active上返回active。

实操心得:演练时建议先把dfs.ha.fencing.methods临时配成shell(/bin/true),避免fencing失败卡住整个切换过程。等确认选主逻辑跑通了,再换成sshfence做一次完整验证。直接上sshfence一旦SSH不通,排查成本会高很多。

再补一个更狠的演练:直接把Active节点的ZKFCkill -9掉,NameNode进程不动。这时候ZK上的锁节点因为会话超时被删除,Standby那边会发起抢占,并且执行fencing——它会试图通过SSH去kill老NameNode。这个过程才真正考验fencing配置是否可用。如果fencing失败,新的主不会被提升,集群会处于"两个都是Standby"的尴尬状态,hdfs haadmin -getServiceState两边都返回standby。这时候手动执行hdfs haadmin -transitionToActive --forcemanual nn2可以救回来,但必须先把老的主真正停掉,否则就是人工制造脑裂。

6. 常见问题与排查实录

ZK参与的部分出问题,症状往往不在ZK本身,而在上层组件表现出的各种异常。下面这几类是我遇到过频率最高的。

6.1 会话超时与连接抖动

典型症状是日志里反复出现Session expired、ConnectionLoss、ZooKeeperServer not serving requests,上层表现为NameNode无缘无故从Active降级又升回去。

排查顺序建议固定下来:

  1. 先在ZK服务端看会话情况。echo cons | nc node01 2181能看到所有客户端连接,包括来源IP、session id、最后操作时间。
  2. 再看服务端的延迟指标。echo mntr | nc node01 2181输出里zk_avg_latency、zk_max_latency是关键,正常应该在个位数毫秒,如果zk_max_latency到了几百毫秒,说明ZK被慢请求拖住了。
  3. 检查ZK所在机器的负载。ZK对磁盘IO极其敏感,如果事务日志和快照写在同一块盘上,而且这块盘还被别的进程抢IO,写入延迟会直接反映成会话超时。
  4. 检查GC日志。ZK的堆一般配2-4GB就够,但如果有客户端注册了大量watch,每个watch都要占内存,堆压力会上去,Full GC一停就是几秒,会话直接断。

注意:ZK的事务日志一定要单独放一块盘,或者至少和数据目录分开。这个优化在测试环境看不出差别,生产环境能直接决定集群稳不稳。

还有一个容易被忽略的点:客户端的会话超时不要配得比服务端允许的上限还大。前面提过,服务端会静默截断,你以为配了60秒,实际生效40秒,于是ZKFC本地判定的切换时机和ZK的判定不一致,日志里会出现"我已经失去锁了但我还不知道"的错乱。

6.2 脑裂真的发生了怎么办

先明确一个判断标准:如果两个NameNode的hdfs haadmin -getServiceState都返回active,或者从DataNode的视角看两边都在接受心跳,那就是脑裂了。

现场处置的优先级是先止血再找原因:

  1. 立刻停掉其中一个NameNode,选哪个停?停掉那个"你更不确定"的。一般停掉后启动的、或者fencing没执行成功的那个。
  2. 检查两个NameNode的edits日志是否已经分叉。看/data/hadoop/name/current下的edits_*文件,如果两边都有各自的写入,说明已经分叉。
  3. 分叉的处理比较重:需要选一个作为基准,另一个用-bootstrapStandby重新同步。如果分叉期间有大量写入,可能还需要人工比对元数据。

预防比处置重要得多。检查清单是:dfs.ha.fencing.methods是否真的可用(SSH能否免密登录、fuser命令是否存在)、dfs.ha.automatic-failover.enabled是否开启、两个ZKFC进程是否都活着。三个ZKFC/NameNode相关进程要一起监控,任何一个静默退出都要立刻告警。

6.3 znode膨胀与jute.maxbuffer

HBase集群跑久了,/hbase/rs下的临时节点如果没有被正确清理,会积累大量僵尸节点。正常情况下RS下线时节点会消失,但如果ZK会话没断、RS进程变成了僵尸进程,节点就一直挂着。表现是HBase Master认为某个RS还活着,一直给它分配Region,而实际那台机器早就不干活了。

处理方式是人工delete掉这些节点,但删之前一定要确认对应的机器确实下线了,删错了会导致正在服务的RS被踢出集群。

另一个相关的问题是jute.maxbuffer。ZK单个znode的数据默认上限是1MB,客户端和服务端都要配这个参数,且必须一致。HBase在Region数量特别多的时候,某些znode(比如meta表的位置信息)可能接近这个上限,超过就会报Packet len is out of range。这个参数需要在zoo.cfg和客户端的JVM参数里同时设置:

# 服务端 zoo.cfg jute.maxbuffer=4194304 # 客户端,加到 HADOOP_OPTS 或 HBASE_OPTS -Djute.maxbuffer=4194304

6.4 排查命令速查表

把常用的四字命令和对应场景整理成表,出问题的时候照着敲,比翻文档快:

命令用途关注点
echo ruok | nc host 2181探活返回imok表示进程在
echo stat | nc host 2181角色与连接概览Mode、Connections 数量
echo mntr | nc host 2181详细指标延迟、znode数、watch数、连接数
echo cons | nc host 2181客户端连接明细找出异常来源IP
echo srvr | nc host 2181服务端概要版本、Zxid、模式
echo dump | nc host 2181会话与临时节点定位僵尸会话
echo wchs | nc host 2181watch 汇总watch数量异常说明客户端有问题

四字命令在3.5之后的版本需要显式加白名单,也就是zoo.cfg里的4lw.commands.whitelist。这个配置不加的话,所有四字命令都返回Command is not executed because it is not in the whitelist,第一次遇到会以为是网络问题。

7. 调优与长期运维的几个细节

7.1 时间参数怎么算

ZK的时间体系是围绕tickTime构建的,把这个算清楚,很多"为什么切换这么慢"的疑问就自动解开了。

服务端最大会话超时 =20 × tickTime,默认40秒。客户端请求的会话超时如果超过这个值,被截断到40秒。HDFS的ha.zookeeper.session-timeout.ms默认10000毫秒,在范围内,实际生效10秒。

但从主节点故障到备节点接管,实际耗时远不止10秒。完整的链路是:主节点进程挂掉 → 会话超时(最多10秒)→ ZK删除锁节点 → 备节点收到watch通知(毫秒级)→ 执行fencing(SSH连接+kill,5-30秒)→ 调用transitionToActive(加载edits,几秒)→ 对外服务。合计通常在30-60秒之间,fencing占了大头。

想让切换更快,能优化的地方是fencing方式。用sshfence的话可以缩短dfs.ha.fencing.ssh.connect-timeout,但不要低于10秒,否则网络稍有抖动就误判失败。有些团队用带外管理接口做fencing,速度快但依赖硬件支持,不是所有环境都有条件。

tickTime本身不建议随便改。调大能让集群对网络抖动更宽容,但代价是故障检测变慢;调小则相反。默认2000毫秒对同机房部署是合适的,跨机房才需要考虑调整。

7.2 磁盘、快照与清理

ZK的磁盘占用主要来自两块:事务日志(version-2/log.*)和快照(version-2/snapshot.*)。事务日志是顺序写的,快照是定期全量落盘。默认情况下两者都在dataDir下。

生产环境的做法是把dataLogDir单独指到另一块盘,让顺序写和随机写分开。配置很简单:

dataDir=/data/zookeeper/data dataLogDir=/data/zookeeper/datalog

清理策略必须配。autopurge.purgeInterval设为24(小时),autopurge.snapRetainCount设为5。这样每天清理一次,保留最近5份快照和对应的日志。不配的话,日志会一直增长,几个月后磁盘告警,而这时候你不敢随便删——删错了可能影响恢复。

还有一种做法是用外部定时任务做清理,但既然ZK自带了这个能力,没必要自己造。唯一要注意的是purgeInterval不要设得太小(比如1小时),频繁的文件操作反而增加IO压力。

7.3 该监控哪些指标

监控做得好,前面那些故障大半能在爆发前发现。ZK这边我关注这几个指标:

指标含义告警阈值建议
zk_avg_latency平均请求延迟持续 > 50ms 关注
zk_max_latency最大请求延迟出现 > 500ms 排查
zk_outstanding_requests积压请求数持续 > 10 说明处理不过来
zk_num_alive_connections活跃连接数接近maxClientCnxns时扩容
zk_znode_count节点总数突增说明有组件异常注册
zk_watch_countwatch 总数异常增长说明客户端泄漏

zk_outstanding_requests这个指标特别有诊断价值。它反映的是排队等待处理的请求数,正常情况下应该是0或个位数。如果持续增长,说明ZK的处理能力跟不上请求速度,这时候要么是客户端请求太频繁(比如某个组件在死循环里重连),要么是ZK的磁盘或CPU出了问题。

上层组件的监控也要跟上。HDFS这边监控getServiceState的结果变化,任何一次非预期的切换都要告警;YARN这边监控RM的健康状态和ZKRMStateStore的写入延迟;HBase这边监控/hbase/rs下的节点数量和实际RS数量是否一致。这三类指标配齐,ZK相关的故障基本都能在用户感知之前被发现。

我在实际运维里的体会是,ZK这套东西最大的特点就是"平时不出声,出事就是大事"。它平时负载低到你几乎忘了它存在,但一旦它的会话机制、磁盘IO或者时间同步出了问题,上面挂着的HDFS、YARN、HBase会同时抖起来,排查的时候很容易被上层组件的各种报错带偏。所以我现在养成一个习惯:任何集群出现"莫名其妙的切换"或者"周期性抖动",第一反应不是去看NameNode的日志,而是先echo mntr | nc一下ZK,看看延迟和积压请求数正不正常。这个动作花不了十秒,能省掉很多弯路。另外就是别把ZK当成一个装完就不用管的组件,它的autopurge、dataLogDir、会话超时上限这几个配置,在一开始就配好,后面能少掉一大半的运维麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询