HDFS 高可用(QJM、ZKFC、Federation)¶
作者:JiangChong | 撰写时间:2026年08月
适用场景:需要理解 HDFS 如何消除 NameNode 单点故障(HA 架构、QJM、ZKFC、fencing),或要在生产集群上配置/排查高可用与 Federation 的大数据工程师。零基础读者建议先读 HDFS 架构原理。
阅读前回顾(30 秒)¶
前三篇留下的伏笔在这里收束:
- HDFS 架构原理 说过:NameNode 是「只记台账的管理员」,整个集群的元数据都在它的内存里,它是单点故障(SPOF)——管理员一倒,仓库再满也没人能指路;
- HDFS 读写流程与副本放置策略 展示过:读写数据都要先问 NameNode;
- ZooKeeper 分布式协调服务核心概念 讲过「抢锁、心跳、通知」三大协调原语——本文第 4 节的 ZKFC 就是它们的直接应用。
本文回答三个问题:怎么让管理员不单点(双机 + 共同记账 + 自动换人)、换人时怎么防止「两个管理员同时上班」(脑裂与 fencing)、管理员的能力上限怎么突破(Federation)。全文的比喻体系延续前两篇的「仓库/管理员」。
关联文章¶
- HDFS 架构原理 — 前置阅读:NameNode/DataNode 职责、FSImage 与 EditLog
- HDFS 读写流程与副本放置策略 — 读写路径与副本放置细节(系列第 2 篇)
- ZooKeeper 分布式协调服务核心概念 — 前置:znode/session/watch/ZAB,本文第 4 节 ZKFC 的底层机制
- HDFS 高可用(QJM、ZKFC、Federation) — 本文
- HDFS 小文件问题与治理 — HA 集群同样受小文件问题困扰
- 纠删码 vs 三副本存储策略 — 与 HA 正交的存储效率话题
- HDFS 数据湖存储底座(与 Iceberg 的关系) — 数据湖场景下的 HDFS
- Vertica Eon Communal Storage 运维实战 — Vertica 视角的共享存储运维
- HDFS 高可用集群部署实战(QJM 手动切换版) — 直接下游:手动切换版部署实操(不含 ZK)
理解全文脉络¶
本文先回答「NameNode 为什么是单点故障、传统方案为什么救不了场」(第 1 节),再逐层拆解 HA 的三根支柱——双机 NameNode(第 2 节)、QJM JournalNode(第 3 节)、ZKFC(第 4 节),第 5、6 节专门讲脑裂与 fencing 这个最容易踩坑的设计点以及完整的故障转移流程。最后讲客户端如何无感知切换(nameservice,第 7 节)、Federation 如何横向扩展(第 9、10 节),并给出配置示例(第 8 节)与最佳实践清单。
新手建议顺序读:每节都是「问题 → 机制 → 比喻 → 结论」的结构。只想速查的读者:配置看第 8 节,选型看第 10 节,运维清单看文末;常见疑问查附录 FAQ。
1 为什么 NameNode 是单点故障¶
在 HDFS 架构原理 中讲过:HDFS 的所有文件元数据(目录树、文件→块映射、块→Datanode 映射)全部常驻 NameNode 内存,客户端的一切元数据操作(create、open、mkdir、rename、list)都必须经过它。这意味着:
- NameNode 是整个集群的唯一元数据入口,它不参与数据搬运,但数据搬运(读/写块)的前提是先问它「文件在哪些块、块在哪些机器上」。
- NameNode 挂了,Datanode 和块数据都还在,但没有任何进程能回答"文件在哪",整个集群对外不可用——不是数据丢了,是"目录柜台关门了"。
- 更致命的是元数据变更(写操作)先落 EditLog 再改内存,NameNode 一死,未 checkpoint 的 EditLog 与 FSImage 的合并进度决定了恢复时间和完整性——单点故障不只是"不可用",还可能叠加恢复慢、丢失已确认操作的风险。
类比:Datanode 是摆满书的书架,NameNode 是图书馆的中央目录。书架烧了只损失一部分书(且多副本在别处),中央目录烧了,整个图书馆立刻停摆——哪怕书一本没少。
1.1 传统方案为什么都不是热备(常见误区纠错)¶
很多人以为 SecondaryNameNode 是"备份 NameNode",这是最常见的误解,必须纠正:
| 传统方案 | 实际作用 | 是否热备(实时持有最新元数据) | 能否自动接管 |
|---|---|---|---|
| SecondaryNameNode | 定期(默认 1 小时)从主 NN 拉取 EditLog 合并出新的 FSImage,减轻主 NN 压力、缩短重启时间 | 否,只是"归档员",其持有状态滞后于主 NN | 不能 |
| Checkpoint Node | 与 SecondaryNameNode 同职,换了个更准确的名字(SecondaryNameNode 在 Hadoop 2.x 中已标记 deprecated) | 否 | 不能 |
| Backup Node | 维护内存命名空间的实时副本,理论上可被 promote 为 Active | 是(内存级实时) | 不能,需手工 promote;Hadoop 2.6 起已标记 deprecated,3.x 中移除 |
关键纠错点:SecondaryNameNode / Checkpoint Node 的名字极具误导性——它们既不是备份、也不参与故障转移,本质只是"FSImage 合并助手"。NameNode 宕机时它们甚至可能因为自己持有的状态落后于 EditLog 尾段,无法直接顶替。Backup Node 虽然持有实时副本,但同样没有自动故障转移与脑裂防护机制,故障时需要人工介入,无法满足 HA 的"自动、快速、不丢元数据"三个要求。
本节结论:NameNode 是单点故障,传统方案只能缓解恢复时间、不能消除故障窗口;真正的解法是让"元数据状态"多机共享、并让接管过程自动化——这就是 HA 架构。
2 HA 架构总览:三组件各司其职¶
HDFS HA 的思路与数据库双机几乎一致:一份元数据状态、两个进程、一套仲裁机制。它的三个组件恰好对应"状态、存储、仲裁"三个问题:
| 组件 | 部署形态 | 职责 | 通俗类比 |
|---|---|---|---|
| Active / Standby NameNode | 两台物理机,各跑一个 NameNode | 同一时刻只有一个 Active 对外服务;Standby 周期性追读 EditLog 同步内存状态(默认每 60 秒检查新日志),切换时先强制追平再顶替 | 正驾驶 / 副驾驶(副驾全程盯仪表盘) |
| JournalNode(QJM) | ≥3 台机器上的小进程 | 共享存储 EditLog,用多数派共识保证写入不丢、不重、不乱 | 董事会会议纪要,须多数董事签字才算数 |
| ZKFC(ZooKeeper Failover Controller) | 每个 NameNode 主机上一个独立进程 | 健康监控 + 基于 ZooKeeper 锁的自动选举与切换 | 裁判 + 接力棒(棒在谁手里谁就是正驾驶) |
整个架构的「一张图」:
graph TB
CL["客户端<br/>只认 hdfs://mycluster/"] --> ANN["Active NameNode<br/>正驾驶"]
ANN -->|"写 EditLog,须多数派确认"| JN["JournalNode ×3<br/>共同记账的董事会"]
JN -->|"Standby 持续追读"| SNN["Standby NameNode<br/>副驾驶"]
ZKFC1["ZKFC ①"] -. 争抢同一把锁 .-> ZK["ZooKeeper ×3<br/>裁判席"]
ZKFC2["ZKFC ②"] -. 争抢同一把锁 .-> ZK
ZKFC1 --- ANN
ZKFC2 --- SNN
这张图就是全文的骨架:客户端只认逻辑名 mycluster;两个 NN 谁当 Active 由 ZKFC 在 ZooKeeper 里抢锁决定;两个 NN 的状态靠共享 EditLog 保持同步。 下面三节逐个展开。
2.1 双机 NameNode:状态周期性追平是"热备"的关键¶
- Active 正常处理所有客户端请求,每次元数据变更先写共享 EditLog(见下节),成功后才返回客户端成功。
- Standby 持续读取共享 EditLog 并应用到自己的内存命名空间,同时也接收 Datanode 的块报告,维护与 Active 几乎一致的块位置映射——这正是它和 Backup Node 的本质区别:Standby 不只是"知道元数据",连"数据在哪"都随时就绪,切换后才不用等全量块报告。
Standby 的追读不是「实时」,而是「周期性追平」:它按 dfs.ha.tail-edits.period(默认 60 秒)的周期到 JournalNode 检查有没有新 edits 段,有新段则下载并重放进内存——因此平时 Standby 的内存状态最多落后 Active 约 1 分钟(追读本身通常毫秒~秒级)。EditLog 的权威副本始终在 JournalNode 上:Active 是「共写者」,Standby 是「追读者」。
切换有多快?当选的新 Active 在上岗前会先把 JN 上剩余的所有已提交事务追平(catch-up,通常毫秒~秒级,落后越多耗时越长),因此已提交的元数据操作一条不丢(RPO = 0);切换总耗时取决于健康检测间隔 + fencing + 这段追平时间,通常数十秒级,远快于从 FSImage + EditLog 重放恢复。对比:非 HA 的 NameNode 重启要从 FSImage 加载再重放 EditLog,TB 级元数据的启动时间以分钟计,且主 NN 崩溃瞬间未 checkpoint 的 EditLog 有丢失风险。
HA 也可以不自动——用 hdfs haadmin -failover nn1 nn2 手工切换(在维护场景很有用,见最佳实践)。但自动故障转移才是真正消除窗口的方案,交给 ZKFC。
本节结论:双机 NN 让"元数据状态"有第二份周期性追平的副本(默认滞后 ≤1 分钟),切换时强制追平后上岗,切换速度从"重启加载"变成"秒级顶替",且不丢已提交事务——这是 HA 的第一根支柱。
3 JournalNode 与 QJM:EditLog 的多数派共识¶
两个 NameNode 必须看到同一份元数据变更记录,否则各自内存状态会分叉。这份记录就是 EditLog。问题来了:EditLog 放哪?
- 放 Active 本地磁盘?Standby 读不到(且 Active 挂了就丢了)。
- 放 NFS 共享盘?NFS 本身就是单点,且历史上 HDFS 的 NFS 方案(shared edits)在 NFS 故障时会留下未确定状态——单点故障从 NameNode 转移到 NFS。
QJM(Quorum Journal Manager,法定人数日志管理器)是官方推荐的共享 EditLog 方案:由一组 JournalNode 进程共同保管 EditLog,写入采用"多数派确认"协议:
- Active 把每条元数据变更(一个事务)追加发送给所有 JournalNode;
- 只要多数派(quorum = N/2+1)JournalNode 确认落盘,该事务即视为提交成功,Active 才向客户端返回成功;
- 少数派 JN 之后会被补齐(对客户端无感);
- Standby 持续从 JN 读 EditLog 段,逐条应用到自己的内存命名空间。
这个过程可以这样理解:EditLog 不是"存在一台机器上",而是"由一伙机器共同记账,多数人签字才算数"。任何单台 JN 挂掉、甚至任意 (N-1)/2 台挂掉,都不影响记账继续——因为只要有 N/2+1 台确认,就能保证"已提交事务必然在某个存活副本上",新 Active 接手后绝不缺账。
3.1 为什么必须奇数个、最少 3 个¶
多数派 = N/2 + 1,用这个公式一算就清楚:
| JN 数量 | 多数派要求 | 可容忍故障数 | 说明 |
|---|---|---|---|
| 1 | 1 | 0 | 单点,等于没做 HA |
| 2 | 2 | 0 | 挂 1 台就凑不齐多数派,与 1 台容错相同但更贵 |
| 3 | 2 | 1 | 官方推荐的最小配置 |
| 5 | 3 | 2 | 更大冗余,成本线性上升 |
| 4 | 3 | 1 | 与 3 台容错能力相同,白多花一台机器的钱 |
3.2 JournalNode 的部署与目录结构¶
JN 的落地细节(以 Apache Hadoop 官方文档为准):
- 初始化:首次搭 HA 时,JournalNode 启动后、NameNode 格式化之前,需执行
hdfs namenode -initializeSharedEdits——在 JN 上创建该 nameservice 的 edits 目录与初始状态。漏掉这步,NameNode 启动会报「共享 edits 目录不存在」。 - 目录结构:JN 本地目录(
dfs.journalnode.edits.dir)下按 nameservice 分子目录,存放edits_inprogress_*(正在写入的段)与以起止 txid 命名的edits_0000000000000000001-0000000000000000100形式的段文件及VERSION文件。NameNode 本地 edits 目录与 JN 上的段命名一一对应——这正是 Standby 能按 txid 顺序追账的基础。 - 段的清理:checkpoint 之后(HA 集群由 Standby 执行),已合并的旧 edits 段会被清理,JN 磁盘不会无限增长。
- 运维注意:JN 磁盘写满或 IO 严重抖动时,多数派确认无法完成,Active 的元数据写会阻塞等待——表现为集群所有写操作卡住。JN 的磁盘容量与 IO 应纳入日常巡检;升级/重启 JN 必须逐个进行(最佳实践第 9 条)。
本节结论:JournalNode 必须奇数个且至少 3 个(容忍 1 台故障),偶数没有额外容错收益——多数派共识决定了 JN 数量必须是 2N+1 的形态。 还要注意 JN 是独立小进程(默认端口 8485),它的本地存储目录必须显式配置 dfs.journalnode.edits.dir 并放在持久化磁盘上——默认落在 /tmp 下,重启即丢,这是最常见的部署事故之一。
4 ZKFC:基于 ZooKeeper 的自动故障转移¶
有了双机 NN 和 JN,还缺最后一块拼图:谁来决定"当前 Active 不行了,换人"?这个角色是 ZKFC(ZooKeeper Failover Controller,ZooKeeper 故障转移控制器)——每个 NameNode 主机上跑一个的独立守护进程,干两件事:
4.1 健康监控(HealthMonitor)¶
ZKFC 内部有一个健康监控线程,周期性通过 RPC 探测本机 NameNode 的状态(active / standby / 无响应),还监控 NameNode JVM 是否存活。监控对象是"本机 NN",各管各的——不存在一个中心裁判去 ping 所有人。
4.2 基于 ZooKeeper 锁的选举(ActiveStandbyElector)¶
两台 ZKFC 在 ZooKeeper 里争抢同一个临时 znode(锁)(路径形如 /hadoop-ha/<nameservice>/ActiveStandbyElectorLock):
- 抢到锁的 ZKFC 通知本机 NameNode 转成 Active,自己成为"持棒者";
- 持棒者通过 ZooKeeper 会话心跳维持锁;只要 ZKFC 进程活着且与 ZooKeeper 网络通畅,锁就一直持有;
- 一旦本机 NN 健康检查失败、ZKFC 崩溃、或与 ZooKeeper 网络断开(会话超时),临时 znode 自动消失,锁自动释放——另一个 ZKFC 通过 watch 机制立刻得知,重新竞争锁并当选。
这里藏着 ZooKeeper 不可替代的价值:临时节点 + 会话超时 = "锁与持有者的心跳绑定"。 如果只是普通文件锁,持有者崩溃后锁永远占着(死锁);ZooKeeper 的会话机制保证"持有者失联,锁自动归还",这是分布式故障场景下仲裁的基石。就像接力赛的接力棒上绑着心率仪——选手心跳一停,棒就自动掉回赛道,下一个选手马上捡起来。
本节结论:ZKFC = 健康监控 + 锁竞争,把"谁当 Active"变成一次分布式选举;而锁的自动释放机制,正是自动故障转移能"自动"的前提。 注意区分挂的是哪一侧:Active 侧 ZKFC 挂掉(或与 ZooKeeper 断连)会触发锁释放、自动 failover——这是机制在正常工作;Standby 侧 ZKFC 挂掉才是隐性单点——服务照常、无人报错,但此后 Active 再出事就没人接管了。两侧 ZKFC 都要随 NameNode 守护(hdfs zkfc 启动,start-dfs.sh 默认会拉起来)并纳入监控。
5 脑裂与 fencing:切主前的最后一道保险¶
自动故障转移最危险的情况不是"换错人",而是换人的瞬间出现两个 Active——这叫脑裂(split brain)。
5.1 脑裂怎么发生¶
设想:Active 的 NameNode 与 ZooKeeper 之间的网络断了,但进程本身没死,还在正常服务客户端、还在写 EditLog。ZooKeeper 那边因为会话超时释放了锁,另一个 ZKFC 当选,把自己的 NameNode 提升为 Active。此刻:
- 旧 Active 仍以为自己是 Active(它感知不到 ZooKeeper 的判决);
- 两台 NN 同时在写共享 EditLog → 元数据分叉,整个文件系统状态被破坏,比单点故障严重得多——单点只是不可用,脑裂是数据损坏。
用「双队长」来理解:一支队伍有两个队长,一个以为另一个阵亡了,两个人都发号施令,队伍的行动记录就乱了套。所以新队长上任前必须确认旧队长已经"物理上无法再发令"——这个隔离动作就是 fencing(围栏/隔离)。
5.2 fencing 怎么做¶
新 Active 的 ZKFC 在提升本机 NN 之前,按 dfs.ha.fencing.methods 配置的顺序执行隔离,常见两种:
| fencing 方法 | 做法 | 适用场景 |
|---|---|---|
sshfence |
SSH 到旧 Active 主机,执行命令杀死其 NameNode 进程 | 有 SSH 免密、可控同一批主机 |
shell(...) |
执行自定义脚本(如关防火墙、拔网卡、下电等) | 需要自定义隔离动作 |
但 fencing 也不是 100% 可靠(比如 SSH 不通)。因此还有最后一道防线:QJM 的 epoch 机制——每次选举产生新 Active,ZKFC 都会让 QJM 提升一个 epoch 号,JN 只接受当前 epoch 的写请求,旧 Active 拿着旧 epoch 来写会被直接拒绝。即便 fence 脚本失败,旧 Active 也无法污染共享 EditLog。这就像「门禁卡每换届一换」:就算旧队长没被反锁在门外,他的旧卡也已经刷不开记账室的门了。
为什么旧 Active 一定写不进去? 拆开四步:
- 每个 JournalNode 在本地持久化一个「当前届次」号(
lastPromisedEpoch,记录在 JN 存储目录的 VERSION 文件中,重启不丢); - NameNode 每次写 EditLog,请求里都携带自己的 epoch 号(相当于每份文件盖「第 N 届」的章);
- 新 Active 上岗前向 JN 集群请求提升 epoch(JN 只允许增加、拒绝回退),从这一刻起 JN 只认新届次的写请求;
- 旧 Active 再写,JN 发现「你的届次小于我承诺的届次」→ 拒绝该请求,旧 Active 随即意识到自己被罢免。
关键:旧 Active 没法伪装新届次——提升 epoch 是选举协议的一部分,由新 Active 执行、JN 确认;旧 Active 与 ZooKeeper 断连、没参与选举,根本无从得知新号是多少,只能拿着旧号反复被拒。所以 sshfence 做的是「物理隔离」,epoch 做的是「逻辑隔离」——脚本失败没关系,届次升级这关它永远过不去。
顺带澄清:epoch 在一个 Active 任期内保持不变,只在换届时提升——它标识的是「任期」而不是「机器」。同一任期内所有写请求用同一个号;同一个物理 NameNode 第二次当选 Active,用的也是新的届次号(JN 只增不减,从不复用旧号)。这个概念与 Raft 的 term、ZooKeeper ZAB 的 epoch 完全同构:用单调递增的届次号,让「过期的队长」无论怎么闹都写不进账本。
本节结论:fencing 是"宁可错杀、不可留患"的强隔离,QJM epoch 是兜底的双保险——两套机制叠加,脑裂才在工程上被压到极小概率。 这也是为什么「新 Active 必须 fencing 成功后才 transitionToActive」是硬性顺序,不可颠倒。
6 自动故障转移全流程¶
把前几节的零件拼起来,一次自动故障转移是这样的:
flowchart LR
A["① 健康监控<br/>发现 Active 失联"] --> B["② 选举<br/>锁自动释放,Standby 侧当选"]
B --> C["③ fencing 隔离<br/>封杀旧 Active"]
C -->|"成功"| D["④ 切换<br/>新 Active 上岗服务"]
C -->|"失败"| E["中止切换<br/>宁可不可用,绝不脑裂"]
- 健康监控:Active 侧 ZKFC 的 HealthMonitor 发现本机 NN 无响应(或 JVM 崩溃,或与 ZooKeeper 的会话超时被判定失联);
- 选举:锁(临时 znode)释放 → Standby 侧 ZKFC 收到 watch 通知 → 竞争创建锁节点成功,赢得选举(若本机 NN 自身不健康则不会参选);
- 切换:赢家 ZKFC 先对旧 Active 执行 fencing(sshfence / shell),确认隔离成功后,再通知本机 NN
transitionToActive——此时旧 Active 的写路径已被物理/逻辑切断,新 Active 先追平 JN 上剩余的全部已提交事务(第 2.1 节),再直接开始服务。
整个过程对客户端是无感的(下一节讲客户端透明),对运维人员则是一行日志:谁在什么时间触发了 failover、fencing 是否成功。自动故障转移的本质是把「检测 → 仲裁 → 隔离 → 切换」四个步骤全部自动化,其中任何一步失败(如 fencing 超时)都会中止切换,宁可不可用也不脑裂。
本节结论:健康监控→选举→切换三步环环相扣,fencing 失败即中止——HA 的第一原则是"绝不牺牲一致性换取可用性"。
7 客户端透明:nameservice 与 logical URI¶
HA 做好了,客户端也得配合:客户端不能写死某个 NN 的 IP,否则 Active 换了它也不知道。HDFS 的解法是 logical URI(逻辑 URI)+ nameservice(命名服务):
- 集群定义了一个逻辑名字,比如
mycluster(对应dfs.nameservices); - 客户端统一用
hdfs://mycluster/data/...访问,从不关心 Active 到底是哪台机器; - 客户端侧通过 proxy provider(如
ConfiguredFailoverProxyProvider)持有该 nameservice 下所有 NN 的地址,连接失败时自动尝试下一个并重连,failover 对客户端完全透明。
类比:nameservice 相当于数据库连接串里的 VIP/负载均衡域名——应用只认域名,不认具体物理机。运维做切换、扩容、甚至整机替换,应用侧一行配置都不用改。
当然你也可以写 hdfs://ActiveNameNodeIP:port/data/... ,只是如果做了切换或者这台服务器挂了,你就连不上了。
本节结论:nameservice 逻辑名把"物理 NN 是谁"与"客户端怎么连"彻底解耦,这是 HA 能落地到业务的前提——否则切了主,业务还死盯着旧 IP。
8 hdfs-site.xml 关键配置示例¶
下面是一份完整的 HA 核心配置(Hadoop 3.x 风格,2.x 的 HTTP 端口不同,见注释)。参数名请以 Apache Hadoop 官方 HA 文档为准,本示例为最常见配置组合,参数的名称都差不多,但不同厂家发行版各个组件的缺省端口会不一样,具体请查看对应的官方文档或咨询技术支撑人员:
<!-- hdfs-site.xml(Active/Standby NameNode 与 JournalNode 主机共同持有) -->
<configuration>
<!-- 逻辑命名空间名,客户端 hdfs://mycluster/... 用它 -->
<property>
<name>dfs.nameservices</name>
<value>mycluster</value>
</property>
<!-- 该 nameservice 下两个 NameNode 的逻辑标识 -->
<property>
<name>dfs.ha.namenodes.mycluster</name>
<value>nn1,nn2</value>
</property>
<!-- 两个 NN 的 RPC 地址(8020 为默认端口) -->
<property>
<name>dfs.namenode.rpc-address.mycluster.nn1</name>
<value>nn1-host:8020</value>
</property>
<property>
<name>dfs.namenode.rpc-address.mycluster.nn2</name>
<value>nn2-host:8020</value>
</property>
<!-- Web UI 地址(Hadoop 3.x 默认 9870;2.x 为 50070) -->
<property>
<name>dfs.namenode.http-address.mycluster.nn1</name>
<value>nn1-host:9870</value>
</property>
<property>
<name>dfs.namenode.http-address.mycluster.nn2</name>
<value>nn2-host:9870</value>
</property>
<!-- 共享 EditLog:qjournal 协议,分号分隔的 3 台 JN,8485 为 JN 默认端口 -->
<property>
<name>dfs.namenode.shared.edits.dir</name>
<value>qjournal://jn1-host:8485;jn2-host:8485;jn3-host:8485/mycluster</value>
</property>
<!-- 客户端故障转移:持有 NN 列表,Active 失败自动重连 -->
<property>
<name>dfs.client.failover.proxy.provider.mycluster</name>
<value>org.apache.hadoop.hdfs.server.namenode.ha.ConfiguredFailoverProxyProvider</value>
</property>
<!-- fencing:先 ssh 杀掉旧 Active 的 NN 进程;可配置多个方法按顺序执行 -->
<property>
<name>dfs.ha.fencing.methods</name>
<value>sshfence</value>
</property>
<property>
<name>dfs.ha.fencing.ssh.private-key-files</name>
<value>/home/hdfs/.ssh/id_rsa</value>
</property>
<!-- 自动故障转移开关 + ZooKeeper 集群地址 -->
<property>
<name>dfs.ha.automatic-failover.enabled</name>
<value>true</value>
</property>
<property>
<name>ha.zookeeper.quorum</name>
<value>zk1-host:2181,zk2-host:2181,zk3-host:2181</value>
</property>
</configuration>
<!-- JournalNode 主机上的 hdfs-site.xml:JN 本地 EditLog 存储目录(必配,勿用默认 /tmp) -->
<configuration>
<property>
<name>dfs.journalnode.edits.dir</name>
<value>/data/hdfs/journalnode</value>
</property>
</configuration>
<!-- core-site.xml(所有客户端与 NN 主机):默认文件系统指向逻辑名 -->
<configuration>
<property>
<name>fs.defaultFS</name>
<value>hdfs://mycluster</value>
</property>
</configuration>
配置完成后还有两个关键初始化动作,缺一个都起不来:
- 首次启用自动故障转移前,在任意一台 NN 主机执行
hdfs zkfc -formatZK,在 ZooKeeper 里初始化 HA 状态(会清空该 nameservice 在 ZK 中的旧数据,切勿在已运行集群上随意执行,先确认官方文档的迁移步骤); - 启动顺序:先起 JournalNode(
hdfs --daemon start journalnode),再起两个 NameNode 与 ZKFC(或直接start-dfs.sh)。
手动切换命令(维护场景):hdfs haadmin -failover nn1 nn2 优雅切换,hdfs haadmin -getAllServiceState 查看当前状态。
本节结论:HA 配置的核心就三组参数——nameservice 定义(NN 地址)、qjournal 共享 EditLog、ZKFC/fencing;参数名与初始化顺序以官方 HA 文档为准,生产变更前务必先在测试环境验证。
9 Federation 联邦:突破单 NameNode 内存瓶颈¶
HA 解决了"可靠",没解决"容量":NameNode 元数据全驻内存,单台机器内存是上限。几十亿文件时,一台 NN 内存动辄上百 GB,成了扩不动、也压不垮别人的瓶颈。Federation(联邦)把问题"横切"开来:
先打个比方:一个超级图书馆藏书太多,一本中央目录记不下了。Federation 的做法是把图书馆拆成几个分馆——每个分馆有自己的中央目录(一组 NameNode),但书仍然放在同一个大仓库(共享的 Datanode 集群)里;大厅里立一块总指示牌(ViewFS),告诉读者「数学书去 2 楼、小说去 3 楼」。 对应到机制上:
- 部署多组独立命名空间(namespace),每组有自己独立的 NameNode(及自己的 FSImage/EditLog),各自管自己目录下的文件;
- 所有 namespace 共享同一批 Datanode 集群:Datanode 会向每个 namespace 注册,数据按 block pool(块池) 隔离——每个 namespace 一个块池,块池之间互不干扰;
- 客户端侧用 ViewFS(虚拟文件系统) 的挂载表(mount table) 把多个 namespace 挂到不同路径前缀下,对应用呈现"一个文件系统"。
挂载表配置示例(core-site.xml):
<configuration>
<property>
<name>fs.defaultFS</name>
<value>viewfs://clusterX/</value>
</property>
<!-- 把 /data 目录的请求路由到 ns-data 这个命名空间 -->
<property>
<name>fs.viewfs.mounttable.clusterX.link./data</name>
<value>hdfs://ns-data/</value>
</property>
<!-- 把 /logs 目录的请求路由到 ns-logs 这个命名空间 -->
<property>
<name>fs.viewfs.mounttable.clusterX.link./logs</name>
<value>hdfs://ns-logs/</value>
</property>
</configuration>
这段配置的语义类似 Linux 的 /etc/fstab:ViewFS 是客户端侧的"挂载表",viewfs://clusterX/data/... 在客户端本地就被解析转发到对应的 nameservice。这样业务看到的还是一个统一路径空间,管理员却可以按目录把不同业务拆到不同 namespace 上——命名空间按业务/租户隔离,单个 NN 的内存压力随之分摊。
本节结论:Federation = 多组 NN 分管不同命名空间 + 共享 Datanode + ViewFS 客户端挂载表,把"单 NN 内存上限"变成"按目录横向切分",还顺带获得故障隔离(一个 namespace 挂了不影响其他)。
10 HA 与 Federation 的组合¶
Federation 的每个 namespace 可以各自独立配置 HA,两者是正交的两个维度,可以自由组合:
| 组合形态 | 架构 | 解决什么问题 | 代价 |
|---|---|---|---|
| 单 NN | 1 台 NN | 最简单 | 单点故障 + 内存上限 |
| 单 NN + HA | 2 NN + 3 JN + ZKFC | 消除单点 | 机器翻倍 |
| Federation | 多组单 NN 共享 Datanode | 内存上限、租户隔离 | 需要 ViewFS,运维复杂 |
| Federation + HA | 每组 2 NN + 3 JN + ZKFC | 同时解决可靠与容量 | 成本与运维复杂度最高 |
生产实践中,Federation 的每个 namespace 通常都配 HA:各组 NN 共用同一套 JournalNode 集群(或各自独立一组 JN,取决于隔离要求),ZKFC 各自管理本组的自动故障转移。ViewFS 挂载表里指向的正是各 namespace 的 nameservice 名——逻辑 URI 那一层天然与 Federation 兼容,客户端完全无感。
本节结论:HA 解决"可靠性",Federation 解决"可扩展性",两者组合后每个命名空间都具备自动故障转移能力;选型上,多数中小集群只做 HA 即可,Federation 是单 NN 内存确实不够时的演进方向。
最佳实践清单¶
- JournalNode 固定部署 3 台(奇数),与 NameNode 分机部署 —— 为什么:3 台容忍 1 台故障(多数派 = 2/3),JN 与 NN 同机意味着一次硬件故障同时带走"记账员"和"主驾驶",多数派优势被削弱。
- 显式配置
dfs.journalnode.edits.dir到持久化磁盘 —— 为什么:默认落在 /tmp,机器重启即丢 EditLog 目录,QJM 会认定 JN 状态失效。 - 启用自动故障转移后,先做一次受控演练 —— 为什么:用
hdfs haadmin -failover在低峰期实测切换,验证 sshfence 免密、ZK 连接、客户端重连三者都通,避免首次真实故障时才发现配置问题。 - 所有客户端一律使用 nameservice 逻辑名,禁止直连 NN IP —— 为什么:直连 IP 等于把 HA 架空,failover 对直连客户端不可见,业务会持续报连接失败。
- 两侧 ZKFC 进程都纳入监控(进程存活 + 心跳延迟) —— 为什么:Active 侧 ZKFC 挂掉会触发切换(可感知);Standby 侧 ZKFC 挂掉则静默退回「无自动切换」,且不报任何错——后者是最容易漏掉的隐性单点。
- ZooKeeper 自身至少 3 台并独立部署 —— 为什么:ZooKeeper 是仲裁层,它挂掉则选举失灵;ZK 与 JN 同为 3 台奇数,语义一致:多数派可用才可用。
- fencing 配置不只一种方法,按顺序多配一层 —— 为什么:sshfence 遇到 SSH 故障就失效,追加
shell(...)等兜底方法(配合 QJM epoch 双保险),把脑裂概率压到最低。 - 维护时用
haadmin -failover优雅切换,不直接 kill 进程 —— 为什么:优雅切换先冻结写入再交接,避免"直接 kill 造成未确定状态 + 意外触发自动切换"的双重风险。 - JN 升级/滚动重启逐个进行,保证多数派在线 —— 为什么:同一时刻最多停 1 台 JN(3 台配置下),否则 EditLog 写入将因凑不齐多数派而阻塞,Active 会一直等不到确认。
- Federation 立项前先按目录规划好 namespace 划分 —— 为什么:ViewFS 挂载表建立后再大改目录归属代价极高(要迁移文件与挂载点),先按业务/租户边界切分,比事后拆容易得多。
附录:新手常见疑问(FAQ)¶
Q1:HA 集群里,Standby NameNode 平时在干什么? 两个动作:持续从 JournalNode 追读 EditLog 并应用到自己的内存命名空间;同时接收 Datanode 的块报告。所以它不只是「知道元数据」,连「数据在哪」都随时就绪——切换后不用等全量块报告(第 2 节)。
Q2:故障转移会丢数据吗? 已提交的元数据操作一条不丢(RPO = 0)——它们都落在多数派 JournalNode 上,新 Active 上岗前会强制追平剩余事务再开始服务(第 2 节)。客户端没收到成功确认的写入可能丢失——这与任何数据库的故障语义一致。
Q3:切换要多久? 通常数十秒级(健康检测间隔 + fencing + 追平剩余 edits 的时间)。其中追平通常毫秒~秒级,但 Standby 平时落后越多(如刚重启过),这次追平就越久。对比非 HA 的重启恢复(TB 级元数据以分钟计),这就是「秒级顶替」的含义(第 2 节)。
Q4:JournalNode 为什么不能 2 台或 4 台? 多数派 = N/2+1:2 台需要 2 台确认(挂 1 台就停摆),4 台需要 3 台(容错 1 台)——偶数台要么零容错、要么容错与更少的奇数台相同,纯属浪费。3 台(容错 1 台)和 5 台(容错 2 台)是标准形态(第 3 节表格)。
Q5:共享 EditLog 用 NFS 行不行? 不行。NFS 本身就是单点——NameNode 的单点故障只是从 NN 转移到了 NFS,且 NFS 故障时会留下未确定状态。QJM 的多数组共识正是为了消灭「存储层单点」(第 3 节)。
Q6:ZKFC 进程挂了会怎样? 看挂的是哪一侧:Active 侧 ZKFC 挂掉(或与 ZooKeeper 断连)→ 锁自动释放 → Standby 侧 ZKFC 接管,触发一次自动 failover——这是机制在正常工作。Standby 侧 ZKFC 挂掉才是隐患:服务照常、无人报错,但此后 Active 再出事就没人接管了(第 4 节)。所以最佳实践第 5 条要求两侧 ZKFC 都纳入监控。
Q7:什么时候该上 Federation? 只有单 NameNode 的内存确实不够时(几十亿文件、堆内存上百 GB 且扩不动)。它带来 ViewFS 和运维复杂度,多数中小集群只做 HA 就够(第 10 节选型表)。
Q8:脑裂和单点故障,哪个更严重? 脑裂。单点只是「不可用」,脑裂是「两个 Active 同时写 EditLog → 元数据分叉 → 数据损坏」。所以 HA 的设计原则是:fencing 失败就中止切换,宁可不可用,绝不脑裂(第 5、6 节)。
参数与默认值以 Apache Hadoop 官方文档(HDFS HA 与 ViewFs 章节)为准;本文覆盖 Hadoop 2.x/3.x 通用机制,具体版本行为以官方 release notes 为准。