本章目标:能用自己的话讲清 HDFS 怎么存、YARN 怎么分配资源、MapReduce 怎么算,以及 Hadoop 生态里各组件的位置。 前置知识:会基本电脑操作;SQL 学过一点更好,没有也能读。 预计用时:2 小时。
1. 为什么需要它
数据开发面对的表,动辄几十 GB 到 TB。单机会撞上三堵墙:
- 存储墙:一块硬盘装不下,加硬盘也有上限。
- 计算墙:一个 CPU 核读 1 TB 文件,按 100 MB/s 估算也要接近 3 小时。
- 故障墙:机器越多,坏盘、宕机越常见,必须靠软件自动绕过。
分布式的基本思路是:把数据切成块放到多台机器;把计算拆成任务送到数据附近;机器坏了用副本继续算。它要回答四个问题:数据怎么切、任务怎么分、故障怎么办、机器之间怎么少搬数据。Hadoop 的三个核心组件就是这四个问题的工程答案:
HDFS 存:大文件切块,多副本
YARN 管:CPU/内存切成 Container
MapReduce 算:Map -> Shuffle -> Reduce
三者可以独立使用:Spark 可以跑在 YARN 上,数据仍然放 HDFS。把存储、资源、计算拆开,是 Hadoop 最重要的设计思想。
注意:不是所有数据都要上集群。几百万行、单机放得下时,DuckDB、MySQL 甚至 pandas 更快。Hadoop 解决的是“单机放不下、算不完”的问题。
2. 核心概念
2.1 HDFS:分布式文件系统
HDFS 是主从结构:
客户端 -> NameNode(目录树、文件到块、块到 DataNode 的映射)
| 返回块位置
DataNode1[block1,block2] DataNode2[block1,block3] DataNode3[block2,block3]
NameNode 管元数据,不管文件内容:目录树、文件切成哪些块、每个块在哪几台 DataNode 上。元数据常驻内存,靠 edits log 和 fsimage 持久化。DataNode 存真正的块,定期向 NameNode 发心跳和块报告;节点失联后,NameNode 把其副本标记不可用并安排恢复。SecondaryNameNode 不是备份,它只定期合并 edits log 和 fsimage;真正的 NameNode 高可用要 Active/Standby 两个节点,配合 JournalNode 和 ZooKeeper 切换。
块(Block) 是存储单位,Hadoop 2.x 以后默认 128 MB。500 MB 文件会切成 128 + 128 + 128 + 116 MB 四块。块大是为了减少寻址开销和元数据条数,代价是最后一块可能利用率低。副本默认 3 份,机架感知下通常一份本机架、一份另一机架、第三份在第二份所在机架的另一节点,既防单机故障也防机架断电。
写流程:客户端向 NameNode 请求创建文件,拿到第一个块可写的 DataNode 列表;把数据切成 packet,写到第一台 DataNode,再转发给第二台、第三台形成管道;每台逐级确认,失败节点被剔除并补副本;文件关闭时持久化元数据。
读流程:客户端向 NameNode 请求块位置,拿到按网络距离排序的 DataNode 列表;优先读最近的副本并做校验和,副本损坏或节点超时就换下一个重试。
常用命令:
| 命令 | 作用 |
|---|---|
hdfs dfs -ls /path |
列目录 |
hdfs dfs -mkdir -p /user/student/input |
递归建目录 |
hdfs dfs -put words.txt /user/student/input/ |
上传 |
hdfs dfs -get /user/student/output/part-r-00000 . |
下载 |
hdfs dfs -cat /path/file |
查看文本 |
hdfs dfs -rm -r /path |
递归删除,默认不进回收站 |
hdfs dfsadmin -report |
看集群容量和节点状态 |
HDFS 适合“一次写入、多次读取”的大文件,不适合大量小文件、频繁随机修改和低延迟点查。1 MB 和 1 GB 文件都可能占一个块,但前者的有效吞吐极低。
2.2 YARN:集群资源的管理者
YARN 不负责计算,只负责把 CPU 和内存分给应用。
客户端 -> ResourceManager(全局资源、队列、调度)
| 分配 Container
NodeManager(单机代理,启动和监控 Container)
+-- Container:ApplicationMaster(申请资源、协调任务)
+-- Container:Map/Reduce Task 或 Spark Executor
| 组件 | 职责 |
|---|---|
| ResourceManager | 全局调度器,掌握整个集群资源,按队列分配 |
| NodeManager | 每台机器上的代理,管理本机 Container,上报资源 |
| ApplicationMaster | 每个应用一个,向 RM 申请资源、向 NM 下任务、监控进度 |
| Container | 资源单位,包含内存和虚拟 CPU 核,不是虚拟机 |
一次提交:客户端把程序提交给 RM;RM 在某个 NM 上启动 AM;AM 向 RM 申请 Container;RM 分配后,AM 通知对应 NM 启动任务;任务向 AM 汇报进度,全部完成后 AM 注销、资源回收。
常见调度器:FIFO 先来先服务,简单但大任务会堵后面;Capacity Scheduler 把资源划成队列,每队有保底容量和上限,适合多部门共享,Hadoop 3 默认;Fair Scheduler 按应用动态平分资源,可抢占。
资源参数要对齐:yarn.nodemanager.resource.memory-mb 是单机可供 YARN 的总内存,yarn.scheduler.maximum-allocation-mb 是单 Container 上限。Spark 的 spark.executor.memory 只是堆内存,YARN 实际申请还要加堆外开销。
2.3 MapReduce:把计算拆成两段
用 WordCount 理解最直观。输入三行,执行过程:
输入分片 -> Map -> Shuffle(按 key 分组、排序)-> Reduce -> 输出
hello hadoop (hello,1) hello -> [1,1] hello 2
hello spark (hadoop,1) hadoop -> [1,1] hadoop 2
hadoop spark (spark,1) spark -> [1,1] spark 2
Map 阶段:每个输入分片由一个 Map Task 处理,逐行拆词,输出 (word, 1)。Map 输出先写环形缓冲区,到阈值后溢写磁盘,溢写前按分区号排序。
Shuffle 阶段:Reduce Task 从所有 Map 输出里拉取属于自己分区的数据,归并排序,把相同 key 的 value 放到一起。这里包含大量磁盘读写、网络传输和序列化,是 MapReduce 最贵的阶段。
Reduce 阶段:对每个 key 的 value 列表聚合。WordCount 就是把一串 1 加起来,每个 Reduce 输出一个 part-r-00000 文件。
Combiner 是 Map 端的预聚合,可以先在本地把相同单词加起来,减少传给 Reduce 的数据。它只能用于满足结合律和交换律的操作,比如求和、最大值;直接算平均值会错,因为平均值不能再平均。Partitioner 决定一条 (key, value) 去哪个 Reduce,默认 hash(key) % reduce 数;某个 key 特别多时,数据会压到一个 Reduce,这就是数据倾斜。
MapReduce 慢,主要慢在:
- Map 输出和 Reduce 输入都落盘,Shuffle 反复读写磁盘;迭代算法每轮重来。
- 复杂任务拆成多个作业,中间结果也落 HDFS。
- 每个 Task 启动 JVM,任务多时启动开销明显。
- 只有 Map 和 Reduce 两种固定阶段,没有全局 DAG 优化;数据倾斜时少数 Reduce 拖住全作业。
这些正是 Spark 要解决的问题,见 Spark 基础。
2.4 Hadoop 生态:每个组件只解决一件事
| 组件 | 定位 | 解决什么问题 | 不适合做什么 |
|---|---|---|---|
| Hive | 基于 HDFS 的离线 SQL 引擎 | 用 SQL 查 PB 级数据,元数据放 Metastore | 低延迟点查、频繁 update |
| HBase | 基于 HDFS 的分布式 NoSQL 数据库 | 按 RowKey 随机读写、海量明细查询 | 多表关联分析、全表报表 |
| Sqoop | 关系库与 HDFS 的批量搬运工具 | MySQL 表批量导入 HDFS/Hive | 实时同步、复杂转换 |
| Flume | 日志采集代理 | 把日志文件稳定送到 HDFS/Kafka | 复杂业务数据同步 |
| ZooKeeper | 分布式协调服务 | 选主、配置管理、分布式锁 | 存业务数据、当数据库用 |
容易混淆的点:
- Hive 的表不是数据库表:一张表 = HDFS 上一个目录 + Metastore 里一份元数据。建表只是写元数据,导数往往只是把文件放进目录。分区表是目录套目录,查某天只扫对应目录,这叫分区裁剪。
- HBase 不是 Hive 的替代品:它擅长按 RowKey 查一行或一小段,不擅长
GROUP BY、多表 JOIN 和全表分析。RowKey 设计决定查询性能。 - Sqoop 已进入 Apache Attic(退休),新项目更多用 DataX、Flink CDC 或云厂商的数据集成服务,但老公司仍会问。
- Flume 的核心是 Source、Channel、Sink:Source 收数据,Channel 暂存,Sink 送出。文件 Channel 能在重启后继续传,代价是吞吐下降。
- ZooKeeper 不存大数据,它存少量协调信息;HBase、Kafka、Hadoop HA 都依赖它做选主和状态协调。
3. 动手示例
3.1 不装集群:用 Python 模拟 WordCount
保存为 scripts/mr_wordcount_demo.py 后运行,不需要 Hadoop:
# -*- coding: utf-8 -*-
from collections import defaultdict
lines = ["hello hadoop", "hello spark", "hadoop spark"]
mapped = [(w, 1) for line in lines for w in line.split()] # Map
grouped = defaultdict(list)
for word, one in mapped:
grouped[word].append(one) # Shuffle
result = {w: sum(v) for w, v in grouped.items()} # Reduce
for word in sorted(result):
print(word, result[word])
python scripts/mr_wordcount_demo.py
输出:
hadoop 2
hello 2
spark 2
这段代码是单机的,但和 Hadoop 的差别只在“任务分到哪台机器”。Map 的输出结构、Shuffle 的分组、Reduce 的聚合逻辑完全一样。先把这个流程讲顺,再看真实集群,名词就不会悬空。
3.2 方案一:Docker 单节点(可选)
装了 Docker Desktop 和 WSL2 后,可以从 Docker Hub 找 Hadoop 单节点镜像,按镜像说明启动 NameNode、DataNode、ResourceManager、NodeManager,映射 9870(NameNode UI)和 8088(YARN UI),进容器执行 hdfs dfs -ls / 和 yarn application -list,再跑一次自带 WordCount。
注意:Hadoop 镜像的 JDK、版本和环境变量差异很大,命令以镜像文档为准。Docker 方案的价值是看到 Web UI,不是维护生产集群。Hadoop 2 的 NameNode UI 端口是 50070,别和 3.x 的 9870 混淆。
3.3 方案二:WSL2 伪分布式(可选,更接近真实)
Windows 原生跑 Hadoop 容易卡在权限和 winutils.exe 上,推荐在 WSL2 的 Ubuntu 里做。关键步骤如下,不要求背:
- 安装 WSL2、Ubuntu、Java 11 与 SSH:
sudo apt update && sudo apt install -y openjdk-11-jdk ssh rsync;下载 Hadoop 3.3.6,解压后配置JAVA_HOME、HADOOP_HOME和PATH。Hadoop 3.3 官方支持 Java 8/11,伪分布式就用 Java 11;跑 PySpark 时再切到 Java 17。 - 在
core-site.xml设置fs.defaultFS为hdfs://localhost:9000;在hdfs-site.xml把dfs.replication设为 1;在mapred-site.xml设置mapreduce.framework.name=yarn;在yarn-site.xml开启mapreduce_shuffle;在hadoop-env.sh、yarn-env.sh写明JAVA_HOME。 - 配置本机免密 SSH,执行
ssh localhost确认不用输密码。 - 第一次启动前格式化:
hdfs namenode -format。只执行一次,重复格式化可能清空元数据。 - 启动:
start-dfs.sh、start-yarn.sh。执行jps,应看到 NameNode、DataNode、SecondaryNameNode、ResourceManager、NodeManager。 - 跑 WordCount:
hdfs dfs -mkdir -p /user/student/input
printf 'hello hadoop\nhello spark\nhadoop spark\n' > words.txt
hdfs dfs -put words.txt /user/student/input/
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar \
wordcount /user/student/input /user/student/output
hdfs dfs -cat /user/student/output/part-r-00000
输出应与 Python 模拟一致。Web UI 在 localhost:9870 和 localhost:8088,能看到 DataNode 存活、副本数和任务历史,这一章的流程就算跑通。
3.4 知道即可的边界
必须会:说出 HDFS 的 NameNode、DataNode、块、副本;画出读写流程;说出 YARN 的 RM、NM、AM、Container;口述 Map、Shuffle、Reduce;说出 MapReduce 为什么慢;说出 Hive、HBase、Sqoop、Flume、ZooKeeper 的定位;记住常用 HDFS 命令。
知道即可,不要背配置:NameNode HA 的 JournalNode、ZKFC 细节;Kerberos 与多租户权限;机架拓扑脚本和跨机房容灾;生产 JVM 参数和 NameNode 堆内存计算;不同版本端口差异;自己搭高可用集群。
4. 常见坑
| 坑 | 现象 | 原因与处理 |
|---|---|---|
| 重复格式化 NameNode | DataNode 起不来,clusterID 不一致 | 格式化会生成新 clusterID;删旧数据目录重格式化,或恢复原元数据 |
| 把 SecondaryNameNode 当备份 | 以为 NameNode 挂了能接管 | 它只做 checkpoint;高可用要用 Standby NameNode |
| 大量小文件 | 任务启动慢、NameNode 内存涨 | 先合并小文件,再用 Parquet/ORC 列式存储 |
| 以为 HDFS 能改文件 | 追加或覆盖失败 | HDFS 以一次写入为主,随机修改用 HBase、Iceberg 等 |
| 副本数设成 1 | 节点挂了数据丢失 | 测试环境可设 1,生产至少 3 |
5. 练习
练习 1:算块数和实际占用
一个 500 MB 文件,块大小 128 MB,副本数 3。它被切成几个块?集群里一共有几份块副本?按块大小粗略估算,占多少存储空间?
参考答案
500 / 128 = 3.9,向上取整是 4 个块:128、128、128、116 MB。每个块 3 个副本,共 4 × 3 = 12 份副本。粗略占用 500 MB × 3 = 1.5 GB。最后一块只有 116 MB,严格算略小于 1.5 GB;面试里说“约 3 倍原始大小”即可。块数向上取整,不要漏掉最后不满的块。
练习 2:写出上传、计算、查看、删除的命令
把本地 words.txt 上传到 /user/student/input/,用 Hadoop 自带示例跑 WordCount,输出到 /user/student/output,查看结果,最后删除输出目录。
参考答案
hdfs dfs -mkdir -p /user/student/input
hdfs dfs -put words.txt /user/student/input/
hadoop jar $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar \
wordcount /user/student/input /user/student/output
hdfs dfs -cat /user/student/output/part-r-00000
hdfs dfs -rm -r /user/student/output
注意:输出目录必须不存在,否则 WordCount 会报错。重跑前先删旧目录,或者换一个输出路径。
练习 3:手写 WordCount 的 Map 和 Reduce 输出
输入 a b a、b c、a c。写出 Map 输出、按 key 分组后的 Shuffle 结果,以及最终 Reduce 输出。
参考答案
Map 输出(顺序不重要):(a,1) (b,1) (a,1) (b,1) (c,1) (a,1) (c,1)。
Shuffle 后:a -> [1,1,1],b -> [1,1],c -> [1,1]。
Reduce 输出:a 3,b 2,c 2。Map 负责拆开打标,Shuffle 负责把相同 key 放到一起,Reduce 负责汇总列表。
练习 4:为什么小文件比大文件麻烦
有 1000 个 1 MB 的小文件,和 1 个 1 GB 的大文件,数据量相同。从 NameNode 元数据、块数量、任务数量三个角度解释:为什么小文件更麻烦?
参考答案
元数据与块:HDFS 为每个文件、每个块维护元数据。1000 个小文件至少 1000 个文件对象和 1000 个块;1 GB 大文件按 128 MB 只需 8 个块。
任务:计算框架通常按文件分片启动任务,1000 个小文件可能启动 1000 个任务,每个只读 1 MB,调度和 JVM 启动开销远大于计算本身。处理办法是合并成较大的 Parquet/ORC 文件,让每个文件接近 128–256 MB。
练习 5:DataNode 挂了,读写会怎样
副本数为 3,一个 DataNode 突然宕机。正在写的数据会怎样?已经写好的数据还能读吗?
参考答案
正在写:管道会感知失败,客户端剔除故障节点,用剩余副本继续写,并由 NameNode 安排补齐副本;已确认成功的 packet 不会丢。
已经写好:NameNode 收不到心跳后把该节点副本标记不可用,客户端改读其他副本;只要剩余副本数大于 0 就还能读,副本不足时会在其他节点补到 3 份。HDFS 的容错不是“等机器修好”,而是“用副本继续服务,再自动恢复副本数”。
6. 面试延伸
面试题:NameNode 和 SecondaryNameNode 有什么区别?
是什么:NameNode 管理命名空间和块映射;SecondaryNameNode 定期合并 edits log 和 fsimage,帮助缩短 NameNode 重启恢复时间。
解决什么问题:edits log 不断增长,重启要重放很久;checkpoint 把旧日志合并进新的 fsimage,控制恢复成本。
我的实操边界:我在本地伪分布式环境里看过两者进程和目录,理解 checkpoint;没有做过生产 NameNode HA 切换和元数据恢复。
面试题:HDFS 的写流程是怎样的?
是什么:客户端向 NameNode 申请创建文件,拿到块和 DataNode 列表后,通过 DataNode 管道逐包写入,逐级确认,最后关闭文件并持久化元数据。
解决什么问题:大文件切块、多副本并行写,用管道确认保证“写入成功”不是只写了一份;节点失败时可以剔除并补副本。
我的实操边界:我用伪分布式跑过上传和 WordCount,能画清楚客户端、NameNode、DataNode 三段关系;没有处理过生产环境的管道恢复和跨机架副本放置。
面试题:MapReduce 为什么慢?Spark 改进了什么?
是什么:MapReduce 用固定的 Map、Shuffle、Reduce 阶段,中间结果大量落盘;Spark 用 DAG 组织多个转换,尽量在内存中传递中间结果,并用统一的 DataFrame/SQL API 表达计算。
解决什么问题:MapReduce 在迭代计算、多阶段 ETL 和交互查询上磁盘开销大;Spark 减少不必要的落盘和作业串联,让复杂流水线在一次作业里完成。
我的实操边界:我用 Python 模拟过 WordCount,也了解 MapReduce 的 Shuffle 和 Combiner;Spark 只在本地模式跑过小数据,没有调过生产集群。面试时我会明确说“原理清楚,生产运维没做过”。
7. 本章验收
- 能画出 HDFS 的 NameNode/DataNode 结构,并口述一次写流程和一次读流程
- 能解释块、副本、机架感知分别解决什么问题
- 能写出 6 条以上 HDFS 常用命令,并说明
-put、-get、-cat、-rm -r的区别 - 能说出 YARN 的 RM、NM、AM、Container 的职责
- 能不看资料口述 WordCount 的 Map、Shuffle、Reduce 过程
- 能说出 MapReduce 慢的至少 3 个原因,以及 Spark 的对应改进
- 能说出 Hive、HBase、Sqoop、Flume、ZooKeeper 各自的定位和不适合的场景
- 完成 5 道练习,其中至少 4 道能独立写出答案
下一章
Spark 基础:理解 Driver/Executor、RDD/DataFrame 和执行模型,并跑通第一个 PySpark 程序。