Hadoop高可用架构原理与实战:NameNode故障恢复与集群切换
2026/9/9 17:14:25 网站建设 项目流程

凌晨三点被监控电话叫醒,这种事干过大数据的都懂。那一晚我值班,HDFS的NameNode挂了,整个数据平台停了将近四十分钟。调度任务全部卡死,实时写入断流,报表查询一片超时,业务方的电话一个接一个打过来。当时集群没有配置高可用(HA),NameNode一死,全集群瘫痪,只能手动恢复元数据,等fsimage和edits日志重建完成。那一晚上之后,我花了整整两周时间把集群的HA架构重新搭了一遍,从HDFS到YARN全部做了高可用改造,期间把各种坑基本踩了个遍。这篇文章就是把那段实践整理出来,讲清楚Hadoop高可用架构设计背后的原理、配置步骤和故障排查方法,给正准备上生产集群、或者正在准备大数据架构面试的朋友做一个参考。

1. 高可用到底在解决什么问题

1.1 先搞清楚:HA保护的是“状态”,不是“节点”

很多初学者对高可用的理解停留在“多部署一台机器,挂了就切换”,但真正做架构设计的时候你会发现,问题远没有这么简单。Hadoop早期版本的HDFS只有一个NameNode,它维护整个文件系统的目录树和文件与数据块的映射关系,所有元数据都存放在内存里,相当于整个集群的“大脑”。这个大脑一旦宕机,集群里其余上千个DataNode都还在,但客户端根本不知道文件被切成了多少块、每一块存在哪台机器上,整个HDFS就变成了一堆散落的硬盘。

传统的恢复手段是依赖SecondaryNameNode。这个名字太有误导性了,它根本不是NameNode的备份节点,它的作用只是定期从NameNode拉取fsimage和edits日志进行合并,帮NameNode减轻压力。一旦NameNode真挂了,你只能把SecondaryNameNode上的元数据拷贝回来,加上NameNode本地残留的edits日志,手工合并再重启。这个过程短则十几分钟,长则一两个小时,期间整个集群对外不可用。更麻烦的是,如果NameNode机器磁盘损坏,连本地日志都丢了,那恢复起来就是噩梦。

所以HA要解决的核心问题,不是“再找一台机器顶上”,而是“如何把NameNode的状态实时地、一致地同步到另一台机器上”。状态是什么?就是文件系统元数据。只有备节点拿到完整且一致的状态,才有资格在主节点故障时无缝接管。这里有两个关键词:完整、一致。完整意味着不能只同步fsimage,还必须同步从上次checkpoint以来的所有操作记录;一致意味着两边看到的文件系统视图必须完全一样,不能出现主节点写了文件A、备节点不知道的情况。为了做到这一点,Hadoop引入了JournalNode,也就是QJM(Quorum Journal Manager,仲裁日志管理器)方案,后面我会详细拆解这个机制。

生活里有个很好的类比:可以把NameNode理解成一本日记,每天都在记录文件系统里发生的所有事情。HA要做的事情就是,你每写一行,旁边那个人立刻照着抄一行。平时他就在那里等着,你一旦出事,他拿起那本抄好的日记就能继续写。但这里有个关键问题:怎么保证同一时间只有一个人在写日记?如果两个人都以为自己是“正主”,同时往日记里写内容,那整个系统就全乱了,这个现象在分布式系统里叫“脑裂”,HA设计里绕不开它,也是面试官最爱问的点。

1.2 架构里都有谁在干活

一个完整可用的Hadoop HA集群,涉及的角色远不止两个NameNode。我用一张表把参与方列清楚,这样后面看配置的时候不会迷路。

