深入 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。NameNode 通过 FSDirectory 的读写锁保护命名空间更新,而不是把整棵目录树简单放进一个 ConcurrentHashMap。
BlocksMap 是 HDFS 元数据的核心索引。给定一个 block ID,能立刻找到它所属的文件,并在运行时查到当前上报该 block 的 DataNode 存储位置。BlockInfo 会记录与存储位置相关的引用,但这些 DataNode 位置不是 FSImage 里的持久化真相,NameNode 重启后要靠 DataNode 的块报告重新建立。
DatanodeManager 是 DataNode 注册表。每个 DataNode 第一次连接 NameNode 时被分配一个 datanodeId,NameNode 在内存里建一个 DatanodeDescriptor 跟踪这个 DataNode 的容量、最近心跳时间、当前持有 block 数等。
LeaseManager 是 HDFS 写并发控制的核心。客户端要写一个文件前必须从 NameNode 拿到这个文件的"软租约"(soft lease,默认 60 秒)。租约保证同一时间只有一个客户端能写这个文件——其他客户端的 create 请求会被拒绝。租约到期后 NameNode 自动释放,允许其他客户端接管。
元数据大小的精确估算
NameNode 内存占用大致可以用一组经验公式估算(Hadoop 3.4.1):
1 | |
这两组对比清楚展示了 HDFS 小文件问题的本质:元数据占用不随数据字节数线性增长,而是由文件数和 block 数决定。按上面的估算,单个 1 KB 文件约占 300 字节元数据,单个 1 GB 文件约占 1.35 KB,后者约为前者的 4.5 倍。
这就是为什么 HDFS 设计假设文件尺寸巨大——大文件让元数据开销分摊到大量数据上,单字节元数据开销可以忽略。在上面的 1 KB 示例中,30 GB 元数据对应 100 GB 数据,元数据虽未超过数据本身,占比却已高达约 30%,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 的命名空间、文件属性和文件到 block 的映射;block 到具体 DataNode 的运行时位置不靠 FSImage 持久化。FSImage 是二进制格式,Hadoop 3.x 默认使用 protobuf image 格式,加载到内存后可以快速重建元数据镜像。
EditLog 是 FSImage 之后的增量操作日志。文件名形如 edits_inprogress_0000000124,记录从 txid 124 开始的所有元数据变更(创建文件、删除文件、添加 block 等)。EditLog 是 append-only 顺序写,性能极高。
NameNode 处理元数据变更的标准流程:
1 | |
步骤 3 是关键——客户端收到成功之前,相关 EditLog 必须已经同步到磁盘。源码里的具体顺序通常是在写锁内更新内存结构并记录 EditLog 条目,释放锁后再 logSync();这既减少锁占用,又保证成功返回的元数据变更可恢复。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 的映射,但还不知道这些 block 当前在哪些 DataNode 上。某些 DataNode 在 NameNode 宕机期间可能也宕机了,某些 block 副本可能丢失了。Safe Mode 等待 DataNode 上报最新块报告,让 NameNode 重新建立 block → DataNode 映射并计算每个 block 的实际副本数。
Safe Mode 离开条件(Hadoop 默认值):
1 | |
这三个参数控制 Safe Mode 的离开门槛。99.9% 看似很严格,但判断依据是最小副本数(Hadoop 3.4.1 默认 dfs.namenode.replication.min=1),不是目标副本数 3;绝大多数 block 在 DataNode 上报后很快满足条件,随后副本不足的 block 再进入重复制队列。
Safe Mode 期间可以手动执行一些操作。hdfs dfsadmin -safemode get 查看当前状态。hdfs dfsadmin -safemode wait 阻塞等待 Safe Mode 离开(脚本常用)。hdfs dfsadmin -safemode enter 强制进入(运维场景,禁止写入)。hdfs dfsadmin -safemode leave 强制离开(慎用——可能让 NameNode 在副本数不足时就接受写入)。
实验:观察 FSImage 与 EditLog
实验状态:UNVERIFIED_RUNTIME。下面是验证步骤,本轮没有连接真实 Hadoop 集群执行。
在 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 恢复;如果缺失相应 EditLog,最近的元数据变更可能丢失。这就是为什么生产集群必须有 SNN 或 HA 部署——单 NameNode 没有可用的元数据备份,启动失败就会演变成数据不可达甚至数据丢失事件。
练习
-
在 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 这套机制需要怎么改造?引入什么新的复杂性?
系列导航
参考资料
- Konstantin V. Shvachko. The Hadoop Distributed File System. MSST 2010.(描述了 FSImage + EditLog 的早期设计)
- Apache Hadoop 官方文档:HDFS Architecture. https://hadoop.apache.org/docs/r3.4.1/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html
- Apache Hadoop 官方文档:NameNode Startup / Checkpoint. https://hadoop.apache.org/docs/r3.4.1/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 流程。
