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才能不糊涂。先说EditLogNamenode每次对元数据的修改比如创建文件、删除目录都会先写EditLog然后才更新内存镜像。在HA架构下这个EditLog必须同时写入JournalNode集群通常3台或5台Active和Standby都从JournalNode读取EditLogStandby在内存里把日志重放一遍就能保持和Active几乎一致的状态。再说BlockReportDataNode会同时向Active和Standby上报自己持有的块信息这样Standby随时知道哪些块在哪不需要等切换后再全量扫描。这两条链路一旦断掉HA就是摆设。所以搭建HA的第一步不是急着装软件而是先把我们到底要防什么故障这个问题想清楚。如果是防进程崩溃ZKFC足够如果是防机器断电、网络分区那就要考虑机架和部署位置的分布如果是防误删元数据目录HA也救不了你那得靠备份。我把话说在前面HA能解决的是单点进程故障和大多数硬件故障但它不是容灾更不是数据备份。很多新手把这几件事混在一起最后备份和HA全做成了半吊子这才是最危险的。2. 搭建前的角色分配三台还是五台先把每一台机器的活儿定清楚Hadoop HA的经典最小规模是3台机器跑两个Namenode一个Active一个Standby同时让其中一台兼任JournalNode和ZooKeeper角色。但我不推荐生产环境这么干省机器容易省出故障就麻烦了。我更建议至少5台其中3台单独跑JournalNode和ZooKeeper2台跑Namenode另外几台跑DataNode。先给出一张我常用的角色规划表假设你有5台物理机或虚拟机内网地址分别是10.0.0.11到10.0.0.15主机名IP角色说明nn110.0.0.11NameNode Active候选同时跑ZKFC、JournalNodenn210.0.0.12NameNode Standby候选同时跑ZKFC、JournalNodejn110.0.0.13JournalNode ZooKeeper推荐和Namenode分开jn210.0.0.14JournalNode ZooKeeper同上jn310.0.0.15JournalNode 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如果只搭HDFSmapred可以不配。其中hdfs-site.xml是HA相关配置的主战场这里挑几个容易配错的地方展开。3.1 配置Namenode的Service ID和NameService ID首先要在core-site.xml里设置默认的文件系统名称这个fs.defaultFS决定了客户端访问集群的入口地址。在HA模式下这个地址不是一个具体的Namenode节点而是一个逻辑名字服务。比如configuration property namefs.defaultFS/name valuehdfs://mycluster/value /property property nameha.zookeeper.quorum/name value10.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作为别名注意这个别名不是主机名只是逻辑IDproperty namedfs.nameservices/name valuemycluster/value /property property namedfs.ha.namenodes.mycluster/name valuenn1,nn2/value /property然后就是每个Namenode的RPC地址和HTTP管理地址。注意这里有几个容易混的端口RPC端口是给客户端连的默认8020或9000HTTP端口是给Web UI用的默认9870老版本是50070。我见过有人把两个端口填反造成客户端连不上Web页面也打不开property namedfs.namenode.rpc-address.mycluster.nn1/name value10.0.0.11:8020/value /property property namedfs.namenode.rpc-address.mycluster.nn2/name value10.0.0.12:8020/value /property property namedfs.namenode.http-address.mycluster.nn1/name value10.0.0.11:9870/value /property property namedfs.namenode.http-address.mycluster.nn2/name value10.0.0.12:9870/value /property如果不使用默认端口必须同时检查防火墙是否放行我一般会先临时关掉防火墙来做初始验证但生产环境记得开白名单。3.2 JournalNode的地址列表按竖杠分隔JournalNode地址通过dfs.namenode.shared.edits.dir配置这里用竖杠还是逗号分隔来自于官方文档我建议严格按竖杠来property namedfs.namenode.shared.edits.dir/name valueqjournal://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 namedfs.ha.automatic-failover.enabled/name valuetrue/value /property property namedfs.ha.fencing.methods/name valuesshfence/value /property property namedfs.ha.fencing.ssh.connect-timeout/name value30000/value /property property namedfs.client.failover.proxy.provider.mycluster/name valueorg.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider/value /propertysshfence的意思是当监控方决定让某个Namenode从Active降级时先通过SSH登录到那台机器杀掉上面的Namenode进程防止两个Namenode同时写EditLog造成元数据脑裂。这个是关键安全网不能省。对应的你需要配置Namenode之间的SSH免密登录并且zkfc进程要有权限去执行kill命令所以通常使用root用户或者hadoop用户并配置sudo权限。有的环境因为安全策略限制SSH端口或者不允许root登录此时可以用shell而不是sshfence但shell方式要求手动实现一个脚本可靠性不如官方自带方案。我强烈建议环境允许的情况下优先用sshfence。4. ZooKeeper与ZKFC故障转移的裁判和执行者光有上面的配置集群还不会自动切换。自动切换的核心是一个叫ZKFCZooKeeper 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.110.0.0.13:2888:3888对应。整体配置大概是server.110.0.0.13:2888:3888 server.210.0.0.14:2888:3888 server.310.0.0.15:2888:38883888端口是ZooKeeper选主用的2888是Leader和Follower之间同步用的。如果不写对你会在启动日志里看到Connection refused或者Session closed之类的报错但服务进程还在。这种问题很隐蔽我建议在每台机器上执行ss -lntp一定要确认这三个端口都在监听一个都不能少。5. JournalNode的诡异表现日志在写却在报Quorum not reachedJournalNode作为日志收集器平时很安静但在做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 namedfs.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是activenn2是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搭起来只是第一步后续的监控、告警、演练、备份才是让这套系统真正高可用的核心部分。