角色数量建议职责挂了会怎样
Active NameNode1对外提供元数据读写服务,写edits日志触发自动切换
Standby NameNode1持续同步edits日志,内存中维护元数据,随时准备接管不影响,尽快修复即可
JournalNode3或5存储edits日志,通过QJM协议保证多数派写入少于多数派时,NameNode写edits会失败
Zookeeper3或5分布式协调,保存锁节点,参与选主多数派存活即可,挂少数不影响
ZKFC(DFSZKFailoverController)每个NameNode节点各1个监控NameNode健康状态,维护ZK锁,执行fencing所在节点的主NameNode无法自动切换
ResourceManager2YARN资源调度,Active/Standby模式触发RM自动切换
DataNode / NodeManager多个存储数据块、执行计算任务各自有容错机制,不直接影响HA

注意,ZKFC虽然名字里带“ZK”,但它是HDFS自带的小进程,不是Zookeeper服务端的进程。它部署在NameNode所在的物理机上,负责做健康检查和故障转移。很多人在部署的时候会漏掉ZKFC,导致NameNode挂了之后完全不触发自动切换,这个坑后面会细说。

1.3 结合业务场景,选哪种HA方案

HA方案不是越复杂越好,要结合集群规模、业务重要程度和运维成本来选。如果你只是搭一套测试环境学习Hadoop,那就没必要上HA,一台NameNode加一个SecondaryNameNode就够了,省心省事。但如果集群要承载生产任务,哪怕只是几十个节点的中小集群,我都建议至少把双NameNode+QJM的HA做上去,因为停机代价远大于配置成本。

关于YARN的HA,很多团队会忽略。他们觉得HDFS做HA就够了,YARN挂了重启一下就行。但如果你跑的是离线批处理任务,RM挂了会导致所有正在运行的任务状态丢失,作业要么失败要么需要手动重试,同样会造成大范围影响。所以在生产环境里,我建议HDFS和YARN的高可用一起配。已经有不少线上事故是HDFS好好的,RM挂了,整个调度层瘫痪,排查半天才发现RM没有配HA。

另外还有一个方案叫HDFS Federation(联邦),它是为了解决NameNode内存瓶颈问题,把文件系统目录切分成多个命名空间,每个命名空间由独立的NameNode管理。联邦和HA是可以叠加使用的,比如两个NameNode组成一个nameservice做HA,另两个NameNode组成另一个nameservice做HA。如果你的集群规模到了几亿甚至几十亿文件的级别,单NameNode内存吃紧,就得考虑联邦方案。但联邦会引入跨命名空间数据迁移的问题,架构复杂度会明显上升,一般中小公司用不到,这里就不展开细讲了。

2. 核心机制拆解:NameNode与ResourceManager如何实现无缝切换

2.1 NameNode HA的底座:QJM共享edits日志

NameNode内存里的文件系统元数据是存在内存中的,但所有修改操作都会先追加写入edits日志。HA架构里,这些edits日志不再写到NameNode本地磁盘,而是通过QJM协议发给一组JournalNode。Active NameNode向所有JournalNode并发写同一批edits,只要大多数(超过一半)JournalNode返回写入成功,就认为这次写操作已经提交。

这里面的逻辑跟Raft协议很像,核心思想就是“多数派”。假设有3个JournalNode,最多允许1个故障;如果有5个,最多允许2个故障。JournalNode越多,容错能力越强,但同步延迟也会有所增加,所以生产环境最常见的选择是3个或者5个,很少用4个,因为4个跟3个的容错能力一样(都只能坏1个),成本还更高。

Standby NameNode做的事情是什么呢?它持续从JournalNode读取edits日志,回放到自己的内存中,让自己的元数据状态无限接近Active节点。这里有一个很多教程没讲清楚的细节:Standby节点并不是等Active完全写完才去读,而是做“无缝跟踪”。它维护一个transaction ID的游标,从JournalNode上不断拉取新产生的edits并应用,因此正常情况下Standby的内存状态与Active的差距非常小。当Active故障时,Standby只需要把最后几条没有同步完的edits补上,就能接管服务。这个“差距”就决定了HA的RPO(恢复点目标)——理论上最多丢失几条尚未同步的edits,但在多数派机制下,Active写入成功的那部分edits一定已经存在于多数JournalNode上,所以Standby一定可以读到,实际场景下几乎可以做到零丢失。

