深入 Hadoop 04 - NameNode 内存模型与启动恢复
上一篇讲了读写路径。本篇展开 NameNode 内存里的元数据结构,以及 NameNode 启动时怎么把磁盘上的元数据恢复成内存镜像。
NameNode 常被描述成"HDFS 的元数据节点"。这个描述对应了职责但没解释机制。准确的说法是:NameNode 是一个把全部元数据驻留内存的进程,磁盘上同时维护两份互补的持久化文件——FSImage 是元数据镜像快照,EditLog 是镜像之后的增量操作日志,启动时把 FSImage 加载进内存、重放 EditLog,恢复到宕机前的最终状态。
本篇只抓一个问题:NameNode 内存里到底有哪些数据结构、FSImage 和 EditLog 是怎么配合的、为什么 NameNode 启动要进入 Safe Mode 等待块报告。
NameNode 内存里的六大数据结构
打开 NameNode 进程的内存,主要元数据可以分成六块:
1 | |
这六块里前三块是核心元数据,决定 NameNode 内存占用的 90%+。后三块是衍生状态——从 BlocksMap 计算得出,主要用于副本数管理。
FSDirectory 是路径树。每个目录是 INodeDirectory,每个文件是 INodeFile。INodeFile 内部记录这个文件被切成哪些 block(一个 BlockInfo 数组)。整个树用 ConcurrentHashMap 或者类似结构支持并发读写。
BlocksMap 是 HDFS 元数据的核心索引。给定一个 block ID,能立刻查到这个 block 的三个副本所在 DataNode。BlockInfo 对象里维护一个 Triple(三个 DatanodeStorageInfo 引用),任何副本变化都更新这里。
DatanodeManager 是 DataNode 注册表。每个 DataNode 第一次连接 NameNode 时被分配一个 datanodeId,NameNode 在内存里建一个 DatanodeDescriptor 跟踪这个 DataNode 的容量、最近心跳时间、当前持有 block 数等。
LeaseManager 是 HDFS 写并发控制的核心。客户端要写一个文件前必须从 NameNode 拿到这个文件的"软租约"(soft lease,默认 60 秒)。租约保证同一时间只有一个客户端能写这个文件——其他客户端的 create 请求会被拒绝。租约到期后 NameNode 自动释放,允许其他客户端接管。
元数据大小的精确估算
NameNode 内存占用大致可以用一组经验公式估算(Hadoop 3.3.x):
1 | |
这两组对比清楚展示了 HDFS 小文件问题的本质:元数据占用与文件大小无关,只与文件数 + block 数有关。1KB 小文件和 1GB 文件的元数据占用大致相同(如果 1GB 文件切成 8 个 block,元数据占用比单个 1KB 文件多 8 倍)。
这就是为什么 HDFS 设计假设文件尺寸巨大——大文件让元数据开销分摊到大量数据上,单字节元数据开销可以忽略。小文件场景下元数据开销甚至超过数据本身,NameNode 内存成为整个集群的瓶颈。
生产集群控制小文件数量是运维的核心主题。常见做法包括:
- 用 HAR(Hadoop Archive)把大量小文件打包成一个大文件
- 用 Sequence File 把大量小 KV 序列化成一个大文件
- 用 Hadoop 3.x 的 EC 把存储效率从 3x 提升到 1.5x(不解决元数据问题但缓解总存储压力)
- 在应用层做小文件合并(Hive 的小文件合并 job、Spark 的 coalesce 算子)
FSImage 与 EditLog 的分工
NameNode 把元数据持久化到磁盘时分两个文件:
FSImage 是元数据镜像快照。文件名形如 fsimage_0000000123,数字是事务 ID(txid)。这个文件包含截至某个 txid 的全部命名空间 + BlocksMap 状态。FSImage 是二进制格式(Hadoop 3.x 起可以选 PROTOBUF 格式),加载到内存后可以快速重建元数据镜像。
EditLog 是 FSImage 之后的增量操作日志。文件名形如 edits_inprogress_0000000124,记录从 txid 124 开始的所有元数据变更(创建文件、删除文件、添加 block 等)。EditLog 是 append-only 顺序写,性能极高。
NameNode 处理元数据变更的标准流程:
1 | |
步骤 2 是关键——必须先写 EditLog 再更新内存。如果顺序反了(先更新内存再写 EditLog),NameNode 在两步之间宕机会丢数据。EditLog 是 NameNode 持久化的唯一保证。
这个顺序决定了 HDFS 元数据操作的延迟下限。每次 create / mkdir / addBlock 都要 fsync 一次 EditLog 文件。fsync 在 SSD 上大约 100μs,HDD 上 1-10ms。如果集群每秒处理 10000 次元数据操作,EditLog fsync 是瓶颈。
Hadoop 2.x 引入了 EditLog 批量写优化——多个元数据操作可以批量 fsync,把 fsync 开销分摊到多个操作上。这个优化让 HDFS 元数据吞吐提升数倍,代价是单次操作延迟略有上升。
Checkpoint:合并 FSImage 与 EditLog
如果只追加 EditLog 不合并,时间久了 EditLog 会无限增长,NameNode 重启时重放 EditLog 耗时数小时。所以 HDFS 周期性把当前 FSImage + 当前 EditLog 合并成新的 FSImage,并把 EditLog 清空。这个操作叫 Checkpoint。
Checkpoint 的触发条件(Hadoop 默认值):
1 | |
执行 Checkpoint 的角色取决于部署模式:
非 HA 部署:Secondary NameNode。这不是 NameNode 的备用节点,而是一个独立进程,定期从 Active NameNode 拉 FSImage + EditLog,在本地合并,把新 FSImage 推回 Active NameNode。Secondary NameNode 不参与客户端请求,只做后台 Checkpoint。
HA 部署:Standby NameNode。Standby NameNode 同时承担"热备"和"Checkpoint"两个职责——它持续从 JournalNode 同步 EditLog、在内存里重放保持与 Active 同步,定期合并 FSImage 推回 Active。
Checkpoint 操作的标准流程:
1 | |
这个流程的关键是 Active NameNode 不参与合并计算(合并要重放 EditLog 是 CPU 密集型操作),由独立的 Checkpoint 节点承担。这是为什么 Secondary NameNode 在非 HA 部署里是标配——即使 NameNode 内存够大,没有 Secondary NameNode 做后台 Checkpoint,NameNode 重启时重放 EditLog 会非常慢。
Safe Mode:NameNode 启动的特殊状态
NameNode 启动时不能立刻接受写请求。它需要经过一个 Safe Mode 阶段,等集群块状态稳定后才能正常服务。
启动流程:
1 | |
为什么需要 Safe Mode?因为 NameNode 启动时内存里的 block → DataNode 映射表是 FSImage + EditLog 重放后的快照,但这个快照里记录的 DataNode 状态可能已经过时——某些 DataNode 在 NameNode 宕机期间自己也宕机了,某些 block 副本丢失了。Safe Mode 等待所有 DataNode 上报最新块报告,让 NameNode 重新计算每个 block 的实际副本数。
Safe Mode 离开条件(Hadoop 默认值):
1 | |
这三个参数控制 Safe Mode 的离开门槛。99.9% 看似很严格,但实际集群因为副本数 3 + 机架感知,绝大多数 block 在 DataNode 启动后立刻满足"最小副本数"条件,几分钟内就能达标。
Safe Mode 期间可以手动执行一些操作。hdfs dfsadmin -safemode get 查看当前状态。hdfs dfsadmin -safemode wait 阻塞等待 Safe Mode 离开(脚本常用)。hdfs dfsadmin -safemode enter 强制进入(运维场景,禁止写入)。hdfs dfsadmin -safemode leave 强制离开(慎用——可能让 NameNode 在副本数不足时就接受写入)。
实验:观察 FSImage 与 EditLog
在 NameNode 节点上找到 dfs.namenode.name.dir 配置的目录(通常 /var/lib/hadoop-hdfs/name),里面是 FSImage 和 EditLog 的实际文件:
1 | |
典型输出:
1 | |
文件名里的数字是 txid。fsimage_0000000123 表示截至 txid 123 的镜像,之后的元数据变更全部记录在 edits_124-150、edits_151-200、edits_inprogress_201 这些 EditLog 段里。
用 hdfs oiv(offline image viewer)可以解析 FSImage 内容:
1 | |
输出按行展示 FSImage 里的每个 INode(路径、所有者、副本数、修改时间等)。可以用 wc -l 粗略估算集群文件数。
用 hdfs oev(offline edits viewer)可以解析 EditLog 内容:
1 | |
输出 XML 格式的操作记录,每条记录是一个事务(OP_ADD、OP_RENAME、OP_CLOSE 等)。
seen_txid 文件记录 NameNode 已知的最大事务 ID。NameNode 启动时用这个数字判断 EditLog 是否完整——如果 fsimage_0000000123 之后没有 edits_124 开始的文件,说明 EditLog 丢失,启动失败。
模式提炼
NameNode 的元数据管理体现的设计模式:
1 | |
这个模式不只是 HDFS。数据库的事务日志(WAL)是同一思路——InnoDB 的 redo log、PostgreSQL 的 WAL、SQLite 的 rollback journal 都是 append-only journal + 周期性 checkpoint。Kafka 的 KRaft log 也是 append-only + snapshot。Redis 的 AOF + RDB 是混合模式。
这种模式的核心权衡是"内存访问速度 vs 持久化开销"。把全部状态放内存牺牲了持久化(宕机会丢),但通过 append-only journal 把每次修改持久化到磁盘,再通过 checkpoint 把 journal 收敛回镜像,达到了内存访问速度 + 持久化保证的平衡。
代价是单点写入——append-only journal 必须由一个节点顺序写,否则事务顺序无法保证。这是 HDFS NameNode 单点的根本原因。第五篇会展开 HDFS HA 怎么用 JournalNode 集群替代单点 journal。
工程迁移表
| NameNode 概念 | 数据库 WAL | Kafka KRaft | Redis AOF | etcd raft log |
|---|---|---|---|---|
| 元数据内存镜像 | Buffer Pool | Controller 内存 | dict 字典 | kv 存储 |
| Append-Only Journal | redo log / binlog | metadata log | AOF 文件 | raft log |
| 周期性 Checkpoint | sharp checkpoint | snapshot | RDB | snapshot |
| 启动重放 | redo log replay | log replay | AOF replay | raft log replay |
| Safe Mode | crash recovery | unset controller | loading | leader election |
| 单点写入限制 | master | controller leader | master | raft leader |
注意 etcd 这一列的差异。etcd 的 raft log 也是 append-only + 周期性 snapshot,但 raft log 是多节点复制的(每个 raft 节点都有一份完整 log)。这让 etcd 没有单点问题——任何节点宕机其他节点有完整 log 可以恢复。代价是每次 append 要走 raft 共识(多数派确认),写入延迟高于单点 journal。
HDFS HA(第五篇)借鉴了同样的思路——Active NameNode 把 EditLog 写到 3 个 JournalNode 组成的多数派集群,相当于在 HDFS 这层引入了类似 raft 的复制机制。
常见误解
误解一:“FSImage 是 NameNode 实时写的”。FSImage 是 Checkpoint 时一次性导出的快照,不是实时写的。实时写的是 EditLog。NameNode 处理元数据修改时不写 FSImage,只追加 EditLog。
误解二:“Secondary NameNode 是 NameNode 的备用节点”。Secondary NameNode(SNN)不是热备,它是一个独立进程专门做后台 Checkpoint。Active NameNode 宕机后 SNN 不能接管服务,需要人工把 SNN 上的最新 FSImage 拷贝到新 NameNode 节点再启动。SNN 这个名字误导了很多人。Hadoop 2.x 引入 HA(Standby NameNode)之后,HA 部署不再需要 SNN——Standby NameNode 同时承担热备和 Checkpoint。
误解三:“Safe Mode 期间集群完全不可用”。Safe Mode 期间只是不接受写请求(create、delete、append 等 RPC 被拒绝),读请求(open、read、list)正常工作。集群仍然可以读,只是不能写。这个特性让 Safe Mode 期间集群可以做读请求限流,但写入必须等待。
误解四:“NameNode 重启时 EditLog 重放很快”。重放速度取决于 EditLog 大小。如果 Checkpoint 没有及时合并,EditLog 累积到几亿条事务,重放时间可以长达几十分钟。生产集群必须保证 Checkpoint 周期合理(默认 1 小时 / 100 万事务),并把 NameNode 内存配足(让 Checkpoint 不至于被元数据规模拖慢)。
误解五:“NameNode 启动失败等于数据丢失”。NameNode 启动失败通常是 FSImage 损坏或 EditLog 不完整。这种情况下 DataNode 上的 block 数据仍然完整(数据在 DataNode 磁盘上),只是元数据无法恢复。运维层面可以从 Secondary NameNode 或 Standby NameNode 上的备份 FSImage 恢复,可能丢失最近几个小时的元数据变更。这就是为什么生产集群必须有 SNN 或 HA 部署——单 NameNode 没有 FSImage 备份,启动失败就是数据丢失事件。
练习
-
在 NameNode 节点上找到
dfs.namenode.name.dir目录,列出里面的 FSImage 和 EditLog 文件。用hdfs oiv -p Delimited解析最近的 FSImage,统计集群里的文件数和目录数。 -
在
apache/hadoop源码里找到FSDirectory.java、FSEditLog.java、FSImage.java(hadoop-hdfs-server 模块),观察元数据写入时 EditLog 与内存更新的顺序。 -
故意在测试集群上
hdfs dfsadmin -safemode enter进入 Safe Mode,尝试hdfs dfs -put上传文件,观察被拒绝的报错。然后hdfs dfsadmin -safemode leave离开,再次上传成功。 -
思考题:如果让 NameNode 元数据也分布式存储(每个 NameNode 节点只存一部分元数据),FSImage + EditLog 这套机制需要怎么改造?引入什么新的复杂性?
系列导航
| 序号 | 主题 | 状态 |
|---|---|---|
| 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 |
参考资料
- Konstantin V. Shvachko. The Hadoop Distributed File System. MSST 2010.(描述了 FSImage + EditLog 的早期设计)
- Apache Hadoop 官方文档:HDFS Architecture. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html
- Apache Hadoop 官方文档:NameNode Startup / Checkpoint. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsUserGuide.html
- Apache Hadoop 源码:
FSDirectory.java、FSImage.java、FSEditLogLoader.java. https://github.com/apache/hadoop - Konstantin Shvachko, Hairong Kuang. A Snapshot of the Accumulo NameNode.(Facebook 关于十亿文件 HDFS 集群 NameNode 内存占用的实战报告)
- Tom White. Hadoop: The Definitive Guide. O’Reilly, 4th Edition 2015. Chapter 3 详述了 NameNode 启动恢复和 Checkpoint 流程。
