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 观察一个真实集群

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.3.x,涉及 3.4 行为时单独标注。

练习

  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 导读: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
07-12 YARN 资源管理层(架构 / 资源模型 / 调度器 / 生命周期 / HA / Timeline v2) 后续阶段
13-15 MapReduce 计算模型(编程模型 / Shuffle / MRv2 on YARN) 后续阶段
16-19 HA、安全与 Common 基础设施(RPC / 序列化 / 安全 / 监控) 后续阶段
20-22 演进、生态与对比(Hadoop 生态 / 对象存储 + K8s / 设计遗产) 后续阶段

参考资料

  • 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/current/
  • 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)