为什么不用共享存储方案?这也是很多刚接触HA的人会问的。早期Hadoop确实支持通过NFS或者共享磁盘来做共享edits目录,我在测试环境也试过。NFS的问题在于它本身是一个单点,NFS服务器挂了,两个NameNode都写不了edits,整个集群等于瘫痪,HA变成了单点故障的放大器。另外,NFS在高并发写入下延迟不稳定,容易成为性能瓶颈,而且没法做多副本容错。QJM方案用普通的本地磁盘就能组,一套JournalNode集群本身就是多副本的,故障域被分散到多台机器上,这才是真正的“高可用”。

2.2 自动切换的真正执行者:ZKFC与Fencing

两个NameNode,一个Active一个Standby,谁来决定什么时候切换?答案是ZKFC,它跑在每个NameNode所在的机器上,是独立于NameNode的Java进程。总结下来它干三件事。

第一,健康监控。ZKFC定期向本机的NameNode发送健康检查命令,如果NameNode进程异常、JVM频繁Full GC导致长时间无响应,或者机器本身出现故障,ZKFC就会判定NameNode不健康。

第二,Zookeeper选主。正常工作时,Active节点的ZKFC会在Zookeeper里持有一个锁节点(类似临时节点),Standby节点的ZKFC一直在尝试获取这个锁。一旦Active节点故障,它的ZKFC进程也会跟着死掉或主动释放锁节点,Zookeeper检测到临时节点消失后,Standby节点的ZKFC就能立刻抢到锁,完成“夺权”。

第三,执行fencing(隔离)。这一步是防止脑裂的关键。假设Active节点不是真的宕机,而是卡死了,但进程还在,网络闪断导致ZKFC和Zookeeper之间失去连接。此时Zookeeper判定锁超时,把锁释放了,Standby节点抢到锁准备接管。但如果Active节点恢复了,它并不知道自己已经被“下台”,还在继续写edits,这时就有两台机器同时以Active自居,数据就会错乱。Fencing的目的就是在Standby接管前,把旧Active节点彻底“干掉”,手段包括:通过SSH登录旧Active机器杀掉NameNode进程、调用指定的隔离脚本、或者通过共享存储的锁机制强制旧节点退出。只有确保旧Active不再工作,新Active才能安全接管。

这个机制说明了为什么自动切换不能只靠“心跳”判断——正因为网络分区是分布式系统最棘手的问题,所以才需要用一个外部仲裁者(Zookeeper)加上强制的隔离动作(Fencing)来保证“同一时刻只有一个主人”。

2.3 ResourceManager HA与任务恢复

YARN的ResourceManager是集群资源调度的大脑,管理着所有NodeManager的资源报告、ApplicationMaster的注册信息、以及各种调度器状态。要对RM做高可用,思路和NameNode类似,也是Active/Standby模式,但存储状态的方式有点不同,它不依赖JournalNode,而是通过RMStateStore把状态保存到Zookeeper或者LevelDB中。

YARN HA的关键配置意图是这样的:启用RM的自动故障转移后,两个RM同时启动,但只有一个处于Active状态。当Active RM故障时,Standby RM通过Zookeeper竞选成为Active,然后从RMStateStore恢复状态。注意,这个“恢复”是有限度的,它恢复的是应用的基本信息和计数器,而不是每一个任务的执行细节。那些正在运行的ApplicationMaster会向新的RM重新注册,如果ApplicationMaster本身也丢了任务进度,那就需要上层的MapReduce或者Spark框架自己去做任务重放。

