Hadoop 常被介绍为"HDFS + YARN + MapReduce 三驾马车"。这个分类把重点放错了位置。把存储、调度、计算切成三件独立的事情,读者很难回答下面这个问题:为什么这三个子系统会一起出现在同一个项目里,而不是像 Spark、Flink、Kafka 那样作为独立组件存在?

更准确的说法是:Hadoop 是一组关于"如何构建运行在大量廉价节点上的分布式系统"的前提假设在三个不同层面的具体化。这组前提把整个项目串成一条主线——节点总会失败、数据总是巨大、廉价比专用更重要。HDFS、YARN、MapReduce 各自处理这组前提下的一个子问题:HDFS 把大文件切成块并多副本放置,YARN 把集群资源切成容器并按策略分配,MapReduce 把大计算切成任务并按失败重试执行。

本系列只抓一个问题:Hadoop 这套以"节点总会失败"为前提假设的工程化方案,在 HDFS、YARN、MapReduce 三个层面是如何具体实现的,每个实现里有哪些可以迁移到其他分布式系统的设计模式。

GFS 论文留下的设计遗产

要理解 Hadoop,必须先回到 Google 2003 年发表的 GFS 论文。HDFS 的设计直接复用了 GFS 的前提假设,差别仅在实现细节上。

GFS 论文(Ghemawat, Gobioff, Leung, SOSP 2003)开篇就列出了四条对当时文件系统设计假设的质疑:

第一,组件失效是常态而非异常。GFS 的设计目标集群包含上千个节点,使用廉价 SATA 磁盘和千兆网卡。在 1000 节点规模下,平均每天都会有几个节点宕机、几块磁盘损坏、几根网线拔错。系统必须把"某个节点此刻不可用"作为基本工作条件,而不是异常事件。

第二,文件尺寸巨大且数量有限。GFS 当时的典型工作负载是处理几个 GB 到几十 GB 的网页快照和日志文件,而不是传统文件系统假设的上百万个 KB 级小文件。元数据管理的代价必须分摊到大尺寸文件本身上,而不是按文件数量线性增长。

第三,工作负载以顺序读写和附加为主,几乎不做随机写入。文件一旦写入,通常只读或者追加新内容,很少在中间修改。这让 POSIX 的强一致约束变得不必要。

第四,应用与文件系统协同设计。GFS 提供的 API 放宽了 POSIX 的一致性保证,例如"附加写入"语义允许偶尔出现重复记录,由应用层去重。这种应用与存储协同设计的思路后来被许多分布式系统继承,包括 Kafka 的 append-only log 和 HBase 的 LSM-Tree。

这四条假设不是 GFS 的妥协,而是 GFS 的设计起点。Hadoop 几乎把四条全部翻译成 Java 实现:

1
2
3
4
5
6
GFS 论文假设                    Hadoop 工程实现
───────────────────────────── ────────────────────────────────
组件失效是常态 DataNode 心跳 + 块报告 + 自动重备份
文件尺寸巨大 128MB Block(Hadoop 2.x 后默认值)
顺序读写为主 写一次读多次 + Append-Only 文件
应用与存储协同设计 放宽 POSIX 一致性 + addBlock / visible 标记

注意一点:Hadoop 1.x 时代块大小默认 64MB,从 Hadoop 2.x 起改为 128MB。原因不是磁盘变快了,而是单集群文件数量规模上升后,NameNode 内存压力要求每个块承载更多数据。这是版本敏感行为,下文涉及具体数字时都会标出版本基线。

三层切分:存储、调度、计算

把"节点总会失败 + 数据巨大"这组前提放到三个不同层面,会得到三组不同的工程对策。

存储层(HDFS)面对的问题是:怎么把一个 100GB 的文件放在 1000 个节点上,并保证任意 2 个节点同时宕机数据不丢?工程对策是大对象切块、副本放在一起、心跳监控。文件被切成 128MB 的块(block),每个块默认三副本,跨机架放置,DataNode 每三秒向 NameNode 发一次心跳,超时则该节点上的所有块被标记为丢失,触发 NameNode 调度其他 DataNode 重新补齐副本数。

