10.5 分布式文件系统 (GFS / HDFS 架构)

一句话

GFS / HDFS 只为一个场景优化:大文件、顺序读、追加写;为此它把 “元数据” 与 “数据块” 彻底分开(一个中心节点管目录,多个节点存块),并主动放弃随机写、小文件与低延迟。

架构:一个大脑,多条腿

角色 GFS HDFS 职责
元数据节点 Master NameNode 命名空间、块映射、租约、副本位置
数据节点 ChunkServer DataNode 存块、响应读写、参与复制
客户端 GFS client DFSClient 先问元数据,再直连数据节点
块大小 64MB 128MB(可配) 大块减少元数据量
副本数 3 3 机架感知放置

写入流程(追加模型):

  1. 客户端向 NameNode 要 “第一块写哪几个节点”;
  2. NameNode 返回一个有序的 DataNode 列表(按机架分散);
  3. 客户端把数据写成流水线:A → B → C,逐级转发并回 ack;
  4. 写满一块才申请下一块;文件一旦关闭,通常不再改。

🔑 关键取舍:追加 + 大块 + 顺序读让 “单 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)、尾部延迟上升。

关联