前阵子在一个项目里搭了一套Hadoop HA高可用集群,从角色规划、ZooKeeper部署、HDFS配置一路调下来,中间踩了不少坑,也总结出一些判断问题的思路。这篇记录就把整个搭建过程和关键细节完整捋一遍,给同样要上Hadoop HA的朋友做个参考。
这篇东西适合两类人看:一类是刚接触Hadoop、想搞明白HA到底在解决什么问题的初学者;另一类是要真正在生产环境落地的工程师。我会尽量把每个配置参数背后的原因也讲清楚,这样你看到的不只是一堆配置命令,而是知道为什么要这么配、出问题该往哪个方向查。
1. 先弄明白HA到底在解决什么问题
1.1 单点不是小事:NameNode挂了会发生什么
在讲HA之前,得先把基础痛点聊透。HDFS的NameNode负责维护整个文件系统的元数据,包括目录树、文件块位置、租约等等。客户端读写文件之前,首先要把这些元数据拿到手,DataNode上报心跳和块报告也都要先经过NameNode。换句话说,NameNode就是整个HDFS的“总闸门”。
没有HA的年代,NameNode挂了,整个HDFS就停摆了。而且最难受的还不是“读写停止”,而是元数据恢复的难度。NameNode把元数据持久化到本地磁盘的fsimage和edits文件里,如果节点物理损坏或者磁盘故障,这些文件没了,那整个命名空间就丢了。这意味着所有文件在HDFS里还存在,但已经不知道它们存哪里了,基本等同数据报废。
后来有人想到“冷备”方案:定期把NameNode的元数据拷贝到另一台机器上。听起来简单,实际很难做。NameNode的元数据是持续变化的,每一次建目录、删文件、写块都要写edits日志,冷备只能按周期同步,周期内的元数据变化很大概率丢失。而且冷备的切换还靠手工操作,恢复时间完全不可控。
Hadoop HA解决的就是这个核心问题:让两个NameNode机器热备运行,一台Active负责提供读写服务,另一台Standby保持元数据实时同步。Active出问题时,Standby能自动接管,把服务中断时间压缩到几十秒甚至几秒内。
1.2 三个组件各司其职:JournalNode、ZKFC、Fencing
HA能跑起来,依赖的不是一个技术,而是一个组合机制。我习惯把HA拆成三块来理解:状态同步、故障探测与选举、原Active的隔离。
状态同步这一块,靠的是JournalNode。JournalNode有点像几个人合用的“工作日志本”:Active NameNode每做一次命名空间变更,都要往这本日志本上写一条;Standby NameNode则实时盯着这本日志本,读到新记录后立刻在自己的内存里重放,让自己的元数据状态尽量贴着Active走。JournalNode本身是一个独立进程,生产环境一般部署奇数台,因为状态同步只要多数派写成功就算数,这是一个典型的Quorum机制。
故障探测与选举这一块,靠的是ZKFC(ZooKeeper Failover Controller)。ZKFC是跑在NameNode机器上的一个小进程,它的工作逻辑很清晰:在ZooKeeper上创建一个临时节点,Active NameNode的ZKFC持有这个节点,Standby NameNode的ZKFC则在旁边监听。一旦Active的ZKFC和ZooKeeper之间的会话超时,临时节点自动消失,Standby的ZKFC立刻竞争创建这个节点,谁抢到了谁就升级成Active。
Fencing(隔离)这一块很多人容易忽略,但其实是最要命的一环。想象一个场景:Active NameNode没死,只是和ZooKeeper之间的网络断了,ZKFC判定它“失联”然后让Standby升为主。这时候如果两个NameNode同时对外服务,就会出现“双脑”状态,也就是常说的脑裂,两个节点同时写edits日志,元数据很快分叉,整个文件系统一致性直接毁掉。Fencing要做的事情,就是在Standby接管之前,用手段把旧Active“按住”,保证它不能再写。
这三块缺一不可。只做状态同步没有自动选举,退化成了手工冷备;只做选举不做fencing,危险系数比单NameNode还高。
1.3 脑裂到底有多可怕
说到脑裂,多说几句。之前我有一个同事对fencing理解不深,觉得“反正两个名字节点,谁状态好谁上不就行了”,结果在测试环境模拟网络抖动时,两个NameNode都尝试写JournalNode,edits日志的epoch直接错乱,后面查日志的时候发现txid都对不上,整个命名空间已经没法继续重放了。
后来我们把fencing配置补上,再模拟网络分区,系统会先等旧Active被隔离成功,Standby才敢真正接管。这个顺序很重要:不是“先切换再隔离”,而是“先隔离再切换”。所以生产环境里,fencing策略的可靠性直接影响切换的时间和安全。
现在好一点的HA实践,其实还会在Fencing之外再加一层保障:JournalNode每条edits记录都带着epoch号,旧的Active即使没被杀干净,它写记录时用的epoch已经过期,会被JournalNode拒掉。这算是有个双保险,但fencing仍然是首道防线。
2. 动手前的环境规划与版本选型
2.1 节点角色怎么分布:最少几台机器
不少新手一上来就问“HA是不是两台机器就够了”,答案是:两台的HA属于勉强跑demo,生产环境别这么干。原因很直接:JournalNode至少要3台才能构成多数派,如果只有2台,一台JournalNode挂掉后剩下1台即使能把edits写进去,也无法满足“多数派”的判定,集群会失去写入能力。
所以在常见规模下,我比较推荐两种方案。
第一种是3台机器,一台做主NameNode+Active,一台做备NameNode+Standby,第三台和另外两台上各放一个JournalNode进程,同时DataNode也分布在这3台上。这套方案能跑通HA,但主备角色和JournalNode挤在一起,NameNode重启或维护时,JournalNode也会跟着动,风险偏高,适合测试环境。
第二种是5台机器,比较适合生产。前两台上只跑NameNode和ZKFC,不跑DataNode,把元数据服务和数据存储分离;另外3台上各跑一个JournalNode和DataNode。这样JournalNode天然就是3个节点,奇数台写Quorum正好满足多数派,NameNode机器也相对干净,GC和IO都不会被DataNode干扰。
还有一个容易被忽略的规划:如果公司只有4台物理机,可以考虑把ZooKeeper也拆成3个进程分布在不同机器上,不要为了省机器把所有进程堆在一起。ZooKeeper和JournalNode都属于“低占用但高可用”的组件,物理机上多跑一个进程不会带来明显负载,但能显著提升容错范围。
2.2 版本选择:稳定优先还是新功能优先
Hadoop版本这一块,我见过太多“非要上最新版”的案例,结果官方文档和第三方博客对不上,折腾一周都在跟版本差异较劲。我的建议很简单:在满足业务最新特性需求的前提下,尽量选已经发布较久、社区反馈集中的稳定版本。
大版本选好之后,还需要考虑一个细节:配套的ZooKeeper版本。Hadoop自带的一些依赖和ZooKeeper版本强相关,ZooKeeper太新或太旧都会出现兼容问题。比如有些Hadoop版本配ZooKeeper 3.5.x没问题,但换到3.7.x就出现会话重复加载。我的习惯是去官方发布说明里查对应兼容版本,然后用包里自带的或推荐版本,别自己拍脑袋升级。
JDK版本同样很重要。Hadoop对JDK版本非常敏感,高版本JDK会出现一些奇怪的JNI调用问题,常见的是Native库加载失败,但有些场景是Spark和HDFS混合部署时,GC参数配置失效。我建议优先选发行方认证过的JDK版本,设置JAVA_HOME时也顺手确认一下lib/native目录里的native库能正常加载。
如果只是为了学习或小型内部系统,用Apache社区版就行,配置简单、资料多。如果业务规模大或者对技术支持有硬性要求,商业发行版会更省心,但有些商业版会改掉默认端口和目录结构,后面的配置需要额外注意。
2.3 系统网络准备及端口清单
在配置HA之前,系统层面的准备直接决定后面能不能顺利启动。我一般按下面这个顺序做。
第一是hosts文件。集群里所有节点的IP和主机名都要写在每台机器的/etc/hosts里,并且保持一致。不要在生产环境依赖DNS解析,DNS偶尔抽风的那几分钟,正是HA最怕的隐患。团队里如果小机器多,建议把hosts文件做成模板统一推送,别一台一台手改。
第二是SSH免密。虽然HA的核心机制不是靠SSH做状态同步,但配置fencing的sshfence策略时必须依赖SSH从一台机器登录另一台机器执行命令,而且日常手动切换时也要靠SSH操作。我通常把每台机器相互之间都配了免密登录,避免两边来回操作时反复输密码。
第三是时间同步。NameNode和JournalNode之间的edits写入对时间顺序很敏感,ZooKeeper的事务语义对节点间时钟偏差也很敏感。时间不同步会导致会话超时判断混乱,出现误切换。最简单的做法是每台机器都装chrony或者ntp,指向同一个时间源,启动后确认每台机器时间差在几百毫秒以内就行。
第四是防火墙和端口。Hadoop集群内部组件之间通信的端口不少,生产环境一般都开了防火墙,这里就要对照端口清单逐项放行。常见的端口如下,建议存成一份清单。
| 组件 | 默认端口 | 用途 |
|---|---|---|
| ZooKeeper | 2181 | 客户端访问 |
| ZooKeeper | 2888/3888 | 选举和节点间通信 |
| JournalNode | 8485 | RPC端口,NameNode写入/读取edits |
| NameNode | 8020 | RPC端口,接受客户端请求 |
| NameNode | 9870 | Web UI,查看集群状态 |
| ZKFC | 8019 | ZKFC内部通信 |
| DataNode | 9864 | DataNode HTTP服务 |
| DataNode | 9866 | DataNode IPC通信 |
最后是磁盘规划。NameNode的元数据目录(dfs.namenode.name.dir)和JournalNode的edits目录(dfs.journalnode.edits.dir)尽量放在单独的、SSD性能好一点的磁盘上,不要让它们和数据目录共享一块盘。元数据写磁盘的延迟直接影响NameNode的写请求性能,共享盘在高负载下抖动会很厉害。
3. ZooKeeper集群部署要点
3.1 为什么ZooKeeper必须要奇数台
HA里的自动故障切换依赖ZooKeeper的分布式协调能力,而ZooKeeper本身又是一个集群。它用的也是Quorum机制:集群中的多数派节点存活时,整个ZooKeeper才能对外服务。所以3台ZooKeeper可以容忍1台挂掉,5台可以容忍2台挂掉。
这里要注意,部署ZooKeeper时大家选的节点数量,其实和“奇数台”这个原则有关系。2台ZooKeeper在功能上是能启动的,但一旦其中一台挂掉,剩余的1台不构成多数派,整个集群就不可用。4台看起来比3台多了一台的冗余,但实际容错数和3台一样,都是只能挂1台,挂2台时整个集群就废了。所以多花的这一台机器没有带来任何可用性收益,奇数台成了一种事实标准。
在实际规划里,3台ZooKeeper是绝大多数中小型集群的配置。ZooKeeper本身很轻量,内存占用主要Follower节点维护事务日志和快照,给它1GB到2GB堆内存基本够用。如果你觉得5台更保险,也可以,但维护成本会上去。
3.2 ZooKeeper安装与配置文件准备
ZooKeeper安装包解压后,核心配置就在conf/zoo.cfg。先讲一下每个参数的用途,这样你看到那些数字不会懵。
tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper clientPort=2181 server.1=node1:2888:3888 server.2=node2:2888:3888 server.3=node3:2888:3888tickTime是ZooKeeper的最小时间单元,单位毫秒,2000表示2秒。initLimit配置的是Follower启动时能容忍的最大初始化连接时间,这里是10个tick,也就是20秒。如果集群规模大、节点多,这个值可以适当调大,比如改成20。syncLimit配置的是Follower与Leader之间通信的超时时间,5个tick也就是10秒,如果网络抖动频繁,可以适当放大一点,但别调得太大,否则故障检测也会变迟钝。
dataDir是事务日志和数据快照的存储目录,最好放在磁盘IO性能好的地方。每个ZooKeeper节点还要在dataDir目录下创建一个myid文件,内容就是它的序号。比如node1的myid内容是1,node2的内容是2,要和zoo.cfg里server.x的数字对应上。这个文件很简单,但漏掉它或者写错数字,启动后ZooKeeper怎么都组不成集群。
还有一个容易踩坑的地方:ZooKeeper进程默认继承系统的JVM内存设置,很多教程没提这茬,结果ZooKeeper吃掉了所有机器内存。建议单独设置ZooKeeper的堆内存,比如在conf/zoo.cfg同目录的java.env或者启动脚本里设置JVMFLAGS,限制到1GB左右。
3.3 启动与验证
三台机器上分别执行zkServer.sh start,然后检查每个节点的状态。
zkServer.sh status正常情况下,会显示一台是leader,其余是follower。如果有节点一直显示standalone或者报connection refused,多半是myid、server.x配置或者防火墙端口的问题。
也可以看ZooKeeper自己的日志,日志里会明确写“QuorumPeerMain”等启动过程信息。如果日志出现“Unexpected exception causing shutdown”,先把配置文件从头对一遍,多半是格式问题。ZooKeeper对配置文件格式还是比较严格的,某个server行少个空格都能启动失败。
4. Hadoop核心配置逐项详解
4.1 core-site.xml:全局基础配置
HA配置的核心文件主要就是两个:core-site.xml和hdfs-site.xml。core-site.xml里最关键的两个配置是默认文件系统和ZooKeeper的地址。
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://mycluster</value> </property> <property> <name>ha.zookeeper.quorum</name> <value>node1:2181,node2:2181,node3:2181</value> </property> </configuration>fs.defaultFS的值是一个逻辑服务ID,我这里的mycluster就是后面在hdfs-site.xml里定义的nameservice名字。客户端访问HDFS时会用这个逻辑名字去解析真正的NameNode地址,而不是直接连某一台物理机。
ha.zookeeper.quorum就是ZooKeeper集群的地址列表,这个配置主要是给ZKFC用的,故障选举时它要连到ZooKeeper去注册和探测。注意这里的地址要写完整,包括端口。
4.2 hdfs-site.xml:HA核心参数
hdfs-site.xml是HA里配置量最大、最容易出错的地方。我把宽度分几个模块过一遍。
先定义nameservice和它包含的NameNode:
<property> <name>dfs.nameservices</name> <value>mycluster</value> </property> <property> <name>dfs.ha.namenodes.mycluster</name> <value>nn1,nn2</value> </property>dfs.nameservices定义了整个HDFS服务集群的逻辑名称。dfs.ha.namenodes.mycluster定义了这个逻辑集群下有哪些NameNode的逻辑名称,我这里是nn1和nn2。这是给客户端做地址解析用的基础信息。
接着给每个NameNode配置RPC地址和HTTP地址,这些地址要能通过SSH访问到对应机器。注意每个NameNode的逻辑ID要清楚,否则后面节点启动时会找不到对应的RPC端口。
<property> <name>dfs.namenode.rpc-address.mycluster.nn1</name> <value>node1:8020</value> </property> <property> <name>dfs.namenode.rpc-address.mycluster.nn2</name> <value>node2:8020</value> </property> <property> <name>dfs.namenode.http-address.mycluster.nn1</name> <value>node1:9870</value> </property> <property> <name>dfs.namenode.http-address.mycluster.nn2</name> <value>node2:9870</value> </property>然后是共享edits目录。QJM的配置格式是一个固定模板:
<property> <name>dfs.namenode.shared.edits.dir</name> <value>qjournal://node1:8485;node2:8485;node3:8485/mycluster</value> </property>这里的意思是通过三个JournalNode的8485端口组成一个quorum,逻辑路径名是mycluster。JournalNode实际写盘的目录在下面这个配置里指定:
<property> <name>dfs.journalnode.edits.dir</name> <value>/data/hadoop/journal</value> </property>接下来是自动故障转移和代理Provider:
<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>proxy provider这一项决定了客户端如何感知NameNode的状态变化。ConfiguredFailoverProxyProvider会读取core-site.xml和hdfs-site.xml里配置好的NameNode列表,在请求遇到故障时自动切换到另一个NameNode。automatic-failover.enabled设为true,表示启用ZKFC自动切换。
然后是Fencing相关的配置:
<property> <name>dfs.ha.fencing.methods</name> <value>sshfence</value> </property> <property> <name>dfs.ha.fencing.ssh.connect-timeout</name> <value>30000</value> </property> <property> <name>dfs.ha.fencing.ssh.private-key-files</name> <value>/home/hadoop/.ssh/id_rsa</value> </property>这里用的是sshfence策略。Hadoop会通过SSH登录到旧Active机器,执行fuser命令去er明占用NameNode端口的进程,从而达到“杀死旧Active”的目的。connect-timeout是SSH连接超时时间,默认比较短,网络慢的话容易fencing失败,可以适当调到30秒。
private-key-files是SSH免密的私钥文件,必须是运行Hadoop的那个用户能访问的,并且权限不能太开放,否则SSH客户端会拒绝加载。
4.3 关于Fencing策略的选型
Fencing策略这块,生产环境里很多人会纠结sshfence和shell(true)该选哪个。sshfence是Hadoop自带的SSH隔离方式,它需要能通过SSH登录到旧Active,然后执行命令kill进程。优点是实现简单、不依赖额外工具,缺点是如果旧Active所在机器网络分区,而且SSH也不通,那fencing就会失败。
shell(true)策略实际上就是“默认认为旧Active已经被隔离”,直接返回成功。这个策略非常危险,等于放弃了物理隔离,一旦出现脑裂,两个NameNode同时写edits,后果不堪设想。我见过的唯一合理用法是和外部物理隔离工具配合,比如通过shell脚本调用带外管理接口或断电工具,但这已经是定制级别了,一般团队不会自己写。
所以我的建议是:默认用sshfence,并且在配置fencing之前先在两台NameNode之间做一轮SSH免密测试,确保能免密登录、能执行fuser命令。fencing失败的表现通常是自动切换卡在Fencing这一步,NameNode迟迟没有变成Active,日志里有大量“Fencing failed”或者超时记录。
4.4 其他值得调的参数
除了HA核心参数,有几个日常运维里经常踩坑的参数我顺手说一下。
dfs.namenode.name.dir是NameNode元数据目录,建议配两个以上路径,可以分布在不同的磁盘或者挂载点上,元数据的多副本能抵挡单盘故障。
dfs.replication控制在HDFS上的副本数。测试环境可以设1到2,生产环境建议3。如果副本数设置太低,数据块在某台DataNode下线后会因为副本不足而迟迟无法恢复。
dfs.namenode.handler.count是NameNode处理RPC请求的线程数。如果NameNode机器核心数多、业务并发大,默认值可能偏小。出现RPC超时或者线程池排队严重时,可以考虑调大这个值,但要结合实际CPU和内存,盲调大了反而增加上下文切换开销。
dfs.image.compress以及事务相关参数我就不具体展开了,这些属于优化项,不影响HA主流程跑通。
5. 集群初始化和启动全流程
5.1 启动顺序:先ZooKeeper,再JournalNode,再NameNode
HA集群的启动顺序比较固定,我每次搭建都按这个节奏来。顺序错了,轻则报错,重则元数据目录状态混乱,后面要花不少时间收拾。
第一步,启动ZooKeeper集群。逐台执行zkServer.sh start,并确认状态正常。
第二步,在每台部署了JournalNode的机器上启动JournalNode进程:
hdfs --daemon start journalnode这一步很多人会忘。JournalNode里会存储共享edits日志目录,如果JournalNode没启动,后面格式化NameNode时可能不会报错,但真正启动时NameNode会连不上JournalNode,卡在启动阶段。
第三步,格式化ZooKeeper上的HA状态。在其中一台NameNode机器上执行:
hdfs zkfc -formatZK这个命令会在ZooKeeper上创建HA相关节点,清空旧的故障转移状态。需要注意的是,这一步只在第一次部署HA或者需要重置HA状态时执行,不要没事就重复跑。跑多了可能把正在工作的自动切换状态破坏掉。
第四步,格式化主NameNode并启动它。在nn1机器上执行:
hdfs namenode -format hdfs --daemon start namenode格式化只在这台机器上做,选哪台当初始Active,不代表以后它永远当Active,只是第一次要有个起点。格式化会在本地元数据目录生成fsimage和VERSION文件,这个过程中包含集群唯一的clusterID,后面Standby节点同步时必须保持这个clusterID一致,否则集群会认为它们在两个完全不同的命名空间里。
第五步,在Standby NameNode机器上执行元数据同步,然后启动它:
hdfs namenode -bootstrapStandby hdfs --daemon start namenodebootstrapStandby做的事情是从已格式化的NameNode或JournalNode上拉取当前元数据,并构建Standby的本地状态。一定要用bootstrapStandby,而不是再执行一次namenode -format。格式化会生成全新的clusterID,两个NameNode的clusterID不一致,整个HA直接起不来。
第六步,启动DataNode。可以逐台执行,也可以用start-dfs.sh统一启动。DataNode启动时会向NameNode注册,两个NameNode都能感知到DataNode的存在,但只有Active的NameNode会管理它们的块汇报。
5.2 第一次格式化的易错点
格式化这个环节,我说一个典型事故。有一次精简测试环境,我临时决定“重新初始化整个集群”,顺手在两台机器上都执行了namenode -format。结果悲剧了:两台NameNode的clusterID完全不同,启动后JournalNode里写入的edits根本没法同步,因为Standby在读共享edits时发现clusterID不匹配,直接拒绝。
正确的重置方式是:只在一台NameNode上format,另一台永远用bootstrapStandby去同步。如果真出现clusterID不一致的情况,可以手动修改两台机器上VERSION文件里的clusterID和storageID,改完再重启,但这个方法比较粗暴,最好还是备份好数据后彻底重来。
还有一个细节:先启动JournalNode再格式化NameNode,以后再启动集群,仍然要保证JournalNode先于NameNode启动。否则NameNode启动时会尝试连接JournalNode拿最新edits,连不上就一直卡在“等待JournalNode”的状态,日志里全是连接超时。
5.3 启动后的验证:jps、Web UI和状态命令
启动流程走完后,先看进程。jps输出里应该能看到NameNode、DataNode、ZKFC这几种进程,不同角色机器上进程组合不一样。
然后浏览器打开Active NameNode的Web UI。UI上有两个关键信息:一个是HA状态,显示为active还是standby;另一个是Active NameNode的“Cluster ID”和“Block Pool ID”。如果看到两个NameNode都是active,那说明fencing没生效,赶紧停一个;如果两个都是standby,说明ZKFC没有成功让其中一个抢到Active,需要去查ZooKeeper会话和ZKFC日志。
命令行验证更直接:
hdfs haadmin -getAllServiceState正常输出是:
node1:8020 active node2:8020 standby这个命令同时验证了ZKFC是否正常工作。如果一直显示两个standby,大概率是ZKFC没启动或者没能和ZooKeeper建立会话。
6. 手动切换与故障演练实操
6.1 手动故障转移命令
自动切换是HA的核心能力,但日常运维中,手动切换才是更常用的操作。比如计划内升级、给NameNode机器更换硬件时,都需要主动把Active切换到另一台机器上,而不是等它故障。
手动切换的命令是:
hdfs haadmin -failover nn1 nn2这个命令的含义是:把Active角色从nn1切换到nn2。Hadoop会先触发nn1的fencing,确认nn1已经失效后,再把nn2拉起为Active。所以手动failover其实和自动failover走的流程是一样的,好处是你可以主动控制切换时机。
如果只是想查询状态,用:
hdfs haadmin -getServiceState nn1执行failover时,有个特别需要注意的地方:如果当前Active的节点还在正常运行,但你手动failover时正好赶上它所在机器网络抖动,fencing阶段可能会超时或者失败,这时命令会报错,集群保持原来状态。所以手动切换也要给SSH和fencing留足时间窗口,别真的在业务高峰做。
6.2 模拟故障:kill掉Active NameNode
自动切换是否真的能用,不能光靠配置来保证,一定要做一次故障演练。我建议在测试环境搭好之后,先把进程跑稳,然后模拟一次Active NameNode宕机。
模拟方式很简单,在当前的Active NameNode机器上执行:
kill -9 <NameNode进程PID>注意这是故意制造一个“没有正常退出通知”的强杀场景,最能检验ZKFC和fencing的真实状态。kill之后,观察Standby节点的日志和状态,正常情况下几十秒内Standby就会变成Active,客户端在短暂报错后能重新连接。
我演练过的场景里,自动切换时间一般在20到50秒之间,具体取决于ZooKeeper会话超时时间、fencing耗时和Standby的元数据加载速度。如果超过1分钟还没有切过去,就要去查ZooKeeper和ZKFC的状态了。
演练结束恢复原状时,再手动failover切回去,然后确认两个NameNode都能正常服务。
6.3 自动切换没生效时的排查思路
如果自动切换迟迟不生效,我的排查顺序一般是这样的。
第一步,看ZKFC进程还在不在。如果ZKFC进程死了,自动切换能力等于零,手动切也不行。
第二步,看ZKFC日志。日志里通常会写清楚是ZooKeeper会话超时、fencing失败,还是候选节点没有获得足够票数。
第三步,看ZooKeeper状态。在ZooKeeper上执行zkCli.sh,检查HA相关的节点是否存在、临时节点是否由正确的ZKFC持有。如果Active已经失联但临时节点还没消失,说明会话超时时间还没到;如果临时节点已经消失但Standby没有接管,问题多半在Standby的ZKFC上。
第四步,确认fencing。如果日志里反复出现“Unable to fence”字样,说明旧Active没被杀掉或者SSH不通,Standby不敢接管。这时候重点检查SSH免密和sshfence的配置。
7. 常见问题排查速查表与运维心得
7.1 高频问题与排查手段
把搭建运维过程中最容易碰到的高频问题整理成一张速查表,方便大家对照排查。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 两个NameNode都是standby | ZKFC未启动或ZooKeeper连接异常 | 检查ZKFC进程和日志,检查ha.zookeeper.quorum配置 |
| 两个NameNode都是active | fence失效,脑裂 | 检查fencing配置,立即停掉一台NameNode进程,防止继续写edits |
| 自动切换不触发 | 会话超时时间过长、fencing失败、ZKFC异常 | 查ZKFC日志、ZooKeeper状态,调整会话超时或fencing策略 |
| NameNode启动后一直卡在等待JournalNode | JournalNode没启动或端口不通 | 确认JournalNode进程和8485端口,防火墙放行 |
| bootstrapStandby报错clusterID不一致 | 误格式化导致多套clusterID | 手动对齐VERSION文件或重新初始化 |
| 慢切换且日志大量重试 | fsimage和edits加载慢,或handler线程不够 | 增大堆内存和handler.count,检查磁盘IO |
| DataNode一直连不上NameNode | RPC地址配置错误或防火墙 | 核对dfs.namenode.rpc-address,测试8020端口连通性 |
| 集群中某个JournalNode写失败 | 磁盘满、权限不对、单台JournalNode宕机 | 检查磁盘空间与目录权限,重启JournalNode |
7.2 配置起来容易踩的坑
配置层面有几个容易被忽略的地方,再提醒一次。
一是权限问题。NameNode元数据目录和JournalNode的edits目录如果权限不是运行Hadoop的用户所有,启动时会直接报Permission denied,而且报错信息藏得比较深,容易被误判成端口问题。所以我一般都会先chown -R把目录归属设置好。
二是端口冲突。如果之前装过单NameNode模式的HDFS,很可能有进程占用了8020或者9870端口。启动前用ss -lntp检查一下端口,避免两个进程互踩。
三是配置同步。核心配置文件一定要在所有相关节点上保持完全一致。不要只在主NameNode机器上改了hdfs-site.xml,其他节点忘了同步,结果启动时各种行为不一致。我记得有一次客户端写文件老是失败,排查半天发现是其中一台DataNode上没有同步更新配置,导致客户端与DataNode之间版本不一致。解决方案很简单,改配置后检查不同节点配置文件md5值。
四是启动顺序。以后每次重启集群,都要先启动ZooKeeper和JournalNode,再启动NameNode。顺序错乱虽然不一定会报错,但会拖慢NameNode的启动过程,因为在等待接入JournalNode时它会反复重试。
7.3 运维阶段的几个经验
HA跑起来之后,日常运维要比单机集群多几件固定动作。
定期做一次故障演练。我一般每个月或者每两个月在测试环境做一次kill Active的演练,看自动切换是否正常。这个习惯很值,因为很多时候配置没变、版本没变,但某台机器磁盘快满或者ZooKeeper节点状态异常,切换便会出问题,演练能提前暴露这些隐患。
监控NameNode的GC、ZooKeeper的会话数和JournalNode的edits写入延迟。这三个指标能早期预警HA的健康度。NameNode GC停顿时间过长,会导致心跳超时,被ZKFC判定失联后触发不必要的自动切换。
版本升级前老老实实看升级文档。HA涉及的组件多,升级顺序和兼容性比较敏感。尤其当你准备小版本升级Hadoop或ZooKeeper时,先看官方升级说明,确认旧版本edits和ZooKeeper元数据能被新版本识别,再安排夜间操作窗口。
还有一个小经验:手动切换之前,先确认目标Standby的元数据真的跟得上。可以通过Web UI看Standby的“Last Checkpoint”或者“Transactions Applied”,如果明显落后于Active,宁可等等再切,也不要强行切换造成大量事务回放,那样NameNode起来后还要做长回放,业务反而停得更久。
8. 最后再分享两个实用工具习惯
第一个习惯,是给所有节点写一个统一的部署检查脚本。脚本内容其实很简单:检查进程、jps对比、端口连通、磁盘空间、配置文件一致性。每次变更后跑一遍,能在几分钟内筛出绝大多数低级失误。我自己用类似脚本省了很多排查时间。
第二个习惯,是保存好每次切换前后的状态快照。用hdfs haadmin -getAllServiceState输出,配合时间戳存到本地文件。这样一旦出现问题,能快速判断到底是哪一步操作导致了状态变化。尤其多人在同一套集群操作时,这个日志能帮你复盘出“是谁动的手”。
如果条件允许,建议构建集群之后专门留一个晚上做一次完整的模拟演练,包括kill Active进程、断开一台JournalNode的网线、调整ZooKeeper会话超时等场景。演练时重点看两点:切换时间是否符合预期、fencing是否每次都能成功。只有把这些都验证过,才能放心把业务交到HA集群上。