调度层(YARN)面对的问题是:怎么把一个由 10000 个 Task 组成的作业分配到 1000 个节点上,并保证其中任意节点宕机作业仍然完成?工程对策是资源切块、双层调度、容器重试。集群资源被切成 Container(CPU + Memory 的最小调度单位),ResourceManager 做中央调度、ApplicationMaster 做作业内二次调度,Task 失败时 ApplicationMaster 向 ResourceManager 申请新 Container 重跑。

计算层(MapReduce)面对的问题是:怎么把一个处理 100TB 数据的计算拆成可独立执行的小单元,并保证其中任意单元失败可以重跑而不影响最终结果?工程对策是计算切块、Shuffle 落盘、Speculative Execution。计算被切成 Map Task 和 Reduce Task,每个 Task 处理一个或多个 InputSplit,Task 失败由 ApplicationMaster 重新调度,慢节点上的 Task 由 ApplicationMaster 启动备份任务(Speculative)同时跑,谁先完成用谁的结果。

三个层面的工程对策高度同构:

1
2
3
4
5
6
HDFS              YARN                  MapReduce
───────────── ────────────────── ─────────────────────
切块 Block Container Task
监控 Heartbeat NodeManager HB Task Attempt Progress
失败 Re-replicate Re-schedule Container Re-launch Task
备份 3 副本 AM 重试 N 次 Speculative backup

这种同构不是巧合。三个子系统都在同一组前提假设下工作,所以会得到同一组工程对策。这就是为什么 Hadoop 把这三件事放在同一个项目里而不是拆成三个独立项目——共用 RPC 层、序列化层、安全层、监控层,整套基础设施只写一次。

一条主线压住整个系列

把这个观察压成一句话:

1
2
3
4
5
Hadoop 是一个以"节点总会失败"为前提假设的分布式系统基座:
HDFS 把大文件切成块并多副本放置、
YARN 把集群资源切成容器并按策略分配、
MapReduce 把大计算切成任务并按失败重试执行;
三者共用同一套 RPC、序列化、安全与监控基础设施。

后续每篇文章都在展开这条主线下的一个子问题。HDFS 篇(01-06)展开"切块 + 副本 + 监控"在存储层的具体实现,YARN 篇(07-12)展开"切块 + 调度 + 重试"在资源管理层的具体实现,MapReduce 篇(13-15)展开"切块 + Shuffle + Speculative"在计算层的具体实现,HA 与 Common 篇(16-19)展开三个子系统共用的那套基础设施,演进篇(20-22)讨论这套前提在云原生时代是否还成立。

实验:用 dfsadmin report 观察一个真实集群

实验状态:UNVERIFIED_RUNTIME。下面是验证步骤,本轮没有连接真实 Hadoop 集群执行。

HDFS 的核心抽象在 hdfs dfsadmin -report 命令的输出里几乎全部体现。这条命令零成本、零风险,可以在任何能访问 Hadoop 集群的环境下运行:

1
hdfs dfsadmin -report

