1. 为什么非要做HA:单点故障的账,算明白了才动手
早在我第一次搭Hadoop集群的时候,组里某导师就说过一句话:Namenode挂了,你的集群就只剩下一堆DataNode在裸奔。当时我不信邪,觉得Namenode进程哪有那么容易挂,结果真遇到一次元数据目录所在磁盘损坏,整个HDFS直接不可写,跑了大半年的离线任务全停在那等恢复。那一次我花了整整两天从SecondaryNamenode的检查点里捞镜像,最后还丢了几小时的增量数据。从那以后,凡是上生产环境的集群,我都把HA当成必选项,而不是可选项。
Hadoop的HA,英文全称High Availability,目标是消除Namenode这个单点。说白了,就是让集群里同时存在两个(甚至更多)Namenode角色,一个是Active,一个是Standby。Active节点负责处理所有客户端读写请求,Standby节点在后台保持元数据同步,一旦Active节点异常,Standby立刻接管,成为新的Active。整个过程尽量对上层透明,客户端不需要改配置,任务不用重跑。
但这里有个最容易混淆的点:HA不是冷备份,不是每天把fsimage拷贝一份放到另一台机器上。它要求Standby的内存里时刻保存着最新的命名空间状态,并且能够无缝切换。这就涉及Hadoop内部两条核心链路:一个是 EditLog 的实时同步,一个是BlockReport的持续上报。理解了这两条链路,后面配置HA才能不糊涂。
先说EditLog,Namenode每次对元数据的修改(比如创建文件、删除目录)都会先写EditLog,然后才更新内存镜像。在HA架构下,这个EditLog必须同时写入JournalNode集群(通常3台或5台),Active和Standby都从JournalNode读取EditLog,Standby在内存里把日志重放一遍,就能保持和Active几乎一致的状态。再说BlockReport,DataNode会同时向Active和Standby上报自己持有的块信息,这样Standby随时知道哪些块在哪,不需要等切换后再全量扫描。这两条链路一旦断掉,HA就是摆设。
所以搭建HA的第一步,不是急着装软件,而是先把"我们到底要防什么故障"这个问题想清楚。如果是防进程崩溃,ZKFC足够;如果是防机器断电、网络分区,那就要考虑机架和部署位置的分布;如果是防误删元数据目录,HA也救不了你,那得靠备份。我把话说在前面:HA能解决的是单点进程故障和大多数硬件故障,但它不是容灾,更不是数据备份。很多新手把这几件事混在一起,最后备份和HA全做成了半吊子,这才是最危险的。
2. 搭建前的角色分配:三台还是五台,先把每一台机器的活儿定清楚
Hadoop HA的经典最小规模是3台机器:跑两个Namenode(一个Active一个Standby),同时让其中一台兼任JournalNode和ZooKeeper角色。但我不推荐生产环境这么干,省机器容易,省出故障就麻烦了。我更建议至少5台,其中3台单独跑JournalNode和ZooKeeper,2台跑Namenode,另外几台跑DataNode。
先给出一张我常用的角色规划表,假设你有5台物理机或虚拟机,内网地址分别是10.0.0.11到10.0.0.15:
| 主机名 | IP | 角色 | 说明 |
|---|---|---|---|
| nn1 | 10.0.0.11 | NameNode Active候选 | 同时跑ZKFC、JournalNode |
| nn2 | 10.0.0.12 | NameNode Standby候选 | 同时跑ZKFC、JournalNode |
| jn1 | 10.0.0.13 | JournalNode + ZooKeeper | 推荐和Namenode分开 |
| jn2 | 10.0.0.14 | JournalNode + ZooKeeper | 同上 |
| jn3 | 10.0.0.15 | JournalNode + ZooKeeper + DataNode | 可以兼任DataNode |
注意上面这张表里,我把JournalNode和ZooKeeper合并在同一批机器上跑,这是很多生产集群的常见做法。因为这两个角色都是轻量级守护进程,CPU和内存占用不高,但它们需要稳定的磁盘和网络,所以单独用一批机器跑是划算的。nn1和nn2那两台机器把JournalNode也带上,纯粹是节省资源,如果机器够多,完全可以把JournalNode独立出去,和ZooKeeper各占三台。
这里有一个关键点:JournalNode必须是奇数个。为什么是奇数?因为JournalNode之间通过多数派投票机制来决定哪个Namenode的写日志请求算成功,类似Raft或Paxos的多数派原则。3个节点允许挂1个,5个节点允许挂2个。如果部署2个,挂1个就剩下1个,永远凑不出多数派,整个集群会陷入脑裂防护的锁死状态。所以要么3,要么5,千万别用偶数。
ZooKeeper也是一样,3个节点允许挂1个,5个节点允许挂2个。ZooKeeper的作用是帮助ZKFC完成故障转移决策,如果你的ZK都挂了,Standby就无法自动接管。所以ZooKeeper节点数量必须独立满足奇数要求,不能和JournalNode共用数量却不部署在独立机器上,那样故障域没有隔离。
网络方面,我需要提醒几件事:Namenode之间的心跳和EditLog同步对延迟比较敏感,同一机房内网延迟一般在1ms以内,问题不大,但不要把两个Namenode放到跨地域的不同机房,那样无法满足HA的实时性。JournalNode的目录不要放在系统盘,最好单独挂一块磁盘,因为EditLog的写入频率很高,每次元数据操作都是一次IO。虽然现在很多用SSD,但即便是机械盘,只要独立分区,也能扛住生产规模。
操作系统层面,推荐CentOS 7.9或者Ubuntu 20.04这类比较稳定的版本,JDK用1.8或11,注意和Hadoop版本对应。不要用太新的JDK,否则会出现各种JNI兼容问题。这一小节没有命令,但请把角色表打印出来贴在工位上,后面所有配置文件里的hostname都会对应这张表。
3. 配置核心文件:hdfs-site.xml里藏着80%的坑
初始化配置时,你会反复修改三个核心文件:core-site.xml、hdfs-site.xml、mapred-site.xml(如果只搭HDFS,mapred可以不配)。其中hdfs-site.xml是HA相关配置的主战场,这里挑几个容易配错的地方展开。
3.1 配置Namenode的Service ID和NameService ID
首先要在core-site.xml里设置默认的文件系统名称,这个fs.defaultFS决定了客户端访问集群的入口地址。在HA模式下,这个地址不是一个具体的Namenode节点,而是一个逻辑名字服务。比如:
<configuration> <property> <name>fs.defaultFS</name> <value>hdfs://mycluster</value> </property> <property> <name>ha.zookeeper.quorum</name> <value>10.0.0.13:2181,10.0.0.14:2181,10.0.0.15:2181</value> </property> </configuration>这里的"mycluster"就是NameService的逻辑名称,你也可以叫"ns1"、"hadoop-ha",随你便,但后面所有的配置文件里这个名称必须完全一致,包括字母大小写,否则HDFS会报找不到Namenode的错。
hdfs-site.xml里必须要配置的是dfs.nameservices和dfs.ha.namenodes.mycluster,后者定义了在mycluster这个服务下有哪些Namenode的别名。这里用nn1和nn2作为别名,注意这个别名不是主机名,只是逻辑ID:
<property> <name>dfs.nameservices</name> <value>mycluster</value> </property> <property> <name>dfs.ha.namenodes.mycluster</name> <value>nn1,nn2</value> </property>然后就是每个Namenode的RPC地址和HTTP管理地址。注意这里有几个容易混的端口:RPC端口是给客户端连的(默认8020或9000),HTTP端口是给Web UI用的(默认9870,老版本是50070)。我见过有人把两个端口填反,造成客户端连不上,Web页面也打不开:
<property> <name>dfs.namenode.rpc-address.mycluster.nn1</name> <value>10.0.0.11:8020</value> </property> <property> <name>dfs.namenode.rpc-address.mycluster.nn2</name> <value>10.0.0.12:8020</value> </property> <property> <name>dfs.namenode.http-address.mycluster.nn1</name> <value>10.0.0.11:9870</value> </property> <property> <name>dfs.namenode.http-address.mycluster.nn2</name> <value>10.0.0.12:9870</value> </property>如果不使用默认端口,必须同时检查防火墙是否放行,我一般会先临时关掉防火墙来做初始验证,但生产环境记得开白名单。
3.2 JournalNode的地址列表:按竖杠分隔
JournalNode地址通过dfs.namenode.shared.edits.dir配置,这里用竖杠还是逗号分隔,来自于官方文档,我建议严格按竖杠来:
<property> <name>dfs.namenode.shared.edits.dir</name> <value>qjournal://10.0.0.11:8485;10.0.0.12:8485;10.0.0.13:8485/mycluster</value> </property>有的教程会写成jnode://,那是错的。qjournal表示用QuorumJournalManager,后面跟三个JournalNode的IP和端口8485,最后是名字空间标识符。这里我还踩过另一个坑:把三个IP写成了我上面角色规划表里的全部三台JournalNode,但你实际只启动了两台,那么写入时永远达不到多数派,Namenode格式化后依旧报"Unable to start journal"。
我还见过把hostname写在IP前面的配置,这在某些版本里也能解析,但跨节点解析一旦依赖/etc/hosts映射出错,就会变成非常诡异的连接问题。所以我的做法是:所有配置里统一写内网IP,只在core-site.xml里写hostname作为别名。这样做不是最优解,但排查起来最省事。
3.3 自动故障转移和隔离机制
接下来配置故障转移相关的项:
<property> <name>dfs.ha.automatic-failover.enabled</name> <value>true</value> </property> <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.client.failover.proxy.provider.mycluster</name> <value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value> </property>sshfence的意思是,当监控方决定让某个Namenode从Active降级时,先通过SSH登录到那台机器,杀掉上面的Namenode进程,防止两个Namenode同时写EditLog造成元数据脑裂。这个是关键安全网,不能省。对应的,你需要配置Namenode之间的SSH免密登录,并且zkfc进程要有权限去执行kill命令,所以通常使用root用户或者hadoop用户并配置sudo权限。
有的环境因为安全策略限制SSH端口,或者不允许root登录,此时可以用shell而不是sshfence,但shell方式要求手动实现一个脚本,可靠性不如官方自带方案。我强烈建议环境允许的情况下优先用sshfence。
4. ZooKeeper与ZKFC:故障转移的裁判和执行者
光有上面的配置,集群还不会自动切换。自动切换的核心是一个叫ZKFC(ZooKeeper Failover Controller)的组件,它伴随每个Namenode进程启动,和ZooKeeper保持会话。它的任务有三个:注册临时节点、监测本机Namenode健康状态、在Active异常时去抢锁并触发切换。
可以这样打比方:ZooKeeper是一个公告板,两个Namenode都在上面"贴条"争夺Active资格,谁贴上了谁是主。ZKFC是候选者手里的笔,那个笔要持续盯着自己的Namenode进程是不是还活着,如果活着且当前是主,就定期写心跳;如果挂了,笔就断了,另一个ZKFC发现公告板空了,就去抢。
这里有个很容易忽略的进程顺序问题。在HA模式下,不要先去启动HDFS的Namenode,而是先把ZooKeeper集群拉起来。然后启动JournalNode。这些基础服务就绪后,再通过hdfs haadmin -transitionToActive和hdfs haadmin -transitionToStandby来手动指定初始的Active和Standby。
我记得第一次做的时候,按网上的教程执行完格式化,直接start-dfs.sh,结果两个Namenode都处于standby状态。原因就是ZKFC还没有运行,自动切换机制没有初始化。正确做法是:首次搭建时,先在nn1上格式化命名空间,然后手动启动两个Namenode,再逐个启动每个节点上的ZKFC。如果是用start-dfs.sh统一启动,请先确认ZooKeeper已经启动,JournalNode也已经启动,start-dfs.sh才会自动拉起ZKFC。
4.1 格式化的正确姿势:HDFS的"格式化"和HA的"引导"是两码事
传统单节点模式下,需要执行hdfs namenode -format,生成fsimage文件。HA模式下启动第一个Namenode时也要做一次元数据格式化,但这个格式化与JournalNode的初始化是绑定在一起的。当你执行:
hdfs namenode -format在HA模式下会连JournalNode一起初始化,把命名空间ID写入JournalNode。这里有个血泪教训:不要在两个Namenode上各自执行一次格式化,那样会生成两个不同的clusterID,导致两个Namenode状态不一致,JournalNode那边直接报NameNode ID冲突,集群起不来。
正确做法是:只在一台Namenode(比如nn1)上格式化,然后在另一台(nn2)上执行:
hdfs namenode -bootstrapStandby这个命令的作用是把nn1上的fsimage和EditLog同步到nn2,让nn2成为合格的Standby。如果执行bootstrapStandby时报错"Unable to connect to nn1",先检查nn1是否真的已经启动,以及RPC端口是否通。
4.2 myid号:ZooKeeper的身份证
每一个ZooKeeper节点上,需要在数据目录(默认是/tmp/zookeeper,但生产要改到持久目录)里新建一个文件叫myid,里面写一个唯一的数字,比如1、2、3。这个数字必须与zoo.cfg里的server.1=10.0.0.13:2888:3888对应。整体配置大概是:
server.1=10.0.0.13:2888:3888 server.2=10.0.0.14:2888:3888 server.3=10.0.0.15:2888:38883888端口是ZooKeeper选主用的,2888是Leader和Follower之间同步用的。如果不写对,你会在启动日志里看到"Connection refused"或者"Session closed"之类的报错,但服务进程还在。这种问题很隐蔽,我建议在每台机器上执行ss -lntp一定要确认这三个端口都在监听,一个都不能少。
5. JournalNode的诡异表现:日志在写,却在报"Quorum not reached"
JournalNode作为日志收集器,平时很安静,但在做HA切换或Namenode重启时,它的状态会直接决定集群能否继续服务。我遇到过这么一个问题:三个JournalNode进程都活着,端口也通,但是Namenode做EditLog写入时总是报:
Quorum journal writes failed for transaction ... (2/3 nodes accepted)日志里显示只有2个JournalNode接受了写请求,另外1个超时。当时我觉得奇怪,三个节点明明是通的,为什么总会有一个超时?后来排查发现,那台"超时"的JournalNode的磁盘突然满了。JournalNode在写EditLog时,是先写入本地缓存或磁盘,如果磁盘没有空间,写入会卡住,直到超时,但进程不会退出。
这件事给我提了个醒:JournalNode的目录满了不会主动报警,你只能通过监控来发现。一定要给JournalNode的数据盘分区分大一点,并且做好磁盘水印监控,达到80%就告警。同时把JournalNode的日志级别调成WARN以上,否则大量INFO日志把错误信息淹没,你很难定位。
5.1 JournalNode目录不能靠默认值
默认的JournalNode数据目录是/hadoop/dfs/journal,如果你没有显式配置,在某些老版本里会默认放在/tmp下,重启就丢数据。推荐在hdfs-site.xml里显式指定:
<property> <name>dfs.journalnode.edits.dir</name> <value>/data/journal</value> </property>请注意,这个目录需要手工创建,并且把属主改为运行Hadoop的用户。我一开始忘了创建,启动JournalNode时报了Permission denied,还以为权限配置出了大问题。
6. 客户端视角:Namenode切换了,客户端怎么知道?
很多人在集群内部做HA测试一切正常,但跑Spark或Hive作业时,发现客户端报:Standby is not available。这是为什么?因为客户端拿到的是来自fs.defaultFS的地址,也就是mycluster这个逻辑名,它必须知道怎么去连接mycluster背后的两个Namenode,而这个逻辑需要引入一个代理类。
在上面3.3节我已经给出了ConfiguredFailoverProxyProvider的配置,但这个类只对Hadoop自带的HDFS客户端有效。如果你用的是Spark、Hive、Flume等外部组件,它们可能通过自己加载core-site.xml去获取配置,只要分发配置一致即可。
还有一种常见情况是,没有把hdfs-site.xml里的dfs.client.failover.proxy.provider配置放到客户端的classpath里,导致客户端根本不知道如何failover。看起来是"主节点切到nn2了,客户端还在反复重连nn1的8020端口",实际上就是缺乏代理配置。另一种做法是在代码里用HdfsConfiguration和FileSystem对象手动指定代理类,但那样太麻烦,不如直接把集群的配置文件分发到所有客户端的/etc/hadoop/conf目录下。
6.1 手动模拟故障:验证你是不是真的把HA搭成功了
配置完成后,不要急着跑业务,先做一次手动故障演练。我习惯的步骤如下:
- 在nn1上执行hdfs haadmin -getAllServiceState,确认nn1是active,nn2是standby。
- 使用hdfs dfs -put写入一个小文件,确认可以正常上传。
- 在nn1上执行kill -9 杀掉Namenode进程,注意别用kill -15,-15会触发优雅关闭,可能让ZKFC来不及感知。
- 等大约30秒(由ZooKeeper会话超时时间决定,默认是5到15秒),再次执行hdfs haadmin -getAllServiceState,检查nn2是否已经变成active。
- 在客户端上再写一个文件,验证读写恢复正常。
这一步一定会暴露问题。最常见的失败点是ZKFC没有启动。你会在日志里看到nn1变成"standby"但nn2并没有接手,而是两个都standby,那就是ZKFC没有参与仲裁。此时检查每台Namenode上的zkfc进程是否还在,当然还要检查ZooKeeper三个节点是否形成多数派。
7. 版本与优化:HA不是配置完就高枕无忧
很多开源的发行版(比如基于Apache Hadoop的二方发行包或者商业化发行版)在HA实现上大同小异,但版本号不同可能引发配置项区别。比如在2.x版本里,dfs.namenode.http-address默认是50070;在3.x版本里改成9870。如果你网上搜到一篇老教程,套到新版本上,地址填错会导致HA状态页打不开,还以为防火墙有问题。
我的建议是用发行版的官方文档来对照配置项,不要盲信年代久远的博客。如果你用的是某个发行版集成的管理界面,它可能会自动配置HA,但底层仍然是同样一套机制。即便如此,我仍然建议理解每一项配置的含义,因为管理界面也会出错,而且一旦出错,排查起来比手配更难。
7.1 调优:让故障转移快点,再快点
HA的自动切换时长由几个参数共同决定:ZooKeeper会话超时时间(zookeeper.session.timeout,默认大约是20秒,可调小到5秒)、ZKFC的healthCheck间隔、以及sshfence超时时间。如果你希望几十秒内完成切换,可以把ZooKeeper的tickTime和minSessionTimeout调低,但注意太低了可能会因网络抖动造成误切。
我们生产上把zookeeper.session.timeout设为了10秒,考虑到运维需求,体验是:Active进程被杀掉后,大概5到15秒内Standby会接管。千万不要追求极致的几秒切换,尤其是在网络不稳定的环境,会造成频繁的主备切换,反而影响集群稳定。
7.2 一个老生常谈的磁盘问题
最后我还要再唠叨一遍,元数据所在磁盘必须和系统盘分离。很多集群的Namenode默认把dfs.name.dir写到/tmp下,导致格式化后一切正常,然后某次重启发现原来的目录没了,整个HA都要重新初始化。如果你还不知道dfs.name.dir在哪个路径,请立即执行hdfs getconf -confKey dfs.namenode.name.dir查看。
我在实际运维中体会最深的一点是:HA给了你恢复能力,但不会给你纠错能力。元数据损坏、误操作、配置错误,这些还是要靠规范和备份来兜底。把HA搭起来只是第一步,后续的监控、告警、演练、备份,才是让这套系统真正高可用的核心部分。