上一篇讲了读写路径。本篇展开 NameNode 内存里的元数据结构,以及 NameNode 启动时怎么把磁盘上的元数据恢复成内存镜像。

NameNode 常被描述成"HDFS 的元数据节点"。这个描述对应了职责但没解释机制。准确的说法是:NameNode 是一个把全部元数据驻留内存的进程,磁盘上同时维护两份互补的持久化文件——FSImage 是元数据镜像快照,EditLog 是镜像之后的增量操作日志,启动时把 FSImage 加载进内存、重放 EditLog,恢复到宕机前的最终状态。

本篇只抓一个问题:NameNode 内存里到底有哪些数据结构、FSImage 和 EditLog 是怎么配合的、为什么 NameNode 启动要进入 Safe Mode 等待块报告。

NameNode 内存里的六大数据结构

打开 NameNode 进程的内存,主要元数据可以分成六块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌──────────────────────────────────────────────────────────┐
│ NameNode JVM Heap │
├──────────────────────────────────────────────────────────┤
│ 1. FSDirectory(命名空间树) │
│ - INode 树(INodeDirectory + INodeFile) │
│ - 路径 → INode 的快速查找 │
│ │
│ 2. BlocksMap(block → DataNode 映射) │
│ - BlockInfo 对象,每个 block 一条 │
│ - 三副本对应三个 DatanodeStorageInfo 引用 │
│ │
│ 3. DatanodeManager(DataNode 注册表) │
│ - datanodeId → DatanodeDescriptor │
│ - 每个 DataNode 的容量、最近心跳、块列表 │
│ │
│ 4. LeaseManager(写入租约) │
│ - clientName → 持有写租约的文件列表 │
│ - 防止两个客户端同时写同一个文件 │
│ │
│ 5. PendingReplicationBlocks(待复制 block 队列) │
│ - 副本数不足的 block 等待补齐 │
│ │
│ 6. InvalidateBlocks(待删除 block 队列) │
│ - 副本数过多的 block 等待删除多余副本 │
└──────────────────────────────────────────────────────────┘

这六块里前三块是核心元数据,决定 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
2
3
4
5
6
7
8
9
10
11
12
INode(每个文件或目录)           ≈ 150 字节
BlockInfo(每个 block) ≈ 150 字节(含三个 DatanodeStorageInfo 引用)
DatanodeDescriptor(每个 DataNode)≈ 几 KB(与该节点上 block 数有关)

一个 1GB 文件(切成 8 个 128MB block):
1 × 150 + 8 × 150 = 1.35 KB

一亿个 1GB 文件:
1 亿 × 1.35 KB ≈ 135 GB(NameNode 堆内存)

一亿个 1KB 小文件:
1 亿 × 150(INode)+ 1 亿 × 150(BlockInfo) ≈ 30 GB(但是数据本身只有 100GB)

这两组对比清楚展示了 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
3
4
1. 客户端 RPC 调用 createFile(/path)
2. NameNode 把这条变更写入 EditLog(fsync 到磁盘)
3. NameNode 更新内存里的 FSDirectory 和 BlocksMap
4. NameNode 返回客户端成功

步骤 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
2
3
4
1. 距离上次 Checkpoint 超过 1 小时(dfs.namenode.checkpoint.period)
2. 自上次 Checkpoint 后 EditLog 累积超过 100 万条事务
(dfs.namenode.checkpoint.txns)
两者满足任一即触发。

执行 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
2
3
4
5
6
7
8
9
10
1. 触发条件满足
2. Active NameNode 滚动 EditLog:当前 in-progress 文件关闭,
新建一个 in-progress 文件继续写
3. Checkpoint 节点(SNN 或 Standby)从 Active 拉取:
- 当前 FSImage
- 已关闭的 EditLog 段(若干个文件)
4. Checkpoint 节点在内存里加载 FSImage + 顺序重放 EditLog
5. 重放完成后导出新 FSImage 文件
6. Checkpoint 节点把新 FSImage 上传给 Active NameNode
7. Active NameNode 替换旧 FSImage,清理已合并的 EditLog

这个流程的关键是 Active NameNode 不参与合并计算(合并要重放 EditLog 是 CPU 密集型操作),由独立的 Checkpoint 节点承担。这是为什么 Secondary NameNode 在非 HA 部署里是标配——即使 NameNode 内存够大,没有 Secondary NameNode 做后台 Checkpoint,NameNode 重启时重放 EditLog 会非常慢。

Safe Mode:NameNode 启动的特殊状态

NameNode 启动时不能立刻接受写请求。它需要经过一个 Safe Mode 阶段,等集群块状态稳定后才能正常服务。