一个典型的三节点伪分布式集群输出大致如下(具体数字会随集群规模和数据量变化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Configured Capacity:
600000000000 (600 GB) # 集群总磁盘容量
Present Capacity:
550000000000 (550 GB) # 扣除 reserved 后的可写容量
DFS Remaining:
540000000000 (540 GB) # 剩余可写
DFS Used:
10000000000 (10 GB) # 已写入数据
DFS Used%:
1.83% # 使用率

Under replicated blocks:
0 # 副本数低于目标的块(应为 0 或很小)
Blocks with corrupt replicas:
0 # 副本损坏的块(必须为 0)
Missing blocks:
0 # 所有副本都丢的块(必须为 0)

Live datanodes (3): # 在线节点列表
Name: 10.0.0.1:9866 ...
Last contact: Mon Jul 26 10:00:01 # 最后一次心跳时间
...

Dead datanodes (0): # 心跳超时节点(应为 0)

四个关键字段直接对应 HDFS 的核心机制:

Configured Capacity / DFS Used 是"切块"的代价——每个块独立分配空间,分散到多个 DataNode 上。

Live datanodes / Dead datanodes 是"节点总会失败"前提的可观察证据。任何真实集群在足够长时间运行后都会出现 Dead datanodes 段,NameNode 会自动把这些节点上的块标记为待重备份,调度其他 Live datanode 补齐。

Under replicated blocks 是"副本放在一起"的工程反馈。当某个 DataNode 宕机,它持有的块副本数从 3 降到 2,这个数字会上升。NameNode 在后台扫描时持续把这些块排队等待补齐,数字最终回到 0。

Missing blocks 是真正意义上的数据丢失——三副本全部不可恢复。这个数字必须长期为 0,否则就是数据丢失事件。

注意一个细节:hdfs dfsadmin -report 这条命令本身也体现了 Hadoop 的设计风格。它不查询 ZooKeeper、不查询独立的元数据库,所有信息都来自 NameNode 内存里的元数据镜像。NameNode 是整个 HDFS 的元数据中枢,下一篇文章会展开。

模式提炼

Hadoop 的整套设计可以提炼成一个可迁移到其他分布式系统的设计模式:

1
2
3
4
5
6
7
模式:假设失败为常态 + 切大对象 + 监控心跳 + 自动重做

- 不要试图让节点不失败,把节点失败作为系统的基本工作条件
- 把大对象(文件 / 资源 / 计算)切成可独立处理的小单元
- 让每个单元有多个备份(数据副本 / 任务重试 / 资源冗余)
- 用周期性心跳监控每个单元所在节点的存活
- 失败时由中央协调者自动调度其他节点重做

这个模式不是 Hadoop 独有。Kafka 的分区副本(Partition Replica)+ ISR(In-Sync Replicas)+ Controller 协调,是同一组前提在流式消息系统上的具体化。Cassandra 的 vnode + replication factor + gossip 协议,是同一组前提在去中心化 KV 存储上的具体化。Kubernetes 的 ReplicaSet + Node Controller + pod 重调度,是同一组前提在容器编排系统上的具体化。

理解这个模式比记住 Hadoop 具体配置重要得多。Hadoop 的配置参数会随版本变化(例如 dfs.replication 在不同版本里默认值不同),但这组前提假设和对应的工程对策是 Hadoop 二十年迭代里没有动过的内核。

工程迁移表

Hadoop 概念 GFS(设计原型) Ceph Kubernetes 对象存储(S3 / OSS)
Block(大对象切块) Chunk(64MB) Object(可变大小) Pod(最小调度单元) Object(无固定大小上限)
Replica(多副本放置) 3 副本 pg + 多副本 ReplicaSet 副本数 后端多副本,对用户透明
Heartbeat(节点心跳) Master ↔ ChunkServer OSD ↔ MON Kubelet ↔ API Server 无(对象存储无独立节点概念)
NameNode(元数据中枢) Master MON 集群 API Server + etcd 元数据服务
Re-replicate(自动重备份) Master 重备份 MON 协调 PG 迁移 Controller 重调度 Pod 后端自动
失败假设(节点总会失败) 是(后端实现层)

注意最后一行的"失败假设"。S3、OSS 这类对象存储对应用层暴露的是高可用 API(数据放在对象存储里"几乎不会丢"),但这并不代表底层没有节点失败。底层实现仍然是大量的节点 + 多副本 + 心跳 + 自动重做,只是这些都被对象存储服务隐藏在 API 之后。第二十一篇会展开这个对比。

常见误解

误解一:“Hadoop 已经过时了”。这个说法的依据通常是"HDFS 被 S3 / OSS 取代了,MapReduce 被 Spark / Flink 取代了,YARN 被 Kubernetes 取代了"。这个依据部分成立但结论错误。具体的子系统确实在换代,但 Hadoop 项目沉淀下来的"假设节点失败 + 切大对象 + 副本放在一起"这组前提假设和对应的工程对策,已经成为后续所有大数据、云原生系统的默认起点。说"Hadoop 过时"等于说"牛顿力学过时"——技术上部分正确,但忽略了它的概念已经被所有后续系统继承。

误解二:“Hadoop 是一个 HDFS 文件系统”。HDFS 只是 Hadoop 项目的存储子系统。把 Hadoop 等同于 HDFS,会忽略 YARN 在容器调度抽象上对后来 Kubernetes 调度器的影响,也会忽略 MapReduce 在分而治之编程模型上对 Spark、Flink 的影响。Hadoop 是一组分布式系统工程对策的总称,不是一个文件系统。

误解三:“Hadoop 集群搭建很复杂所以是过时技术”。集群搭建复杂度的真实原因是 Hadoop 假设了底层有大量廉价节点,并暴露了大量配置参数让运维调优。这种"假设运维是工程师"的设计风格在云原生时代被 managed service 取代,但 Hadoop 本身的内部机制(NameNode 元数据管理、YARN 双层调度、MapReduce Shuffle)仍然是分布式系统设计的标准教科书。

误解四:“Hadoop 1.x 的 JobTracker / TaskTracker 还在用”。这是常见错误。自 Hadoop 2.0(2013 年)引入 YARN 起,JobTracker 已经被拆成 ResourceManager 和 ApplicationMaster 两个角色,TaskTracker 已经被 NodeManager 取代。任何关于 Hadoop 现状的文章必须以 2.x 及以后的架构为基线。本系列固定以 Hadoop 3.4.1 为文档与行为基线;更新版本的变化会单独标注,避免 docs/current 随最新发布漂移。

练习

  1. 在能访问的 Hadoop 集群(本地伪分布式或云上托管)运行 hdfs dfsadmin -report,找出 Live datanodes、Dead datanodes、Under replicated blocks、Missing blocks 四个字段。如果是单节点伪分布式,思考为什么 Under replicated blocks 不为 0(提示:副本数 3 但只有一个 DataNode)。

  2. 在 Hadoop 源码仓库 apache/hadoop 检索 DatanodeProtocol,找到 NameNode 与 DataNode 之间的心跳 RPC 定义。观察心跳请求里携带的字段。这个 RPC 是 Hadoop 所有"节点失败"判断的源头。

  3. 思考题:如果让 Hadoop 假设"节点几乎不会失败"(像传统分布式数据库那样),HDFS 的设计可以做哪些简化?反过来,传统分布式数据库在节点规模从几十台扩展到几千台时,会哪些地方必须向 Hadoop 学习?

系列导航

序号 主题 状态
00 导读:节点总会失败 本篇
01 HDFS 架构与三层切分 下一篇
02 文件写入路径与流水线
03 文件读取路径与副本选择
04 NameNode 内存模型与启动恢复
05 HDFS HA 与脑裂防御
06 HDFS 3.x 演进与纠删码
07 YARN 架构与三方契约
08 YARN 资源模型、Container 与 NodeLabel
09 YARN 调度器对比:FIFO、Capacity、Fair
10 YARN 应用程序生命周期
11 YARN HA 与 Federation
12 YARN Timeline Service v2
13 MapReduce 编程模型与分而治之
14 MapReduce Shuffle 全流程
15 MRv2 on YARN ApplicationMaster 与 Task Attempt
16 Hadoop RPC 协议栈
17 序列化与压缩
18 Hadoop 安全 Kerberos Token ProxyUser
19 监控与运维 Metrics JMX 日志聚合
20 Hadoop 生态 Hive HBase Pig
21 Hadoop 与对象存储 Kubernetes 演进对比
22 Hadoop 设计遗产从 GFS MapReduce 到云原生

参考资料

  • Sanjay Ghemawat, Howard Gobioff, Shun-Tak Leung. The Google File System. SOSP 2003.(HDFS 设计原型,四条前提假设的源头)
  • Konstantin Shvachko, Hairong Kuang, Sanjay Radia, Robert Chansler. The Hadoop Distributed File System. MSST 2010.(HDFS 在 Hadoop 1.x 时代的官方架构说明)
  • Apache Hadoop 官方文档:https://hadoop.apache.org/docs/r3.4.1/
  • Apache Hadoop 源码仓库:https://github.com/apache/hadoop
  • Tom White. Hadoop: The Definitive Guide. O’Reilly, 4th Edition 2015.(Hadoop 2.x 时代圣经,存储和 YARN 章节仍可读)
  • Vinod Kumar Vavilapalli et al. Apache Hadoop YARN: Yet Another Resource Negotiator. SOCC 2013.(YARN 引入论文,解释为什么从 JobTracker 拆出 ApplicationMaster)