10.5 分布式文件系统 (GFS / HDFS 架构)
一句话
GFS / HDFS 只为一个场景优化:大文件、顺序读、追加写;为此它把 “元数据” 与 “数据块” 彻底分开(一个中心节点管目录,多个节点存块),并主动放弃随机写、小文件与低延迟。
架构:一个大脑,多条腿
| 角色 | GFS | HDFS | 职责 |
|---|---|---|---|
| 元数据节点 | Master | NameNode | 命名空间、块映射、租约、副本位置 |
| 数据节点 | ChunkServer | DataNode | 存块、响应读写、参与复制 |
| 客户端 | GFS client | DFSClient | 先问元数据,再直连数据节点 |
| 块大小 | 64MB | 128MB(可配) | 大块减少元数据量 |
| 副本数 | 3 | 3 | 机架感知放置 |
写入流程(追加模型):
- 客户端向 NameNode 要 “第一块写哪几个节点”;
- NameNode 返回一个有序的 DataNode 列表(按机架分散);
- 客户端把数据写成流水线:A → B → C,逐级转发并回 ack;
- 写满一块才申请下一块;文件一旦关闭,通常不再改。
🔑 关键取舍:追加 + 大块 + 顺序读让 “单 Master 管元数据” 成为可行设计(元数据量小到内存能装下)。
高可用:单点是怎么被治住的
| 问题 | 方案 | 代价 |
|---|---|---|
| NameNode 单点 | HA:Active/Standby + JournalNode 共享编辑日志 + ZKFC 自动切换 | 复杂度上升,脑裂要防 |
| 元数据过大 | 联邦(Federation):多个 NameNode 各管一部分命名空间 | 跨命名空间操作困难 |
| 数据损坏 | 校验和 + 后台扫描 + 副本重建 | 修复占用带宽 |
小文件:HDFS 的 “死穴”
一个文件、一个目录、一个块,都在 NameNode 内存里占一条记录(约 150 字节量级)。1 亿个小文件 ≈ 15GB 元数据,NameNode 直接被打爆。
| 症状 | 原因 | 对策 |
|---|---|---|
ls / count 极慢 | 元数据遍历 | 合并小文件(打包、SequenceFile、Parquet) |
| NameNode 堆内存告警 | 对象数太多 | 提高块大小、压缩小文件、联邦 |
| 写入吞吐上不去 | 每个文件一次 RPC | 批量写 + 追加日志模式 |
与对象存储的边界
| 维度 | HDFS | 对象存储(S3) |
|---|---|---|
| 访问方式 | 挂载/客户端库,POSIX 近似 | HTTP API |
| 随机写 | 不支持(追加) | 不支持(整对象替换) |
| 元数据 | 中心节点(内存受限) | 分布式 KV(可扩展) |
| 计算亲和 | 可数据本地化 | 计算存储分离 |
| 适合 | 批处理、数据湖(Hadoop 生态) | 海量对象、静态资源、备份 |
趋势是 “计算存储分离”:数据放对象存储,计算层(Spark/Presto)直接读,HDFS 退居 “需要数据本地化的场景”。
对象存储与 HDFS 的分工见 10.1 块存储 / 文件存储 / 对象存储、10.8 对象存储系统设计;选型方法见 10.9 存储选型。
落到数字:HDFS 的容量账
| 项 | 量级 | 影响 |
|---|---|---|
| 单文件/目录/块在 NN 内存 | 约 150 字节 | 1 亿文件 ≈ 15GB 堆 |
| 块大小 | 128MB(默认) | 块越大,NN 元数据越少,但碎片越多 |
| 副本写入 | 3 份(跨机架) | 写放大 3 倍,读可本地化 |
| 单集群文件数 | 数千万–数亿 | 超过即需要联邦或对象存储 |
hdfs dfs -count /data # 目录/文件/总字节数:先看小文件比例
hdfs fsck /data -files -blocks | tail -3 # 缺块与副本不足
反模式
| 反模式 | 后果 |
|---|---|
| 用 HDFS 存海量小文件 | NameNode 内存爆,集群不可用 |
| 把 HDFS 当 “网络盘” 做随机改 | 每次改动重写整块,性能极差 |
| 单 NameNode 不做 HA | 一次故障停服数小时(元数据恢复慢) |
| 副本数设 1 求省钱 | 任何一块坏 = 数据永久丢失 |
| 忽略机架感知 | 副本全在同一机架,机架断电全灭 |
| 用 HDFS 存“需要频繁 rename 的文件” | 元数据操作放大,NN 压力陡增 |
三条铁律
🔑 大文件 + 追加 + 顺序读:这是 GFS/HDFS 的舒适区,反向使用一定痛苦。
🔑 元数据放内存是设计的核心假设;对象数一多,架构就崩。
🔑 副本要跨机架;不然副本只是 “同一故障域里的三份拷贝”。
自检
Q1 为什么 GFS 把块设成 64MB(HDFS 128MB),而不是像本地文件系统那样 4KB?
▸ 答案:因为块大 → 块数少 → 元数据少(NameNode 内存能装下),且顺序读时寻址次数少、吞吐高。代价是内部碎片(小文件浪费块)与随机读放大。这正是 “为大文件优化” 的直接体现。
Q2 HDFS 为什么不能像本地文件系统那样随机写?
▸ 答案:因为它采用 “一次写、多次读” 的追加模型:块一旦写满并关闭就不可变,改数据只能重写新块并切换引用。这样省掉了分布式锁、并发写一致性与复杂的块修改逻辑,换来简单与高吞吐。需要随机写的场景应选数据库或块存储。
Q3 一个集群有 1 亿个小文件,最先撑不住的是什么?
▸ 答案:NameNode 内存。每个文件/目录/块在内存里都有对象开销(约 150 字节量级),1 亿文件轻松吃掉十几 GB 堆,触发 Full GC 甚至 OOM,表现为 “整个集群卡住”。修法是合并小文件或上联邦/对象存储,而不是加 DataNode。
Q4 NameNode HA 里的 JournalNode 起什么作用?
▸ 答案:它保存编辑日志(editlog)的共享副本:Active 把每次元数据变更写到多数派 JournalNode,Standby 持续回放,从而保持内存元数据接近同步。切换时 Standby 只需回放剩余日志即可接管,避免 “重启后重建元数据” 的数小时停机。
Q5 什么时候该从 HDFS 迁到对象存储?
▸ 答案:当负载是 “计算存储分离 + 大量对象 + 弹性计算” 时(数据湖、日志、备份、静态资源)。反之,如果作业强依赖数据本地化(避免跨网络读)或需要 HDFS 特有的追加语义(如 HBase 的 WAL),迁移收益有限。判断依据是 “网络带宽 vs 本地磁盘” 与 “对象数规模”。
Q6 把块大小从 128MB 调到 256MB,会改善什么、恶化什么?
▸ 答案:改善:同样数据量的块数减半,NameNode 元数据减半,顺序读寻址更少。恶化:小文件更浪费(一个 1MB 文件也占一个块位)、故障恢复粒度更粗(重建一个块要复制 256MB)、尾部延迟上升。
关联
- 前置:10.1 块存储 / 文件存储 / 对象存储
- 并列:10.8 对象存储系统设计
- 下游:10.9 存储选型