深入 Hadoop 01 - HDFS 架构与三层切分
上一篇把 Hadoop 整体压成一句话:以"节点总会失败"为前提,HDFS 切大文件、YARN 切资源、MapReduce 切计算。本篇进入 HDFS。
HDFS 常被介绍为"分布式文件系统"。这个说法没错但不够准确。准确的说法是:HDFS 是一个把元数据、数据块、节点监控切成三层独立职责的分布式文件系统。三层职责分离是 HDFS 设计的核心抽象,决定了 HDFS 的所有后续行为——为什么元数据全内存、为什么写文件要 Pipeline、为什么读文件可以就近选副本、为什么 NameNode 是单点。
本篇只抓一个问题:NameNode、DataNode、客户端这三层职责是怎么切开的,以及这种切法为什么会让 HDFS 长成现在的样子。
三层职责切分
HDFS 的架构可以用一张三层图概括:
1 | |
三层职责分别如下:
NameNode 只管元数据。它知道每个文件被切成哪些块、每个块存放在哪些 DataNode 上、每个文件的目录树结构。NameNode 不参与数据传输,不存储任何用户数据的字节。所有元数据驻留在 NameNode 内存里(具体内存模型第四篇展开),写操作同时追加到磁盘上的 EditLog,定期合并到 FSImage 落盘。
DataNode 只管数据块。它知道自己本地磁盘上存了哪些 block,每个 block 多大,校验和是否正确。DataNode 不知道任何文件级别的概念——它手里的 block 没有文件名,只有一个 64 位的 block ID。DataNode 周期性向 NameNode 发心跳(默认 3 秒)和块报告(默认 6 小时),并执行 NameNode 下发的块操作指令(创建、删除、复制)。
Client 是 HDFS 的应用接口。它要从 HDFS 读文件时,先问 NameNode 拿这个文件对应的 block 列表和每个 block 所在的 DataNode,然后直连 DataNode 传输数据。它要写文件时,先问 NameNode 申请一批 block ID 和目标 DataNode 列表,然后直连 DataNode 写数据。NameNode 不参与数据传输。
这种切分有一个关键推论:HDFS 的元数据路径和数据路径完全分离。元数据路径经过 NameNode,是慢路径但只在打开/关闭文件时走一次;数据路径绕过 NameNode 直接连 DataNode,是快路径,承担所有数据字节传输。
NameNode 为什么要全内存
NameNode 把所有元数据驻留内存这件事是 HDFS 设计里最反直觉的决定。一个 1000 节点集群、10 亿文件的 HDFS 集群,NameNode 内存动辄上百 GB。为什么要把元数据全放进内存?
答案是 HDFS 把元数据访问路径设计成单点同步——任何元数据操作(创建文件、删除文件、查询 block 位置)都必须经过 NameNode。如果元数据放在磁盘上,每次操作都要做磁盘 I/O,在 1000 个 DataNode 同时心跳和成千上万个客户端并发访问的场景下,NameNode 会成为严重瓶颈。把元数据放内存让元数据操作延迟降到微秒级,足够支撑每秒几万次操作。
HDFS 元数据的内存占用大致可以用一组经验值估算(Hadoop 3.3.x):
1 | |
这就是 HDFS 著名的"小文件问题"。如果同样的 100GB 数据被切成 10 亿个 100 字节的小文件,NameNode 内存占用是 10 亿 × 150 = 150 GB,但实际数据量只有 100GB——元数据比数据本身还大。把小文件合并成大文件(HAR、Sequence File、Hadoop 3.x 的 EC)是 HDFS 运维的核心主题之一。
NameNode 的内存设计也带来一个副作用:元数据修改必须先写 EditLog 才能返回客户端成功,否则 NameNode 宕机会丢元数据。EditLog 是顺序追加写,性能很好,但 NameNode 必须在写完 EditLog 之后才能更新内存表。这个顺序决定了 HDFS 写操作的延迟下限。第四篇会展开 EditLog 与 FSImage 的细节。
DataNode 的两种心跳
DataNode 与 NameNode 之间有两条独立的"心跳"通道,分别承担不同职责:
第一条是 DatanodeProtocol.sendHeartbeat。这条 RPC 默认每 3 秒发一次,主要携带三类信息:DataNode 当前存储容量、当前正在传输的块操作数、DataNode 状态。NameNode 收到心跳后回执里携带一批指令,例如"删除以下 block"、“复制以下 block 到某 DataNode”、“恢复以下损坏 block”。
第二条是 DatanodeProtocol.blockReport。这条 RPC 默认每 6 小时发一次(Hadoop 3.x 默认值,早期版本是 1 小时),把 DataNode 上所有 block 的完整清单一次性发给 NameNode。NameNode 用这份清单校验自己内存里的 block → DataNode 映射表是否准确。如果不一致(比如某个 block 在 NameNode 看来副本数是 3 但 blockReport 显示只有 2),NameNode 会触发对应补救操作。
为什么要分两条?因为这两类信息有完全不同的特性:
1 | |
把存活探测(高频小请求)和一致性校验(低频大请求)拆成两条 RPC,避免了用 3 秒间隔的请求传输百万级 block 清单带来的网络与 CPU 压力。这是分布式系统常见的"控制平面 vs 数据平面"分离,在 HDFS 这里得到了对应:心跳是控制平面,块报告是元数据一致性平面。
NameNode 通过心跳的"最后联系时间"判断 DataNode 是否死亡。默认配置是 10 分 30 秒(dfs.namenode.heartbeat.recheck-interval 默认 5 分钟 × 2 + 心跳间隔 3 秒 × 10 ≈ 630 秒)之后判定为 dead。一旦判定 dead,该 DataNode 上所有 block 被标记为副本数减一,NameNode 调度其他 DataNode 复制补齐到目标副本数。这个延迟是 HDFS 容错机制的固有时间窗口——比 3 秒心跳慢得多,原因是必须避免网络抖动造成的误判。
Client 的元数据获取模式
Client 与 HDFS 交互的标准流程里,元数据路径和数据路径严格分离:
读文件流程:
1 | |
注意步骤 2 里 NameNode 返回的不只是 block 列表,还包括每个 block 的副本所在 DataNode 列表。这个列表会按"距离 Client 最近的副本优先"排序(机架感知,第二篇展开)。Client 拿到列表后所有数据传输都直连 DataNode,不再经过 NameNode。
写文件流程:
1 | |
这种"元数据走 NameNode、数据走 DataNode"的设计,让 HDFS 的吞吐可以线性扩展。1000 节点集群上,1000 个 DataNode 同时承担数据读写,NameNode 只承担元数据 RPC。如果数据也走 NameNode,NameNode 网卡会立刻成为瓶颈。
代价是 NameNode 的元数据 RPC 仍然是单点瓶颈。这是 HDFS 单 NameNode 的固有上限,也是后续 HDFS Federation、Router-based Federation、Observer NameNode 等设计出现的原因(第六篇展开)。
实验:观察 NameNode Web UI
任何一个能访问的 Hadoop 集群,NameNode 默认监听 9870 端口(Hadoop 3.x 起,2.x 是 50070)暴露 Web UI。打开 http://namenode-host:9870 可以看到 NameNode 的运行时状态。
Summary 标签页展示的信息直接对应本篇讨论的几个核心概念:
1 | |
Datanodes 标签页列出每个 DataNode 的详细状态:最后心跳时间、容量、块数。如果某个 DataNode 的 Last Contact 字段大于 30 秒,说明该节点心跳延迟,可能即将被判定为 dead。
观察 NameNode Web UI 时有几个值得注意的细节:
Under Replicated Blocks 长期不为 0 是 HDFS 不健康的标志。常见原因包括 DataNode 容量不均(无法分配新副本)、网络分区(部分 DataNode 心跳超时但实际存活)、副本放置策略触发限制(机架感知要求副本跨机架但某个机架满了)。这个数字应该在几分钟内自动归零,否则需要人工介入。
Missing Blocks 长期不为 0 是数据丢失。HDFS 不会自动放弃丢失的 block,但读取这些 block 的请求会失败。运维层面需要从其他备份恢复数据,或者用 hdfs fsck / -delete 清理(这条命令是破坏性操作,必须谨慎)。
Block Pool ID 是 HDFS Federation 引入的概念。一个 NameNode 管理的 block 集合称为一个 Block Pool。Federation 部署里多个 NameNode 各自管理独立 Block Pool,第六篇会展开。
模式提炼
HDFS 的三层职责切分可以提炼成一个可迁移的设计模式:
1 | |
这个模式不只在 HDFS 出现。GFS 用同样的设计(Master + ChunkServer)。MongoDB 的 Config Server + Shard Server 是同一思路在文档数据库上的具体化。Ceph 的 MON + OSD 做了去中心化改造,但本质上还是元数据/数据分离。Kafka 的 Controller + Broker 是同一思路在流式消息上的具体化(Kafka 的元数据规模比 HDFS 小得多,所以 Controller 不需要全内存)。
反过来,分布式数据库(Spanner、CockroachDB、TiDB)选择把元数据和数据混在一起,让每个节点都承担部分元数据 + 部分数据。这种设计的代价是元数据访问延迟变高(要 Raft 共识),收益是元数据本身也分布式的,没有单点。HDFS 选了另一条路——元数据单点,但追求极致元数据访问性能。
工程迁移表
| HDFS 概念 | GFS | Ceph | MongoDB | Kafka |
|---|---|---|---|---|
| NameNode(元数据单点) | Master | MON 集群 | Config Server | Controller |
| DataNode(数据节点) | ChunkServer | OSD | Shard (mongod) | Broker |
| Block(大对象切块) | Chunk(64MB) | Object(可变) | Chunk(64MB) | Partition Replica |
| 元数据内存镜像 | Master 内存 | MON 内存 | Config Server 内存 | Controller 内存 |
| EditLog(操作日志) | Operation Log | MON Paxos Log | Config Server OpLog | KRaft Log / ZK |
| 块报告 | ChunkServer 报告 | OSD PG 报告 | chunk version | ISR 同步 |
| 心跳存活探测 | Master → CS | MON → OSD | CS → mongos | Controller → Broker |
注意 Kafka 这一列。Kafka 的 Controller 和 HDFS 的 NameNode 在概念上对应,但 Kafka 元数据规模小(只有 topic / partition / ISR),所以 Controller 不需要全内存设计,也不需要 FSImage/EditLog 这种重型持久化机制。这是为什么 Kafka 在 2.8 之后可以直接用 KRaft 替代 ZooKeeper——元数据规模决定了可以采用的算法复杂度。
常见误解
误解一:“NameNode 是 HDFS 的 Master,所以数据也归它管”。NameNode 不存数据字节,只存元数据。任何把数据字节传给 NameNode 的设计都会立刻让 NameNode 网卡成为瓶颈。Client 写数据时 NameNode 只分配 block ID 和 DataNode 列表,然后 Client 直连 DataNode。
误解二:“NameNode 内存越大集群越快”。NameNode 内存决定的是能存多少元数据(即能存多少文件 / block),不是访问速度。如果集群只有 1TB 数据但被切成 10 亿个小文件,NameNode 需要 150GB+ 内存才能装下元数据,但读写速度反而比 10 个大文件慢得多——每个文件打开都要走一次 NameNode RPC。集群快慢的核心是"文件数量 vs 单文件大小"的比例,不是 NameNode 内存本身。
误解三:“HDFS 是强一致文件系统”。HDFS 不是 POSIX 文件系统。一个文件一旦写入并关闭就是 immutable,不允许随机修改(只允许 append)。多个客户端同时写同一个文件在 HDFS 是不允许的(lease 机制会拒绝)。这个限制简化了实现:写文件时不需要锁机制,副本一致性靠 Pipeline 顺序写入保证。
误解四:“DataNode 死了数据就丢了”。默认副本数 3 的 HDFS,单个 DataNode 宕机不会丢数据——只是把对应 block 的副本数从 3 降到 2,NameNode 会自动调度其他 DataNode 补齐到 3。同时两个 DataNode 宕机才可能丢数据(如果它们恰好持有同一个 block 的两个副本)。三个 DataNode 同时宕机的概率是数据丢失事件的真实门槛。生产集群通常把副本数提到 3,配合机架感知策略让副本跨机架放置,进一步降低同时丢失所有副本的概率。
误解五:“DataNode 宕机立刻被发现”。NameNode 判断 DataNode dead 默认要 10 分 30 秒(5 分钟 recheck × 2 + 30 秒心跳间隔)。这个延迟是 HDFS 容错的固有窗口——窗口内 DataNode 上的 block 不会被补救。短延迟靠心跳判断的话网络抖动会误判。
练习
-
在 Hadoop 集群打开 NameNode Web UI(默认
http://namenode:9870),找到 Under Replicated Blocks 和 Missing Blocks 字段。如果集群里没有这两个字段都为 0,思考可能原因。 -
用
hdfs fsck / -files -blocks列出根目录下所有文件的 block 分布。观察一个大文件被切成了多少个 block、每个 block 在哪些 DataNode 上。这条命令是 HDFS 元数据体检的标准工具。 -
在
apache/hadoop源码里找到DatanodeProtocol.java接口定义(hadoop-hdfs-project 下),观察 sendHeartbeat 和 blockReport 两个方法的参数和返回类型。这是 NameNode 与 DataNode 之间 RPC 契约的源头。 -
思考题:如果让 NameNode 元数据也分布式存储(像 Spanner 那样每个节点存一部分),HDFS 的设计可以做哪些简化?反过来,会引入哪些新问题?
系列导航
| 序号 | 主题 | 状态 |
|---|---|---|
| 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 |
参考资料
- Sanjay Ghemawat, Howard Gobioff, Shun-Tak Leung. The Google File System. SOSP 2003.(NameNode + ChunkServer 设计原型)
- Konstantin Shvachko, Hairong Kuang, Sanjay Radia, Robert Chansler. The Hadoop Distributed File System. MSST 2010.(HDFS 1.x 时代官方架构说明,三层职责的最早正式描述)
- Apache Hadoop 官方文档:HDFS Architecture. https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html
- Apache Hadoop 源码:
DatanodeProtocol.java、NameNodeRpcServer.java. https://github.com/apache/hadoop - Tom White. Hadoop: The Definitive Guide. O’Reilly, 4th Edition 2015. Chapter 3 “The Hadoop Distributed File System” 详细描述了 NameNode 内存占用估算公式。