启动流程:

1
2
3
4
5
6
7
8
9
10
1. NameNode 进程启动
2. 从磁盘加载最新 FSImage 到内存(几分钟到几十分钟,取决于元数据规模)
3. 重放 FSImage 之后的所有 EditLog 段(增量恢复)
4. 此时内存里的元数据恢复到宕机前状态
5. 进入 Safe Mode
- 不接受写请求(创建文件、删除文件、修改文件等 RPC 都被拒绝)
- 接受读请求
- 等待 DataNode 上报块报告
6. 当上报的 block 副本数达到阈值(默认 99.9% block 已上报)
7. 自动离开 Safe Mode,正常服务

为什么需要 Safe Mode?因为 NameNode 启动时内存里的 block → DataNode 映射表是 FSImage + EditLog 重放后的快照,但这个快照里记录的 DataNode 状态可能已经过时——某些 DataNode 在 NameNode 宕机期间自己也宕机了,某些 block 副本丢失了。Safe Mode 等待所有 DataNode 上报最新块报告,让 NameNode 重新计算每个 block 的实际副本数。

Safe Mode 离开条件(Hadoop 默认值):

1
2
3
dfs.safemode.threshold.pct = 0.999   (99.9% block 已达最小副本数)
dfs.safemode.min.datanodes = 0 (最少等待 DataNode 数,0 表示不强制)
dfs.safemode.extension = 30000 (满足条件后再等 30 秒)

这三个参数控制 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
ls -lh /var/lib/hadoop-hdfs/name/current/

典型输出:

1
2
3
4
5
6
7
fsimage_0000000123       500M    (最近一次 Checkpoint 的镜像)
fsimage_0000000123.md5 32B (镜像的 MD5,用于校验)
edits_0000000000000124-0000000000000150 20M (一段已关闭的 edit log)
edits_0000000000000151-0000000000000200 30M
edits_inprogress_0000000000000201 5M (当前正在写的 edit log)
seen_txid 8B (NameNode 已知的最大 txid)
VERSION 100B (集群 ID、storage ID 等元信息)

文件名里的数字是 txid。fsimage_0000000123 表示截至 txid 123 的镜像,之后的元数据变更全部记录在 edits_124-150edits_151-200edits_inprogress_201 这些 EditLog 段里。

hdfs oiv(offline image viewer)可以解析 FSImage 内容:

1
2
hdfs oiv -i fsimage_0000000123 -o fsimage.txt -p Delimited
head fsimage.txt

输出按行展示 FSImage 里的每个 INode(路径、所有者、副本数、修改时间等)。可以用 wc -l 粗略估算集群文件数。

hdfs oev(offline edits viewer)可以解析 EditLog 内容:

1
2
hdfs oev -i edits_inprogress_0000000000000201 -o edits.xml
head edits.xml

输出 XML 格式的操作记录,每条记录是一个事务(OP_ADD、OP_RENAME、OP_CLOSE 等)。

seen_txid 文件记录 NameNode 已知的最大事务 ID。NameNode 启动时用这个数字判断 EditLog 是否完整——如果 fsimage_0000000123 之后没有 edits_124 开始的文件,说明 EditLog 丢失,启动失败。

模式提炼

NameNode 的元数据管理体现的设计模式:

1
2
3
4
5
6
7
8
模式:内存镜像 + Append-Only Journal + 周期性 Checkpoint

- 全部元数据驻留内存以追求访问性能
- 元数据修改时先 append 到顺序写的 journal(保证持久化)
- journal 写完后才更新内存(保证一致性)
- 周期性把内存镜像导出为 snapshot
- snapshot 之前的 journal 可以丢弃
- 启动时加载 snapshot + 重放 journal,恢复到最终状态

这个模式不只是 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 备份,启动失败就是数据丢失事件。

练习

  1. 在 NameNode 节点上找到 dfs.namenode.name.dir 目录,列出里面的 FSImage 和 EditLog 文件。用 hdfs oiv -p Delimited 解析最近的 FSImage,统计集群里的文件数和目录数。

  2. apache/hadoop 源码里找到 FSDirectory.javaFSEditLog.javaFSImage.java(hadoop-hdfs-server 模块),观察元数据写入时 EditLog 与内存更新的顺序。

  3. 故意在测试集群上 hdfs dfsadmin -safemode enter 进入 Safe Mode,尝试 hdfs dfs -put 上传文件,观察被拒绝的报错。然后 hdfs dfsadmin -safemode leave 离开,再次上传成功。

  4. 思考题:如果让 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

参考资料