这里有个容易被忽略的点:RM故障切换期间,已经提交但还没被调度执行的作业会短暂地“卡住”,等新RM就绪后重新调度;正在执行的任务可能会因为AM重新注册而出现短暂停顿,但一般不会直接失败。所以YARN HA能做到的,是让RM故障不导致整个集群的调度能力长时间中断,而不是让所有任务无缝无感。设计高可用架构的时候,要对业务方讲清楚这个预期,不然会被人误以为RM挂了之后作业完全不受影响。

2.4 存储与计算节点的高可用细节

NameNode和ResourceManager的HA解决的是“大脑”高可用,那么被管理的DataNode和NodeManager挂掉怎么办?这两个角色本身不存“状态”,容错方式就是靠冗余和重新调度。

DataNode通过心跳向NameNode上报自己持有的数据块信息。如果一个DataNode超过10分钟没有心跳(参数是dfs.namenode.heartbeat.recheck-interval和心跳超时时间配合计算),NameNode就会判定该节点宕机,然后检查它上面的块副本数是否低于配置的副本因子。默认副本数是3,如果少了,NameNode会调度其他活跃的DataNode从剩余副本中复制数据,补足副本数量。这个复制过程是后台自动完成的,不需要人工介入,但会消耗一定的网络和磁盘IO,所以大节点宕机后集群往往会有一段时间负载升高。

NodeManager挂了的逻辑类似,但它管理的是计算资源。NodeManager失联后,ResourceManager会把它上面的所有Container标记为失败,然后通知对应的ApplicationMaster重新申请资源、重跑任务。这里需要提醒的是,如果你的任务没有做checkpoint或者不支持失败重试,NodeManager一挂,任务还是会失败。所以严格意义上讲,计算节点的高可用不完全靠YARN本身,还得靠上层任务框架和任务设计来兜底。

还有一个经常被架构师忽略的层面:机架感知。如果你把三个副本放在同一个机架的同一台交换机下面,那交换机断电或者光模块故障,三个副本同时丢失,无论HDFS怎么自愈都救不回来。所以生产环境一定要配置机架感知脚本,让Hadoop了解网络拓扑。副本放置策略默认是“第一个副本在客户端本地机架、第二个副本在同机架不同节点、第三个副本跨机架”,这样任何一个机架级故障,都不会导致数据全部丢失。配置机架感知的成本很低,就是一个脚本加上拓扑信息的映射,但收益是实实在在的故障域隔离,这是HA架构在“数据冗余”层面最重要的一环。

3. 实操:从零搭建一套双NameNode HA集群

3.1 节点规划与资源估算

我平时指导团队搭集群,最常遇到的第一个问题就是“我该准备几台机器”。这里我给出一个标准的生产环境小集群规划,一共5台,可以承载中等规模的离线数据处理任务。

主机名IP规划部署角色
nn110.0.0.11NameNode(Active), ZKFC, ResourceManager(Active), Zookeeper
nn210.0.0.12NameNode(Standby), ZKFC, ResourceManager(Standby), Zookeeper
jn110.0.0.13JournalNode, Zookeeper, DataNode, NodeManager
dn110.0.0.14DataNode, NodeManager
dn210.0.0.15DataNode, NodeManager

注意,上表里nn1和nn2同时也是Zookeeper节点,这没问题,但Zookeeper的数据目录最好放在独立的磁盘分区上,不要和NameNode元数据目录挤在一起。JournalNode建议放在独立的机器上,至少不要和NameNode共用同一块物理磁盘,否则NameNode的磁盘故障同样会拖垮JournalNode,失去故障域隔离的意义。如果机器数量不够,退而求其次把JournalNode放在DataNode节点的独立目录上,也比全部堆在主节点要稳妥。

