深入 Hadoop 05 - HDFS HA 与脑裂防御
上一篇讲了 NameNode 单点的元数据持久化机制——FSImage + EditLog。本篇展开 HDFS HA 怎么解决 NameNode 单点问题。
HDFS HA 常被介绍成"两个 NameNode 互为热备"。这个描述对应了部署形态但掩盖了关键机制。准确的说法是:HDFS HA 把原来由 Active NameNode 独占写的 EditLog 拆出来放到 3 个 JournalNode 组成的多数派集群上,Active NameNode 每次写 EditLog 都要等多数派 JournalNode 确认。Standby NameNode 实时 tail 这个共享 EditLog,与 Active 保持内存同步。Active NameNode 宕机时,Standby NameNode 接管前必须先确保旧的 Active 被隔离,防止两个 NameNode 同时声称自己是 Active(脑裂)。
本篇只抓一个问题:Quorum Journal Manager 怎么工作、ZKFC 怎么检测 NameNode 健康、为什么脑裂是 HA 系统的头号敌人以及 HDFS 怎么防御它。
为什么不用共享存储
HDFS HA 的早期方案(Hadoop 1.x 时代)依赖共享存储——Active NameNode 把 EditLog 写到一个 NAS 或者 NFS 共享目录,Standby NameNode 从这个目录 tail。这个方案的缺陷是共享存储本身是单点——NAS 故障会导致整个 HA 失效。
Hadoop 2.x 引入 QJM(Quorum Journal Manager)替代共享存储。QJM 的核心思想是把 EditLog 复制到 3 个(或更多)JournalNode 组成的集群,每个 JournalNode 都是独立节点,写入需要多数派(quorum)确认。
3 个 JournalNode 的集群容忍 1 个节点宕机——多数派是 2,任何 2 个 JournalNode 写入成功就算 EditLog 持久化。5 个 JournalNode 集群容忍 2 个节点宕机。
为什么是奇数个 JournalNode?因为偶数个节点的多数派与奇数+1 个节点的多数派容忍故障数相同,但多一个节点的写入开销。3 个 JournalNode(容忍 1 个故障)和 4 个 JournalNode(多数派 3,仍然容忍 1 个故障)效果一样,后者多一个节点的写入延迟。所以 JournalNode 集群通常部署 3 个(小集群)或 5 个(大集群)。
QJM 替代共享存储的收益是把"单点 EditLog"变成"多数派 EditLog",HA 的可靠性从依赖外部 NAS 变成依赖内部 3 节点集群。3 个 JournalNode 可以与 NameNode、ResourceManager、ZooKeeper 等核心节点同部署,不引入额外物理设备。
Active 和 Standby 的 EditLog 同步机制
QJM 部署下的两个 NameNode 角色分工:
1 | |
Standby NameNode 的核心工作是"实时 tail + replay"。它持续从 JournalNode 拉取最新 EditLog 段,在内存里重放,让自己的内存状态尽可能追上 Active。理想情况下 Active 和 Standby 的内存差异只有"未 tail 的最后几个事务"(毫秒级延迟)。
这种设计的精妙之处是 Standby 是热备——接管时不需要重放整个 EditLog,只需补上最后的尾巴。Active 宕机到 Standby 接管的间隔可以控制在秒级。
Standby 还承担一个职责:周期性 Checkpoint。前面讲过非 HA 部署由 Secondary NameNode 做 Checkpoint,HA 部署由 Standby NameNode 做。Standby 持续在内存里重放 EditLog,状态与 Active 接近,做 Checkpoint 比独立的 SNN 更高效(不需要从 Active 拉取 FSImage + EditLog,本地内存里就有)。Checkpoint 完成后把新 FSImage 推回 Active,Active 替换旧 FSImage 并清理已合并的 EditLog 段。
ZKFC:NameNode 健康监控和故障切换
光有 QJM 和 Standby 还不够,还需要一个组件负责"检测 Active 宕机 + 触发切换 + 隔离旧 Active"。这个组件是 ZKFC(ZooKeeper Failover Controller)。
每个 NameNode 节点上同进程部署一个 ZKFC(实际上是独立 JVM 进程,但与 NameNode 在同一物理机上)。ZKFC 做三件事:
第一,周期性健康检查。ZKFC 每隔几秒(默认 ha.health-monitor.sleep-interval.ms = 1000ms)向同节点的 NameNode 发 RPC 探活。探活检查内容包括:NameNode 进程是否响应 RPC、NameNode 是否处于 Safe Mode、NameNode 是否能正常与 JournalNode 通信。任何一项失败,ZKFC 把这个 NameNode 标记为 unhealthy。
第二,在 ZooKeeper 上持有"Active 锁"。集群部署时在 ZooKeeper 上预先创建一个 znode(路径形如 /hadoop-ha/nameservice1/ActiveStandbyElectorLock)。当前 Active NameNode 对应的 ZKFC 持有这个 znode 的临时节点(ephemeral node)。临时节点的生命周期绑定到 ZooKeeper session——ZKFC 进程崩溃或者与 ZooKeeper 心跳超时,session 失效,临时节点自动消失。
第三,触发切换和隔离。当 Active ZKFC 检测到 Active NameNode 宕机,或者 ZKFC 自己失去与 ZooKeeper 的连接导致临时节点消失,另一个 ZKFC(Standby 侧)尝试获取 Active 锁。获取成功后,新 ZKFC 在确认旧 Active 被隔离(fencing)的前提下,把本地 Standby NameNode 提升为 Active。
脑裂:HA 系统的头号敌人
脑裂(split-brain)是分布式系统 HA 的经典问题。具体到 HDFS:
1 | |
脑裂的后果是灾难性的——两个 Active 各自处理客户端请求,DataNode 收到两个相互矛盾的块操作指令,集群元数据混乱,恢复时无法判断哪个 EditLog 是权威版本。
防御脑裂的关键是 fencing——在提升新 Active 之前,必须确保旧 Active 不能再写 EditLog。HDFS HA 用三层 fencing:
第一层:失去 ZooKeeper 锁。Active ZKFC 失去锁后,理论上应该立刻让本地 NameNode 退出 Active 状态。但这只是"软隔离"——如果 ZKFC 自己崩溃或者 ZKFC 与 NameNode 通信断开,NameNode 不知道自己失去了 Active 锁,继续以 Active 身份工作。
第二层:SSH kill 旧 Active 进程。新 Active 接管前,ZKFC 尝试 SSH 到旧 Active 节点执行 kill -9 把 NameNode 进程杀掉。这是 HDFS HA 默认的 fencing 方法(配置项 dfs.ha.fencing.methods = sshfence)。SSH kill 失败的常见原因是 SSH 密钥配置不对,或者旧 Active 节点 SSH 守护进程也宕了。
第三层:电源 / 网络隔离。生产集群可能配置更激进的 fencing——例如调用 PDU(电源分配单元)API 切断旧 Active 节点的电源,或者调用交换机 API 把旧 Active 节点从网络里隔离。这种"硬件级 fencing"成本高但绝对可靠,称为 STONITH(Shoot The Other Node In The Head)。
三层 fencing 的核心思想是"宁可错杀不能漏杀"。如果第二层 SSH kill 失败,ZKFC 会拒绝提升新 Active——宁可让集群暂时不可用,也不能容忍脑裂。
切换流程的完整序列
把切换流程展开成完整时序:
1 | |
这个流程的关键时间点在 T=6——fencing 必须成功,否则 ZKFC-2 拒绝提升 NN-2 为 Active,宁可集群暂时不可用。
注意 T=10 DataNode 的角色。DataNode 同时向 Active 和 Standby 发送块报告和心跳(配置 dfs.ha.namenodes.id 列出所有 NameNode ID)。当 Active 切换时,DataNode 立刻知道哪个是新 Active,向新 Active 发完整的块报告,让新 Active 重建 BlocksMap。
客户端如何感知 Active 切换
客户端 RPC 失败时如何处理 Active 切换?HDFS 客户端内置了故障切换逻辑。
客户端初始化时拿到 NameNode 的逻辑地址(HA 部署里是 nameservice ID,例如 hdfs://nameservice1/path)。客户端通过配置 dfs.ha.namenodes.nameservice1 拿到这个 nameservice 下的所有 NameNode RPC 地址(Active + Standby)。
客户端发 RPC 时先尝试第一个 NameNode。如果收到"我不是 Active"的响应或者连接超时,客户端自动切到下一个 NameNode 重试。重试次数有上限(默认 ipc.client.connect.max.retries = 10),重试用尽才抛 IOException 给应用层。
这种客户端层故障切换让 Active 切换对应用层基本透明——应用层 RPC 失败重试时,客户端已经换到新 Active。少数场景下应用层会看到短暂的 IOException(例如写文件过程中 Active 切换导致 Pipeline 中断),需要应用层捕获重试。
Hadoop 2.x 还提供了 RPC 层透明故障切换——DFS Client 自动处理 Active 切换,应用层无感知。但写文件这种有状态操作(Pipeline)无法完全透明,必须重试。
实验:观察 HA 状态和切换
部署了 HA 的 HDFS 集群可以用 hdfs haadmin 命令观察和管理 HA 状态:
1 | |
输出 active 或 standby,标识每个 NameNode 当前角色。
更详细的 HA 状态在 NameNode Web UI 上。Active NameNode 的 Web UI 顶部有 “HA State: active” 标识,Standby NameNode 是 “HA State: standby”。
切换演练(failover)的命令:
1 | |
这条命令模拟把 Active 从 nn1 切换到 nn2。ZKFC 会执行完整切换流程:fencing nn1、提升 nn2、确认 nn2 成为 Active。客户端在这个过程中可能看到几秒的 RPC 失败,重试即可恢复。
观察 ZooKeeper 上的 Active 锁:
1 | |
ActiveStandbyElectorLock 这个临时节点的内容记录当前 Active NameNode 的标识。手动删除这个节点会触发一次切换(生产环境不要尝试)。
模式提炼
HDFS HA 体现的设计模式:
1 | |
这个模式不只是 HDFS。HBase Master HA 用类似设计——多个 HMaster 通过 ZooKeeper 选主,Backup Master tail Master WAL。YARN ResourceManager HA(第十一篇展开)也用类似思路——RM State Store 写到 ZooKeeper 或 LevelDB,多个 RM 通过 ZK 选主。Kafka Controller 用 KRaft / ZooKeeper 选主,Controller 注册到 ZooKeeper 临时节点。
数据库领域的对应物是"主从同步 + 故障切换"。MySQL MHA、PostgreSQL Patroni 都是这个模式的实现。差异在于数据库的 binlog 复制是异步的(Standby 落后 Active 是常态),HDFS HA 的 EditLog 同步是准同步的(Standby 实时 tail,延迟毫秒级)。这个差异源于一致性要求——数据库允许读 Standby 一致性弱,HDFS 不允许读 Standby(Standby 不接受 RPC)。
工程迁移表
| HDFS HA 概念 | HBase Master | YARN RM HA | Kafka Controller | MySQL MHA |
|---|---|---|---|---|
| Active / Standby | Active / Backup Master | Active / Standby RM | Leader / Follower | Master / Slave |
| Quorum Journal | ZooKeeper / WAL | RM State Store | KRaft log | binlog |
| ZKFC 等价物 | MasterQuorumManager | EmbeddedElect | KRaft voter | MHA Manager |
| Active 锁 | /hbase/master znode | /yarn-leader | KRaft leader epoch | virtual IP |
| Fencing 机制 | SSH kill | SSH kill | KRaft quorum | STONITH |
| 切换时间 | 秒级 | 秒级 | 秒级 | 30 秒+ |
注意 MySQL MHA 这一列的差异。MHA 的切换时间显著长于 HDFS HA,原因是 MySQL binlog 是异步复制——切换时 Slave 必须先追上 Master 的 binlog(可能差几分钟甚至几小时),然后再切换。HDFS HA 因为 EditLog 准同步,Standby 始终接近 Active,切换时不需要等待追赶。这是同步复制 vs 异步复制的根本权衡——同步复制降低写入吞吐(每次写入要等多数派确认)但提升 HA 切换速度。
常见误解
误解一:“HA 部署后 NameNode 永远不会宕机”。HA 解决的是 NameNode 单点问题,但 NameNode 仍然会宕机(OOM、节点故障、人工维护)。HA 的价值是宕机后 Standby 能在秒级接管,不是消除 NameNode 宕机这件事本身。
误解二:“Standby NameNode 可以接受读请求分担负载”。HDFS HA 里 Standby NameNode 不接受客户端 RPC(Hadoop 3.x 引入 Observer NameNode 才提供读能力,第六篇展开)。设计上让 Standby 专心做 tail 和 Checkpoint,不分散资源。
误解三:“Active 切换对应用完全透明”。读操作确实透明——客户端 RPC 失败时自动切到新 Active。但写操作不完全透明——Pipeline 在 Active 切换时中断,客户端必须重试整个文件或最后几个 block。应用层必须捕获 IOException 重试。
误解四:“3 个 JournalNode 够用,多几个更好”。JournalNode 数量越多,多数派越大,写入延迟越高(要等更多节点确认)。3 个 JournalNode 已经能容忍 1 个节点宕机,对绝大多数集群够用。5 个 JournalNode 仅在大集群(容忍 2 个节点同时宕机)才考虑。
误解五:“fencing 失败无所谓,反正 Standby 已经追上”。这是危险的误解。fencing 失败意味着旧 Active 可能仍然在写 EditLog,此时提升新 Active 必然导致脑裂。HDFS HA 的设计原则是"fencing 失败宁可集群不可用,也不容忍脑裂"。运维必须保证 SSH fencing 配置正确,或者在硬件层做 STONITH。
练习
-
在 HA 部署的集群上运行
hdfs haadmin -getServiceState nn1和hdfs haadmin -getServiceState nn2,确认当前 Active。然后用hdfs haadmin -failover nn1 nn2触发一次手动切换,观察客户端在切换过程中的 RPC 行为。 -
在 ZooKeeper 客户端执行
ls /hadoop-ha/nameservice1和get /hadoop-ha/nameservice1/ActiveStandbyElectorLock,观察 Active 锁的内容。 -
在
apache/hadoop源码里找到ZKFailoverController.java、ActiveStandbyElector.java、JournalNode.java,观察 ZKFC 健康检查和 JournalNode 多数派写入的实现。 -
思考题:如果让 JournalNode 集群部署 4 个节点(多数派 3),容忍故障数仍然是 1,但写入延迟会上升。这种部署相比 3 个 JournalNode 有任何好处吗?反过来,如果集群要求容忍 2 个节点同时宕机,应该部署几个 JournalNode?
系列导航
| 序号 | 主题 | 状态 |
|---|---|---|
| 00 | 导读:Hadoop 的核心前提是节点总会失败 | |
| 01 | HDFS 架构:NameNode、DataNode 与元数据的三层切分 | |
| 02 | 文件写入路径:从客户端到 DataNode 的流水线 | |
| 03 | 文件读取路径:副本选择与短路读 | |
| 04 | NameNode 内存模型:FSImage、EditLog 与启动恢复 | 上一篇 |
| 05 | HDFS HA:Quorum Journal Manager、ZKFC 与脑裂防御 | 本篇 |
| 06 | HDFS 3.x 演进:纠删码、路由联邦与 Observer NameNode | 下一篇 |
参考资料
- Apache Hadoop 官方文档:HDFS High Availability. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html
- Apache Hadoop 官方文档:HDFS HA with NFS. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithNFS.html(早期方案,已被 QJM 替代)
- Sanjay Radia et al. A Technical Overview of the Hadoop Distributed File System.(Apache Hadoop 三位核心贡献者关于 HA 设计的官方说明)
- Apache Hadoop 源码:
QuorumJournalManager.java、JournalNode.java、ZKFailoverController.java、ActiveStandbyElector.java. https://github.com/apache/hadoop - Flavio Junqueira, Benjamin Reed. ZooKeeper: Distributed Process Coordination. O’Reilly, 2013.(ZooKeeper 原理,HDFS HA Active 锁依赖 ZooKeeper)
