NameNode 元数据持久化:FsImage、EditLog 与启动恢复
引言
NameNode 是 HDFS 的大脑,整个命名空间——目录树、文件到 block 的映射、权限、副本数、时间戳——常驻内存。内存意味着快,也意味着一旦进程退出就什么都没了。所以元数据的持久化,是 NameNode 能活下来的命脉。这篇不重复 HA 与 Federation,专门讲元数据本身:FsImage 和 EditLog 怎么存、checkpoint 怎么做、冷启动怎么恢复、坏了怎么修。
我在某集群上运维过约 12 亿 block 规模的 HDFS,NameNode 重启一次要四十多分钟,期间业务全部卡死——这种痛让我对元数据链路格外较真。下面把这条链路从写到读、从正常到异常,完整拆一遍。
两类元数据
NameNode 的持久化数据只有两类文件,都落在 dfs.namenode.name.dir 指向的本地磁盘上。
| 类型 | 角色 | 内容 | 生成时机 |
|---|---|---|---|
| FsImage | 全量快照 | 目录树、文件→block 列表、权限、副本数、时间戳 | checkpoint 时生成 |
| EditLog | 增量日志 | FsImage 之后每一次状态变更(创建/删除/改名/副本变更等) | 实时 append |
一个简单的等式贯穿始终:
1 | 内存当前状态 = 最新 FsImage + 回放其后所有 EditLog |
FsImage 是个大而全的序列化文件,二进制格式(Hadoop 2.x 起支持 protobuf),写起来慢、读起来也慢,但能完整描述某一个时刻的命名空间。EditLog 是预写日志(WAL),每次只追加一条记录,写快、读慢(要顺序回放)。两者配合,本质是”全量 + 增量”的经典日志结构存储思路,和数据库的 checkpoint + redo log 是一套打法。
1 | FsImage (txid=1,000,000) |
文件名里的数字就是 txid 区间,启动时 NameNode 找到最大的 FsImage txid,然后回放所有 起始 txid > 该值 的 edits 文件。
写路径与事务
每一次让 NameNode 状态发生变更的操作——比如 mkdir /data/2026、create /log/app.log——都会被分配一个全局自增的事务号 txid,然后写入 EditLog。完整流程大致是:
- NameNode 分配下一个自增 txid
- EditLog append 一条记录(含操作类型、路径、参数、时间戳、txid)
- EditLog 强制刷盘到
dfs.namenode.edits.dir的所有目录(多目录同步写) - 视操作类型,通知 DataNode 执行块操作 / 等待副本数满足
- 更新内存命名空间
- 返回 client 成功
关键点:必须先落盘,再返回 client。这是 WAL 的铁律。如果先更新内存再写日志,进程崩了,client 收到了”成功”但元数据丢了,数据就和元数据对不上——HDFS 最深的恐惧就是 block 在 DataNode 上存在但 NameNode 元数据里没有对应文件,等于数据静默丢失。
dfs.namenode.edits.dir 默认等于 dfs.namenode.name.dir,可以单独配。生产上我习惯让 edits 和 fsimage 落同一批磁盘(顺序写、随机读的混合负载),也有人把 edits 单独放到更快的 SSD 上加速启动回放。
EditLog 的并发写通过 FSEditLog 的 double-buffer 实现:一个 buffer 给写线程追加,另一个 buffer 异步刷盘,两者交替使用,避免每次 append 都阻塞等 fsync。但每条 edits 是否真正持久化,仍取决于对应的 fsync 完成——这是 dfs.namenode.edits.dir 多目录的意义,多个 fsync 并行,但任一失败要回滚。
Checkpoint 机制
EditLog 会无限增长。如果不合并,启动时回放几亿条 edits 能把 NameNode 启动时间拖到几小时。所以需要周期性把 FsImage 和 EditLog 合并,生成新的 FsImage,把旧的 edits 清掉——这就是 checkpoint。
在非 HA 模式下由 SecondaryNameNode 完成;HA 模式下由 Standby NameNode 完成。两者流程一致:
1 | SecondaryNameNode / Standby NN: |
触发条件由几个参数控制:
1 | # 定时触发,默认 3600 秒(1 小时) |
前两个条件是”或”的关系,任意满足就触发。生产上我会把 num.checkpoints.retained 调到 4,留点回滚余地。还有个 dfs.namenode.checkpoint.check.period(默认 60 秒)是 SNN/Standby 检查触发条件的轮询间隔。
SaveNamespace 是 Active/Standby 执行 checkpoint 写出 FsImage 时的内部流程,它会拿全局写锁(FSNamesystem.writeLock()):
- 加写锁,阻塞所有新的写操作
- 滚动 edits(让后续写入落到新 edits 文件)
- 把内存命名空间序列化成 FsImage 文件
- 写一个
fsimage_0000000123+ 对应的 MD5 校验文件 - 持久化新的
seen_txid(记录已 checkpoint 到哪个 txid) - 释放写锁
这个写锁就是 SaveNamespace 卡顿的根源——文件数越多,序列化越久,写锁持锁越长,期间所有写请求排队。某集群 8 亿文件时,一次 SaveNamespace 能锁 90 秒,期间业务写入全部超时、client 重试风暴、甚至触发上层熔断。监控 SaveNamespace 耗时是 NameNode 运维的必备项。
启动恢复
NameNode 冷启动是元数据链路最脆弱的时刻。完整流程:
1 | NN 启动 |
Safemode 的退出由这几个参数控制:
1 | # 已达到最小副本数的 block 占比阈值,默认 0.999f |
冷启动为何慢?因为时间 = 加载 FsImage + 回放 edits + 等块报告,三者都和文件/block 数线性相关。某集群 12 亿 block,光等块报告就要二十多分钟——DataNode 上报是分批的,每批还要经过 NameNode 处理和校验。这也是为什么 HDFS 要搞 HA:Standby 常驻内存、持续回放,故障切换只要几秒到十几秒,不用冷启动。
safemode.extension 我习惯设大一点(比如 60 秒),让晚到的 DataNode 有机会补报,避免刚一退出 safemode 就因为缺副本触发大量补齐风暴,把网络和磁盘打满。
多目录冗余
dfs.namenode.name.dir 是 NameNode 最关键的冗余配置:
1 | # 多目录,逗号分隔,建议分布在不同物理磁盘 |
NameNode 对每个目录同步写一份完整的 FsImage 和 EditLog。任何一个目录损坏,NameNode 仍能从其他目录读出完整元数据启动。生产铁律:
- 至少 2 个目录,跨物理磁盘(不是同盘不同分区,那是骗自己)
- 有条件跨 RAID 卡或跨 JBOD,但 RAID 不是必需,NameNode 自己做了多副本
- 不要配太多,4 个足够,每多一个就多一份 fsync 开销,影响写吞吐
- 目录所在磁盘要监控 SMART 错误和坏道,预防性更换
我踩过一个坑:name.dir 配了 3 个目录,运维挂载磁盘时手滑把 /data2 挂成了 /data1 的拷贝,结果三个”目录”其实是同一个物理盘的两份拷贝——盘坏的时候两份一起没。挂载后务必 ls -li 看 inode、df 看设备号,确认是不同物理设备。
元数据损坏与修复实战
这是这篇最值钱的部分。元数据损坏分两种:EditLog 损坏和 FsImage 损坏,严重程度天差地别。
EditLog 损坏
表现:NameNode 启动时报 Corrupted EditLog 或 txid 断裂、回放到某条 edits 抛 IOException 或 EOFException。
典型原因:机器掉电、磁盘满、内核 panic 导致 edits 写到一半就被截断。EditLog 是 append-only,损坏通常发生在最后一条未完整刷盘的记录。
修复手段按严重程度递进:
- 自动 recover:NameNode 启动时如果检测到 edits 不完整,会尝试跳过最后一条坏记录继续回放。多数轻微损坏这样就能过。
hdfs namenode -recover:交互式恢复模式,会提示 “roll edits? (Y/N)” 之类。本质是回滚到最后一个完整 checkpoint,丢弃其后的所有 edits——意味着上次 checkpoint 之后的写操作全部丢失。所以要勤做 checkpoint。- 手动删坏 edits:极端情况下,定位到
edits_inprogress_xxxxx文件,备份后删除,让 NN 从上一个 FsImage 启动。丢的数据更多,但能起来。
-recover 的内部逻辑是:先尝试最近的 FsImage,再回放 edits;遇到坏 edits 就停在那条之前。所以最近一次 checkpoint 越新,丢的越少。这也是为什么 checkpoint 周期不能拉太长。
FsImage 损坏
表现:启动时加载 FsImage 报 MD5 mismatch 或反序列化失败、InvalidProtocolBufferException。
FsImage 损坏比 EditLog 损坏严重得多,因为 FsImage 是全量基线,丢了它等于丢了整个命名空间的起点。修复路径:
- 从其他 name.dir 恢复:多目录冗余的意义就在这里,某个目录的 fsimage 坏了,从另一个目录拷过来覆盖。
- 从 Standby/Secondary 拷贝:HA 集群里 Standby 时刻有较新的 FsImage,直接拷过来。
- 从异地备份恢复:定期把 FsImage 异地备份(见下文)。
最惨的情况:单目录、没 Standby、没备份,FsImage 损坏——基本就是数据全没。只能从 DataNode 的 block 反向重建目录树(用 hdfs debug + 人工拼凑),工程量极大且不保证完整。所以多目录 + 定期备份是底线,不是可选项。
离线分析:oiv 与 oev
hdfs oiv(Offline Image Viewer)和 hdfs oev(Offline Edits Viewer)是排查元数据问题的两把利器,不需要启动 NameNode 就能看 FsImage 和 EditLog 的内容。下面是一组实战命令示例:
1 | # 1. 把 FsImage 转成可读的 XML(小集群适用,大集群 XML 会很大) |
我用 oiv 做过的事:统计全集群小文件数量(FileDistribution 看小于 1MB 的 block 占比,一度占到 60%)、审计某目录下文件数(Delimited + grep + wc)、确认 checkpoint 是否成功(对比新旧 fsimage 的 txid 是否推进)。oev 则用来排查”为什么这次启动回放特别慢”——看 edits 里是不是被某个离线任务的批量 rename 灌了几百万条记录。
备份纪律
元数据备份是运维的底线:
- 定期把最新 FsImage + MD5 异地备份(重要集群每小时一次,普通集群每天一次)
- 备份到对象存储或异地 NAS,不要和 NameNode 同机房、同机架
- 备份脚本要校验 MD5,损坏的备份等于没备份
- 定期做”恢复演练”——拿备份的 FsImage 在测试集群起一遍,确认能加载、能回放
我见过最痛的事故:某集群两年没备份过 FsImage,主磁盘连环故障,最后从 DataNode block 反推目录树,用了三周才恢复 80% 的元数据,剩下 20% 永久丢失,业务层大量依赖路径的 ETL 任务重写。
HA 下的元数据同步
HA 模式下 Active/Standby 通过 QJM(Quorum Journal Manager)同步 edits:
- Active NN 写 edits 时,同时写本地和一组 JournalNode(通常 3 或 5 个,多数派成功才算提交)
- Standby NN 持续从 JournalNode 拉取 edits 并回放,保持内存状态紧随 Active
- 故障切换时,新 Active 先确保把 JournalNode 上所有已提交的 edits 回放完,再对外提供服务
这样冷启动的痛点被化解了——Standby 随时在回放,切换是热的,不需要加载 FsImage。QJM 的多数派写也取代了早期依赖共享存储(NFS/BookKeeper)的 edits 共享方案,避免了单点。细节属于 HA 篇,这里不展开,只强调一点:JournalNode 的可用性直接决定 edits 不丢,JN 至少 3 节点跨机架部署,且独立于 NameNode 所在机器。
性能与运维
元数据链路的常见性能问题:
- SaveNamespace 卡顿:文件数多时序列化慢,写锁持留长。缓解:勤做 checkpoint 让单次 fsimage 不要太大;升级到 Hadoop 3.x 用原生 protobuf 序列化;监控
SaveNamespace耗时并告警,超过阈值要查。 - EditLog 膨胀:checkpoint 不及时,edits 文件堆积,启动回放慢。检查
dfs.namenode.checkpoint.tx是否设得太高;SNN/Standby 是否健康在跑 checkpoint(看 JMX 的LastCheckpointTime)。 dfs.namenode.max.objects上限:默认 0(无限制),但文件/目录/block 总数受 JVM 堆内存约束。经验值:64GB 堆大约撑 4 亿文件。接近上限时要横向扩容(Federation)或治理小文件。- 启动超时:冷启动慢的本质是文件/block 多。除了做 checkpoint,还可以调大
dfs.namenode.handler.count让块报告处理并发更高,缩短 safemode 等待;dfs.namenode.blockreport.maxSize调大单次块报告体量。
小结
NameNode 元数据持久化就两件东西:FsImage 存全量快照,EditLog 存增量变更,内存状态等于两者之和。checkpoint 把它们周期性合并,防止 edits 无限膨胀;冷启动时先加载 FsImage 再回放 edits,进 safemode 等块报告达到阈值再退出。多目录冗余保命,定期异地备份保底,oiv/oev 是离线排查的瑞士军刀,-recover 是 EditLog 损坏的最后一道防线。HA 用 QJM 让 Standby 热备、持续回放,绕开了冷启动的痛。把这条链路理解透了,NameNode 才不会在你最不想它崩的时候崩。