资源估算方面,NameNode的JVM堆内存是一个核心参数。经验上,每100万个元数据文件/块对象,大约需要1GB到2GB的堆内存。一个5000万文件的中等集群,建议堆内存给到16GB到32GB。但注意JVM堆超过32GB后,对象指针压缩会失效,GC停顿会明显变大,所以单个NameNode的堆尽量控制在32GB以内,文件数再多就考虑联邦方案了。JournalNode默认堆内存1GB够用,edits日志的写入是磁盘IO密集型的,内存需求不高。Zookeeper的堆内存默认1GB通常够用,但如果客户端连接数特别多,可以提到2GB到4GB,前提是不要随便改它的默认GC配置,容易出现奇怪的性能问题。

磁盘规划上,DataNode多块盘建议直接用JBOD(Just a Bunch Of Disks)模式,不要做RAID。这个很多初学的人想不通:磁盘坏了怎么办?答案是HDFS本身有副本机制,不需要RAID来提供冗余。RAID5在HDFS场景下反而会降低磁盘利用率,重建时间也长,完全是花冤枉钱。NameNode的元数据目录建议用RAID1或者交给云盘做冗余,因为元数据的可靠性比性能更重要。

3.2 环境准备与Zookeeper集群搭建

开始配Hadoop之前,基础环境要处理好,这部分不能省,否则后面排查问题会排查到怀疑人生。所有节点统一时间同步,我用chrony就够了,误差控制在毫秒级。配置各节点的hostname和/etc/hosts,确保节点之间通过主机名互相解析。Hadoop各组件之间大量使用主机名通信,如果解析有问题,会出现各种诡异的超时报错。JDK用1.8,同时配置好JAVA_HOME环境变量。

配置SSH免密登录,注意需要配置的是nn1到nn2、nn1到各JournalNode和DataNode的免密,因为NameNode启动时要通过SSH远程启动其他节点的守护进程。很多教程直接让你配所有节点全互通,那是图省事,但生产环境里最小化授权才是更安全的做法。

接下来搭Zookeeper,下载解压后配置conf/zoo.cfg,核心参数如下:

tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper/data dataLogDir=/data/zookeeper/logs clientPort=2181 server.1=nn1:2888:3888 server.2=nn2:2888:3888 server.3=jn1:2888:3888

有几个细节必须注意。dataDir和dataLogDir要分开,日志盘故障时不会影响数据文件。server.X里的X对应的是每台机器上myid文件的内容,myid文件要放在dataDir目录下,这个不要配错,配错了Zookeeper会反复找leader找不到。端口规划上,2181是客户端端口,2888是集群内部通信端口,3888是选举端口,防火墙要全部放通。启动后使用zkServer.sh status检查,正常情况下三个节点里一个leader两个follower。

3.3 HDFS HA关键配置与启动顺序

配置HDFS HA是整个过程的重点,我直接给出核心的配置片段,并逐项说明为什么这么配。

core-site.xml的配置:

<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://mycluster</value> </property> <property> <name>ha.zookeeper.quorum</name> <value>nn1:2181,nn2:2181,jn1:2181</value> </property> </configuration>

第一个配置把默认文件系统设置为一个逻辑名称mycluster,这个名称是我们在hdfs-site.xml里定义的nameservice。第二个配置告诉Hadoop集群的Zookeeper地址列表,ZKFC和自动故障转移都靠它。

hdfs-site.xml的配置比较多,我挑了最关键的几项:

<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>nn1:8020</value> </property> <property> <name>dfs.namenode.rpc-address.mycluster.nn2</name> <value>nn2:8020</value> </property> <property> <name>dfs.namenode.http-address.mycluster.nn1</name> <value>nn1:9870</value> </property> <property> <name>dfs.namenode.http-address.mycluster.nn2</name> <value>nn2:9870</value> </property> <property> <name>dfs.namenode.shared.edits.dir</name> <value>qjournal://nn1:8485;nn2:8485;jn1:8485/mycluster</value> </property> <property> <name>dfs.client.failover.proxy.provider.mycluster</name> <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value> </property> <property> <name>dfs.ha.automatic-failover.enabled</name> <value>true</value> </property> <property> <name>dfs.journalnode.edits.dir</name> <value>/data/hadoop/journal</value> </property> </configuration>

