从单点走向双活:HDFS高可用(HA)架构的底层逻辑与落地实践
做大数据这行的人,几乎都被一个问题折磨过:NameNode挂了怎么办。早期接触Hadoop的时候,集群规模不大,大家普遍觉得NameNode单点就单点吧,反正重启也就几分钟的事。但等集群真正跑起生产任务,数据目录从几百个增长到上万个,你才明白NameNode的单点问题不是“可用性焦虑”,而是直接影响业务连续性的硬伤。这篇笔记(整个系列的Hadoop基础篇第6篇)想系统性拆解HDFS的HA方案,弄明白它在解决什么问题、背后的核心机制是什么、实际部署时有哪些容易踩的坑,适合正在搭建生产集群、或者准备从单NameNode往HA迁移的同学参考。
1. 绕不开的单点瓶颈:NameNode在集群中的定位
1.1 NameNode到底在干什么
先回溯一下NameNode的职责。HDFS是典型的Master/Slave架构,NameNode负责管理整个文件系统的元数据(namespace),包括目录树、文件与数据块的映射关系、副本位置、配额信息等。DataNode只是被动地执行块读写和心跳上报,并没有能力决定“某个文件分成几个块、放在哪些节点”这类全局决策。
所以客户端在真正读写数据之前,第一步一定是找NameNode拿元数据:文件在哪个块、块在哪些DataNode上。NameNode一旦宕机,整个HDFS的文件操作都会卡住。更麻烦的是,如果NameNode所在节点发生磁盘损坏,元数据丢失,那么即使DataNode上的数据块都完好无损,你也无法把它们重新组织成完整文件。数据块还在,但“文件不见了”。
这也是为什么我在给团队做技术分享时经常强调:NameNode不是简单的“管理节点”,它更像整个分布式文件系统的大脑和目录册。目录册没了,书架上堆满的书也找不回来。
1.2 单机时代为什么还能凑合
早期很多生产集群只有一台NameNode,这不是设计缺陷,而是成本与可用性之间的权衡。单台NameNode部署简单,不需要额外维护ZooKeeper、JournalNode这类组件,运维成本低。对于开发测试环境、日处理量不大、允许中断的业务来说,单NameNode确实够用。
但随着业务量上来,问题就暴露了。首先是恢复时间不可控。NameNode重启或者宕机后,要从本地镜像(fsimage)和编辑日志(edits)恢复元数据,这个加载过程在元数据量大时可能长达几十分钟甚至更久。期间整个集群处于只读或完全不可用状态。其次是元数据丢失风险。虽然可以配置多目录存储、定期做Checkpoint,但本质上还是单机状态,任何硬件故障都可能导致最后一个持久化副本不可用。
我身边一个真实的例子是,某公司的生产集群因为一次断电,NameNode所在物理机的两块磁盘同时出现异常,虽然DataNode节点都正常,但整个集群最后还是花了两天才恢复业务。从那以后,他们就把HA升级提上了最高优先级。
1.3 HA能解决什么,不能解决什么
HA的核心思路很简单:让两台(或者更多)NameNode组成主备关系,一台处于Active状态对外提供服务,另一台处于Standby状态持续同步元数据状态。Active节点故障后,Standby节点可以快速接管,对外继续提供服务。这样可以把恢复时间从“分钟级甚至小时级”压缩到“秒级”,同时避免单台机器的磁盘故障导致元数据彻底丢失。
但注意,HA不是一键万能的。它解决的是“单点故障导致服务中断”的问题,不解决数据规模本身带来的性能瓶颈,也不解决机架感知配置错误、网络分区、人为误操作这类问题。也就是说,HA解决的是可用性(Availability),不是扩展性(Scalability),更不是数据安全性的全部。你把所有的鸡蛋从一个篮子分散到两个篮子,但两个篮子之间的同步机制如果设计得不合理,反而会引入新的问题。
2. HDFS HA整体架构拆解
2.1 何时需要引入ZooKeeper
设计一个主备方案,第一个要回答的问题是:两个节点如何确认谁该干活?最朴素的方法是双机心跳,A和B互相探测,发现对方挂了就接管。但在分布式系统中,这种直接的心跳检测会有一个致命问题:网络分区。假如A和B之间网络抖动,A认为B死了,于是接管成Active;同时B也可能认为A死了,也把自己切换成Active。这就出现了两个Active同时写元数据——也就是脑裂(Split-Brain)。
为了避免脑裂,HDFS HA引入ZooKeeper作为分布式协调组件。ZooKeeper在这个场景里的角色是“第三方裁判”:它负责记录当前哪个NameNode是Active,并响应NameNode的选主请求。因为ZooKeeper本身是多数派协议(ZAB)保证一致性的,所以即使在网络分区的情况下,也只会有一个NameNode能被ZooKeeper确认为Active。
这有点像两个候选人竞选某个职位,必须到一个权威机构做登记备案,谁能拿到“在任证明”谁才是合法的。心跳互探只能证明对方可能挂了,权威机构认可才是“合法上岗”。
2.2 核心组件:ZKFC、JournalNode、共享编辑日志
HDFS HA里最核心的是三个组件,先把它们之间的关系理清楚。
ZKFC(ZooKeeper Failover Controller)是运行在NameNode节点上的一个独立进程,全名是DFSZKFailoverController。它的职责有三个:监控本机NameNode的健康状态、向ZooKeeper发送心跳和选举请求、在需要时执行主备切换。ZKFC对外是一个独立的Java进程,启动后就和NameNode进程并行运行在同一个物理节点上。
JournalNode(JN)集群负责存储共享编辑日志(edits)。Active NameNode把每一条元数据修改操作写入JN的共享edits日志,Standby NameNode持续从JN读取这些edits并应用到自己的内存命名空间中,从而保证两个NameNode的元数据最终一致。JN本身是一个轻量级进程,通常需要奇数个(至少3个)形成一组,通过多数派机制保证可用性。
需要注意的是,早期的HDFS HA方案还支持基于NFS共享存储的方式,也就是Active和Standby共用同一份共享目录,通过OS级别的文件锁来保证互斥。但这种方案依赖NFS的高可用,NFS本身就是单点,而且文件锁的行为在不同的NFS实现上并不一致。后来社区主推的方案就是基于QJM(Quorum Journal Manager)的共享edits日志方式,也是目前生产环境的主流选择。这篇笔记讨论的也是QJM方案。
2.3 Standby节点是“热备”不是“冷备”
很多初学者容易有个误解:Standby是不是就是一个空闲的备份节点?其实不是。HDFS HA里的Standby节点是热备,它一直在后台运行,应用JN上的edits日志,把namespace状态保持在和Active接近最新的水平。
Standby节点也维护与所有DataNode的块报告(Block Report)。DataNode同时向Active和Standby发送心跳与块信息,这样一旦发生切换,Standby马上就有完整的DataNode状态,不需要重新扫描全集群的数据块。
此外,Standby节点还可以承担一部分“读”职责。虽然严格来说,HDFS HA并不默认支持Standby对外提供读服务,但在一些定制版本或者通过配置开启“观察者(Observer)”模式后,Standby或观察者节点是可以分摊读流量的。社区里也有通过在Standby节点上跑一些离线分析任务来利用闲置资源的做法,不过这些都属于进阶玩法,刚上手的时候别把架构搞复杂了。
3. 数据同步的核心:QJM与edits日志机制
3.1 为什么需要共享edits日志
在NameNode的内存中,所有元数据操作都会被追加记录到edits日志文件。Active节点执行一条“创建文件”操作,除了修改内存namespace之外,还要先写入edits日志,然后这个日志才会被持久化。如果edits日志写失败了,整个操作就会失败。
主动备之间要同步元数据,最直接的方式就是把同一份edits日志放到双方都能访问的地方。QJM方案就是围绕“日志同步”来设计的:一组JournalNode共同对外提供日志读写服务,Active NameNode把每条edits按顺序写入多个JN,只要大多数JN(通常是2/3)写成功了,就认为本次日志写入成功。
这个过程是同步的,也就是说,Active节点在执行一条元数据修改操作时,会等待JN的多数派确认,这与异步复制有本质区别。异步复制可能丢日志,同步复制虽然增加了一点写延迟,但保证了“如果客户端收到成功响应,那么这修改至少存在于多数派JN上”。这是HA不丢元数据的底层保证。
3.2 QJM写入的原子性与顺序性
QJM本质上是一组轻量级的JournalNode进程,每个JN上维护自己本地磁盘上的edits段文件。多个客户端可能同时尝试写同一个JN吗?不会。因为同一时刻只有一个NameNode是Active,只有Active才能发起日志写请求。但这需要选举机制来保证“单写者”约束。
在具体实现中,QJM把日志组织成连续的段(segment),每段包含一批edits记录。Active在切换段或执行定期Roll时,会生成新的段标识,并将日志写入各个JN。写每条记录时,会带有一个单调递增的事务ID(txid),Standby读取时按txid顺序应用,避免乱序。
这里有一个容易忽略的点:QJM虽然是多副本,但它是“日志复制”而不是“全量元数据复制”。两个NameNode内存中的namespace最终一致,靠的是Standby持续应用edits来追赶,而不是直接把Active的内存快照拷贝一份。所以在切换后,Standby可能还需要短暂追赶一段日志,这就是为什么故障转移时间虽然短,但并不是“0毫秒”。
3.3 JournalNode的部署要求
JournalNode虽然是轻量级组件,但它的可靠性直接决定HA的可用性。生产环境建议部署3个(或者5个)JN,分布在不同的物理机/机架上,避免同一机架故障导致JN同时丢失。
关于JN的磁盘,强烈建议用独立的、快速响应的磁盘。JN的写入路径是Active NameNode每次元数据修改都要同步写的,如果JN磁盘慢,会直接拉高写操作的延迟。我在实践中发现,同一台物理机上如果JN进程和DataNode进程共用一块繁忙的机械盘,NameNode收到“edits写入成功确认”的时间会明显变长。尽量给JN安排SSD,或者至少单独的磁盘分区。
另外JN目录不要和NameNode镜像目录放在同一块盘上。虽然JN日志和NameNode的fsimage是不同用途的数据,但磁盘故障不分你我。曾有同事某次硬盘坏道,恰好同时影响了一个NameNode的本地镜像和一个JN的数据目录,虽然凭借多数派机制没有丢数据,但恢复过程极其惊心动魄。
4. 自动故障转移:ZKFC与Fencing机制
4.1 ZKFC的三种状态与切换流程
ZKFC在每个NameNode节点上运行,它维护本机NameNode的health状态,并和ZooKeeper集群保持会话。工作流程可以概括为:
- ZKFC发起选举,尝试在ZooKeeper上创建路径为/hadoop-ha/集群名/ActiveStandbyElectorLock的临时节点。
- 成功创建的ZKFC,其所在节点成为Active NameNode;另一个节点的ZKFC监听该节点的存在状态。
- 如果Active节点的ZKFC发现本机NameNode不健康(比如进程OOM、长时间没响应),它会主动删除临时节点,释放Active身份。
- 备用节点的ZKFC监听到临时节点消失,会再次发起选举,创建临时节点,然后调用相应的脚本完成切换。
这里注意一个细节:ZKFC的“健康检测”默认是给NameNode发RPC请求来确认它是否还活着。如果NameNode JVM发生Full GC导致长时间停顿,RPC请求超时,ZKFC也可能误判为不健康,从而触发切换。所以JVM参数里对大堆的GC调优在HA集群也格外重要。
4.2 防脑裂的第一道防线:Fencing
前面说过,网络分区时可能出现“两个节点都以为自己是Active”的情况。如果两边同时写同一个文件系统的元数据,整个HDFS的namespace就崩坏了。QJM本身采用多数派机制,理论上可以拒绝“旧的Active”的写入请求(因为它的租约可能已经失效),但在真正切入之前,系统还需要主动隔离对方,这就是Fencing(隔离)机制。
HDFS HA中,ZKFC在切换到Active状态之前,会先尝试对旧的Active执行一次fence操作。默认的fence方法是sshfence,即ssh到旧Active节点上,使用kill命令杀掉它的NameNode进程。如果ssh失败(比如网络分区导致无法远程执行命令),或者进程没有被杀掉,ZKFC会拒绝完成切换,宁可让系统继续处于无Active状态,也不能冒两个Active同时写入的风险。
这个设计思路非常关键。对于一致性优先的分布式系统来说,“暂时不可用”是可以接受的,“数据被写坏”是不可接受的。在实际运维中,如果遇到故障转移后集群一直无法选主,不要急着强行启动NameNode,先排查fence失败了还是ZooKeeper会话异常。
4.3 手动故障转移 vs 自动故障转移
HDFS HA支持两种切换模式:手动和自动。很多早期版本的部署手册只配置了手工切换,即管理员在NameNode宕机后手动执行hdfs haadmin -failover命令。这种模式适合对自动切换不放心、或者团队希望完全掌控变更窗口的场景。
自动故障转移则依赖ZKFC和ZooKeeper。生产上我建议直接上自动模式,因为你搭建HA的初衷就是降低人工介入时间。但自动模式有个前提:ZooKeeper集群本身必须是健康的。假如ZooKeeper集群挂了,自动故障转移会直接失效,整个HA架构也会退化成“双机热备但无法选举”的尴尬状态。所以HA集群的监控一定是“NameNode + ZKFC + ZooKeeper + JournalNode”四位一体,缺一个都不行。
4.4 YARN的HA也顺带提一句
学了HDFS的HA,很容易自然联想到YARN。ResourceManager(RM)同样是单点,也需要HA。YARN HA的架构和HDFS HA类似:RM选主通过ZooKeeper完成,Active RM承担所有调度,Standby RM通过ZooKeeper感知并随时准备接管。
但注意,它们的一致性模型不同。HDFS的NameNode之间通过edits日志同步,是“数据层面的一致性”;YARN的RM之间的状态同步相对简单,主要靠ZooKeeper存储的一些全局状态和周期性的应用状态汇报。所以在故障切换时,YARN对正在运行的作业的影响通常比HDFS切换影响更明显,因为ApplicationMaster可能也需要跟着恢复。把这部分列进笔记,是因为很多同学把HDFS HA搭完就以为整个集群HA了,其实要保证整个大数据平台的高可用,HDFS、YARN两个组件的HA都要配好。
5. 部署与配置实操:一步步搭起HA集群
5.1 集群规划与前置条件
在动手之前,先规划节点角色。以一个常规的3节点生产小集群为例(仅示例,可按实际规模扩展):
- 节点A:NameNode(主候选)、ZKFC、ResourceManager、ZooKeeper、JournalNode
- 节点B:NameNode(备候选)、ZKFC、ResourceManager、ZooKeeper、JournalNode
- 节点C:DataNode、ZooKeeper、JournalNode
注意JournalNode建议至少3个,所以如果只有两台机器,至少也要把JN拆开部署在3台不同物理机上。NameNode候选节点可以只有2个,JN和ZK的奇数个要求可以单独实现。
前置条件:
- 所有节点之间配置免密SSH,特别是NameNode候选节点之间。
- 各节点时间保持同步,建议配置NTP。时钟偏移太大会导致ZK会话异常或租约误判。
- 确保ZooKeeper集群提前部署完成,并建立好对应的HDFS HA的ZNode路径权限(可以使用ZK的认证机制,避免未授权客户端干扰选举)。
5.2 核心配置项解析
HDFS HA的配置主要在hdfs-site.xml中,下面这些是核心项,我把含义和取值经验整理成表:
| 配置项 | 作用 | 建议值/说明 |
|---|---|---|
| dfs.nameservices | 命名服务逻辑名 | 例如mycluster,所有NameNode共享 |
| dfs.ha.namenodes.mycluster | 每个NameNode的ID列表 | 例如nn1,nn2,逗号分隔 |
| dfs.namenode.rpc-address.mycluster.nn1 | nn1的RPC地址 | 节点名:8020 |
| dfs.namenode.rpc-address.mycluster.nn2 | nn2的RPC地址 | 节点名:8020 |
| dfs.namenode.http-address.mycluster.nn1 | nn1的HTTP UI地址 | 节点名:9870 |
| dfs.namenode.shared.edits.dir | QJM地址 | qjournal://jn1:8485;jn2:8485;jn3:8485/mycluster |
| dfs.client.failover.proxy.provider.mycluster | 客户端故障转移的代理实现 | org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider |
| dfs.ha.automatic-failover.enabled | 是否启用自动故障转移 | true |
| dfs.ha.fencing.methods | 隔离方法 | sshfence |
| dfs.ha.fencing.ssh.private-key-files | 免密登录用的私钥文件 | /home/xxx/.ssh/id_rsa |
| dfs.journalnode.edits.dir | JN本地存储edits的目录 | 例如/data/jn,独立磁盘 |
光看这些项可能没什么体感,我把一个常见错误强调一下:很多新手只配置了共享edits目录,却忘记配置dfs.client.failover.proxy.provider。没有它,客户端根本不知道如何做故障转移,NameNode切换之后,客户端还死连原来那一台。这个配置的效果是,客户端会先解析命名服务地址,如果原Active不可用,会自动切换到新的Active,这也是HA对“访问方透明”的关键。
5.3 初始化启动顺序:比配置还容易出错
搭建HA集群时,启动顺序很重要。很多人按单NameNode的经验来,先启动NameNode,再起JournalNode,结果出现各种元数据不一致。
正确的初始化流程大致是:
- 确保ZooKeeper集群正常。
- 在所有JournalNode节点启动journalnode进程。
- 在其中一台NameNode候选节点执行格式化命令:hdfs namenode -format,这会生成初始的namespace镜像。
- 把格式化后的NameNode目录拷贝到另一台候选节点(可以用distcp或直接分发磁盘目录),保证两个NameNode初始元数据一致。
- 首次启动时,先启动已格式化/已同步了元数据目录的NameNode节点,再在另一台节点执行hdfs namenode -bootstrapStandby,让备节点自动追平活跃节点的日志。
- 在NameNode候选节点上启动ZKFC进程,它会自动通过ZooKeeper选举产生一个Active。
这里有个容易忽略的细节:初始化HA的时候,原始元数据目录(dfs.namenode.name.dir)必须保证两台机器初始状态一致。如果没有先拷贝就启动两个NameNode,它们会认为自己拥有不同版本的namespace,出现“分歧”后极难修复。我建议在首次部署时,干脆格式化全新的目录,不要尝试恢复老集群的元数据,风险高得多。
5.4 日常验证与状态查看
集群起来后,用以下命令确认HA状态:
hdfs haadmin -getAllServiceState输出应该类似:
- 节点A: active
- 节点B: standby
也可以通过Web UI查看。如果显示两个都是standby,通常是ZK选举失败;如果两个都是active,那就要立刻检查fencing配置和ZK节点临时节点状态,这是紧急事故,需要马上介入。
另外可以模拟一次手工切换:
hdfs haadmin -failover nn1 nn2切换后观察Web UI和监控指标,确认客户端访问没有中断。这一步建议在维护窗口多做几次,让团队所有人都熟悉切换流程,真出事的时候才能稳住。
6. 常见故障与排障经验
6.1 切换后NameNode一直处于Standby或恢复失败
现象:Active节点宕机后,备用节点迟迟没有变成Active,或者一直报错“unable to fence old active”。
排查路径:
- 检查ZooKeeper服务状态和ZKFC进程状态,是不是ZKFC进程没起来或者一直Crash。
- 查看ZK中/ha路径的临时节点是谁创建的。如果旧Active的临时节点一直没有被删除,新ZKFC无法抢占锁。
- 检查sshfence能否正常执行,手动ssh到旧节点测试免密登录是否畅通。
在一次实际运维中,我遇到过旧Active节点虽然说已经宕机,但它的ZKFC进程还活着,导致临时节点一直不释放。这种情况下需要手动在ZooKeeper中删除对应的临时节点,或者直接kill掉旧节点上的残留进程,然后再触发主动切换。
6.2 JournalNode数据不一致或日志追平过慢
QJM靠多数派保证日志不丢,但如果某个JN节点长期离线后再恢复上线,它上面可能缺了不少edits段。这种情况JN启动后会自动追平,但也有可能因为网络或磁盘问题导致追平速度极慢,进而拖慢整个集群。
排查时重点看以下几点:
- JN磁盘是否满了,edits目录有没有积压的临时文件。
- NameNode的RPC延迟是否异常,JN写入是否成为瓶颈。
- Standby节点有没有积压大量待应用日志,元数据版本是不是差距过大。
我在实际项目中遇到过JN数据文件权限配置错误,导致NameNode写JN日志一直失败,集群频繁发生“元数据操作超时”。当时查了半天配置,最后发现是新加的JN节点上目录属主不是hdfs用户。这种环境问题比组件本身的Bug更容易踩中,部署时一定要做完整的权限自检。
6.3 脑裂的迹象与处理
脑裂在HA中属于最严重事故之一。虽然fencing机制能降低发生概率,但fencing本身失败时仍然可能出现两个Active。
如何判断?在ZooKeeper中查临时节点,正常情况下只有一个owner;如果两个Active同时存在,客户端访问时会出现不稳定的“文件或目录不存在”“lease expired”等异常,NameNode日志里会出现同一笔edits被写入两次或事务ID冲突。
处理手段只有一个:立刻把其中一个NameNode停掉,保留元数据更新更完整的一方,然后检查fsimage和edits日志的完整性。如果无法判断谁更完整,选择保留JournalNode多数派中对应edits更完整的一方。启动前建议离线段修复,不要暴力地把两个节点都拉起来。
6.4 客户端访问不到新Active
故障转移成功后,业务侧报错找不到NameNode。这个多半是客户端配置的问题。可能的原因:
- 客户端使用的core-site.xml里没有配置ha的命名服务和proxy provider。
- 客户端缓存了原始NameNode地址,需要刷新配置或等待重试超时。
- 使用了旧版本的Hadoop客户端,不支持lexical的命名服务解析方式。
解决办法是从集群里把最新的core-site.xml和hdfs-site.xml同步到所有客户端,并确认Hadoop版本一致。版本不一致时,HA故障转移的协议可能不兼容,这是很常见但又很容易忽略的坑。
7. 个人维护心得:三个容易被低估的细节
最后聊几个我在实战中反复踩过、觉得很有价值的点。
第一,JournalNode的重要性被低估了。很多人把注意力放在NameNode和ZooKeeper上,觉得JN只是“转发日志的轻量组件”,结果JN磁盘IO打满后,整个集群的元数据写入全部跟着变慢,业务表现为“写文件偶尔卡顿”。提前给JN配置独立磁盘、独立网络优先级的收益,比优化NameNode配置更明显。
第二,备份与容灾不要因为HA就放松。HA可以解决单点故障,但解决不了“整个机房断电”“多个节点同时损坏”这类极端场景。定期导出fsimage和edits,或者通过定期“快照+异地存放”的方式保留离线备份,仍旧是最后的保命手段。我在另一家公司的经验是,HA集群正常运行两年,大家觉得非常安心,结果一次存储阵列故障导致两台JN和一台NameNode同时受影响,幸好有之前的离线fsimage备份,才避免了最坏结果。
第三,常态化做切换演练。HA方案搭完不是终点,建议每个季度至少做一次“主动故障转移演练”:手动kill掉Active节点,观察集群恢复时间、告警是否及时、客户端影响范围。演练过程中记录的优化点(比如监控阈值、超时参数、自动化脚本),比任何高可用架构文档都值钱。真正故障来临时,团队能不能不慌不忙地处理,靠的就是演练积累的肌肉记忆。
HDFS的HA机制本身并不神秘,核心就是“共享日志+分布式选举+主动隔离”这三板斧。把这三板斧的原理吃透,配置和排障就都有逻辑可循了。这套笔记也只是整个Hadoop基础篇里的一个节点,后面还有更多有意思的分布式存储、计算调度内容可以继续深挖。