这些配置里,最容易被配错的是qjournal地址的格式。它的结构是qjournal://jn1:8485;jn2:8485;jn3:8485/nameserviceId,注意中间用分号分隔,最后跟的是nameservice名称,必须和dfs.nameservices保持一致。dfs.journalnode.edits.dir是JournalNode本地存储edits日志的路径,这个路径在三个JournalNode上都要创建,并且要放在可靠磁盘上。

配置完成后,首次启动的顺序相当有讲究,我列出标准流程:

  1. 在所有JournalNode节点上先启动journalnode进程:hdfs --daemon start journalnode
  2. 在nn1上执行格式化命令:hdfs namenode -format,这一步生成初始的命名空间和fsimage
  3. 由于是首次部署,需要把nn1的元数据同步到nn2,在nn2上执行:hdfs namenode -bootstrapStandby,它会自动从nn1拉取最新的元数据
  4. 如果启用了自动故障转移,还需要初始化Zookeeper中的HA状态:hdfs zkfc -formatZK,这一步本质是往ZK里创建HA相关的znode
  5. 在nn1上执行hdfs --daemon start namenode启动Active节点,然后在nn2上执行同样的命令启动Standby节点
  6. 分别启动两个节点的ZKFC进程:hdfs --daemon start zkfc

启动完成后,用hdfs haadmin -getAllServiceState查看两个NameNode的状态,正常情况下应该一个是active一个是standby。然后SSH到nn1执行kill -9杀掉NameNode进程模拟故障,观察约10到20秒后,nn2应该自动切换成active。这里能切换成功,说明ZKFC、Zookeeper和fencing整体链路都是通的,集群HA基本就绪。

整个过程中最容易出的问题有两个。第一个是在格式化的时候先后对两台机器执行了hdfs namenode -format,导致两个NameNode的namespaceID不一致,它们连到同一组JournalNode时就会出现严重冲突。解决方式只能是把所有元数据和JournalNode的数据清空,重新走一遍整个格式化流程,非常痛苦。第二个是启动顺序不对,你先启动了NameNode再启动JournalNode,NameNode启动时会因为连不上JournalNode而失败,或者初始化共享edits目录失败。切记首次搭建时先起JournalNode,再起NameNode。

3.4 YARN HA配置与切换演练

HDFS的HA搞定之后,YARN HA就相对简单了,都是依赖Zookeeper做选主和状态存储。先看yarn-site.xml里的核心配置:

<property> <name>yarn.resourcemanager.ha.enabled</name> <value>true</value> </property> <property> <name>yarn.resourcemanager.cluster-id</name> <value>my-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>nn1</value> </property> <property> <name>yarn.resourcemanager.hostname.rm2</name> <value>nn2</value> </property> <property> <name>yarn.resourcemanager.zk-address</name> <value>nn1:2181,nn2:2181,jn1: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> <property> <name>yarn.resourcemanager.ha.automatic-failover.enabled</name> <value>true</value> </property> <property> <name>yarn.resourcemanager.ha.automatic-failover.embedded</name> <value>true</value> </property> </configuration>

这里有两个容易被忽略的点。第一个是yarn.resourcemanager.recovery.enabled必须配置为true,否则RM的Standby即使抢到了主控权,也无法从状态存储里恢复应用信息,切换后所有应用的状态都会丢失。第二个是automatic-failover.embedded这个参数,它默认就是true,代表以嵌入式的方式在RM进程内部跑一个选主客户端,不需要像HDFS那样单独启动ZKFC进程。很多人会误以为YARN也要单独启动一个类似ZKFC的进程,其实不需要,RM进程内部已经把这个事情做掉了。

启动方式很简单,在nn1上执行yarn --daemon start resourcemanager,在nn2上执行同样的命令。然后通过yarn rmadmin -getServiceState rm1查看状态。想让RM切换,可以直接杀掉Active RM进程,观察Standby RM是否自动变成active。切换完成后,新Active RM会从Zookeeper里恢复之前提交的应用状态,正在运行的任务会经历短暂的“失联”然后恢复。

3.5 集群扩容与均衡的实操经验

集群上线运行半年后,存储容量不够了,要给HDFS扩容时,操作上并不复杂,但有几个隐藏的坑。新节点配置好Hadoop和JDK,把core-site.xml、hdfs-site.xml、yarn-site.xml等配置文件从旧节点拷贝过来,确保完全一致,然后启动DataNode和NodeManager。启动后执行hdfs dfsadmin -report,如果新节点的Capacity正常显示,说明它已经被NameNode纳管。

接下来一个常见问题是:新节点加入后,数据并不会自动往它上面迁移,所有新写入的数据可能会继续落在旧节点上,导致新节点磁盘紧张、旧节点磁盘宽松,分布严重不均衡。这时需要手动执行均衡命令:

hdfs balancer -threshold 10 -bandwidth 104857600

threshold参数表示允许的磁盘使用率偏差百分比,bandwidth是均衡过程允许占用的网络带宽,单位是字节每秒。上面这个例子就是允许偏差10%,带宽最多100MB/s。我一般建议在业务低峰期跑均衡任务,因为balancer本质上是在节点之间移动数据块,会占用大量网络和磁盘IO,跑得太猛会把在线业务的延迟打上去。

这里还有一个更为隐蔽但经常在面试中被问到的点:扩容一个新节点后,如果没配置机架感知就启动DataNode,新节点的副本放置将不遵循多机架策略,三个副本可能全部落在同一个机架上。一旦这个机架发生物理故障,数据就全丢了。所以先配置好机架感知再扩容,这个顺序不要反过来。

4. 常见问题与故障排查实录

4.1 高频故障速查表

下面这张表是我这几年在HA集群运维里遇到的高频问题,整理成速查表,可以直接收藏起来。

故障现象可能原因排查命令与手段
Active NameNode频繁切换网络抖动、ZKFC健康检查超时、NameNode Full GC过长看ZKFC日志,检查GC时间,ping对端节点
自动failover没有生效ZKFC进程没启动、ZK锁节点异常、fencing脚本失败hdfs haadmin -getAllServiceState查看,检查ZK节点
Standby一直处于standby但数据不同步JournalNode的edits目录损坏或磁盘满查看JournalNode日志,检查journal目录磁盘空间
客户端报NotActiveException客户端连接的NameNode不是当前Active检查客户端failover proxy provider配置
格式化后两个NameNode无法组集群二次format导致namespaceID不一致确认日志中的namespaceID,必要时清空数据重建
JournalNode磁盘写满edits日志持续增长、没有定期清理监控磁盘,扩展磁盘或迁移日志目录
jar does not exist或is not a normal file配置的依赖路径错误或workers节点未分发jar包检查yarn.application.classpath和相关环境变量

4.2 案例一:NameNode宕机后自动切换失败

这个案例值得单独展开,因为它是最典型的“配置看着都对,就是切换不了”的问题。有一次测试环境里,我手动kill掉Active NameNode后,等了很久Standby都没有接管。排查过程是这样的。

先看Standby节点的ZKFC日志,发现它一直在报“unable to get the lock”,说明Standby侧的ZKFC根本没有抢到Zookeeper里的锁。然后我检查Zookeeper的节点状态,发现锁节点依然存在。为什么Active进程都被kill了,锁节点还在?原因是ZKFC进程和NameNode是独立进程,我只kill了NameNode,ZKFC还活着,它持有的临时锁节点就没有被释放,Standby自然抢不到。

这个案例说明一个关键点:HA的故障切换针对的是“整个节点的故障”,而不是“单个进程的故障”。如果NameNode进程挂了但ZKFC进程还活着,ZKFC会检测到NameNode不健康,然后主动释放锁并触发切换。但如果ZKFC自己也没了,Zookeeper需要等待临时节点超时才能释放锁,这个超时时间默认是10秒左右,期间集群处于不可用状态。所以每次演练时,最贴近真实故障的做法是直接断掉整台机器的电源,或者执行kill -9杀掉这台机器上的所有相关进程,而不是只杀NameNode。

4.3 案例二:日志报“jar does not exist or is not a normal file”

这个报错看起来很低级,但在实际运维里经常出现,特别是在用脚本批量部署或者升级Hadoop版本之后。具体情况是:你提交一个MapReduce作业,YARN在启动容器的时候去加载某个jar包,报错说路径不对。根本原因一般有三类。

第一类是Hadoop自身的依赖路径变了,比如你升级了Hadoop版本,但yarn.application.classpath还写的是旧路径。检查yarn-site.xml里是否配置了classpath,或者是否依赖yarn-env.sh里自动生成的路径,确保新版本的所有jar目录都被正确包含。第二类是自定义的依赖jar包没有分发到所有NodeManager节点上,只在提交作业的那台机器上存在。这种情况在测试环境很常见,本地跑没问题,提交到集群就报jar不存在,本质上就是jar包没有通过-libjars参数或者分布式缓存分发到各个节点。第三类是配置的路径本身拼错了,少了一层目录或者文件名大小写不对,这种问题用ls命令验证一下就行了。

排查这个报错的思路是:不要只看报错那台机器的路径,要全局检查所有NodeManager节点上该路径是否存在。因为YARN的容器可能在任意一个NodeManager上启动,jar包只要缺一个节点,作业就会随机失败。

4.4 案例三:扩容后数据不均衡

扩容之后新DataNode磁盘是空的,但写入的数据仍然集中在旧节点上,这算正常现象。HDFS默认不会因为新节点加入就自动重新分布数据,必须手动执行balancer。这里有一个容易漏掉的细节:balancer在移动副本时,会优先考虑“跨机架”的移动,如果你的机架感知配置错误,balancer可能反复移动数据但始终达不到均衡状态,日志里会出现大量的Data is not being moved之类的警告。遇到这种情况,优先检查拓扑脚本的输出是否符合预期。

另外,如果集群长期不写数据,只是存储静态文件,balancer移动完一轮后再也不会触发,新增节点依然会被闲置。针对这种情况,可以配置dfs.datanode.balance.bandwidthPerSec来控制均衡速度,或者干脆把静态数据重新写一遍,强制触发副本重新分布。不过手工重写数据有风险,操作前务必确认业务可以接受短暂的写入中断。

4.5 日常巡检与监控建议

HA架构运行是否正常,不能等故障了才知道。我梳理了一个简单的巡检清单,每周做一遍,能提前发现大部分隐患。

检查所有NameNode和RM的状态,确认主备角色符合预期,不能出现双Active或者双Standby的情况。用hdfs haadmin -getAllServiceStateyarn rmadmin -getServiceState就能看到。检查JournalNode的磁盘空间,edits日志是持续增长的,磁盘满会导致NameNode无法提交元数据操作,这是高危故障,必须提前设置磁盘使用率告警。检查Zookeeper集群状态,用zkServer.sh status确认leader存在、各节点都在正常参与选举。日常监控上,重点盯NameNode的JVM堆内存使用、GC时间、JournalNode的同步延迟transactions、以及自动切换是否被意外触发过。

踩过很多次坑之后,我最大的体会是:HA切换链路里的每个环节都可以单独做测试,ZKFC的健康检查、Zookeeper的锁竞争、JournalNode的日志同步、Fencing脚本的执行,任何一个环节出问题,整个自动切换都会失效。所以新集群上线前,无论如何都要做至少两次完整的故障演练,一次模拟单节点宕机,一次模拟网络分区,确认系统的表现符合预期,再交付给业务方使用。

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

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

立即咨询