61.1 Hadoop 生态
一句话
Hadoop 生态不是 “一个软件”,而是四层分工:HDFS 负责存、YARN 负责调度、MapReduce/Spark 负责算、Hive/HBase 负责 “SQL 与随机读写”;记住分层,比记住组件名有用得多。
四层分工
| 层 | 组件 | 职责 | 现代替代 |
|---|---|---|---|
| 存储 | HDFS | 大文件、多副本、顺序读 | 对象存储(S3/OSS) |
| 调度 | YARN | 集群资源分配、队列 | K8s + 调度器 |
| 计算 | MapReduce / Spark / Flink | 批/流计算 | Spark、Flink |
| 接口 | Hive(SQL)、HBase(KV/随机读写)、Presto/Trino(联邦查询) | 把存储+计算包装成可用接口 | 湖仓一体(Iceberg/Hudi + Trino/Spark) |
一句话关系:Hive 把 SQL 翻译成 MapReduce/Spark 作业,YARN 给它分资源,HDFS 存中间结果。所以 “Hive 慢” 往往不是 Hive 的问题,而是 “翻译出来的作业 shuffle 太多”。
实测:一个 wordcount 的 IO 放大
100MB 输入,按典型 MapReduce 路径估算:
| 阶段 | 字节 |
|---|---|
| 读输入 | 100MB |
| map 输出 | 120MB |
| 溢写(spill) | 120MB |
| 合并(merge)读写 | 180MB |
| shuffle 传输 | 120MB |
| reduce 读入 | 120MB |
| reduce 输出 | 10MB |
| 合计 | ≈770MB(7.7× 输入) |
这解释了 “为什么批处理要尽量减少 shuffle 与落盘”:一个最简单的 wordcount 就能把 IO 放大 7–8 倍,而真实作业往往有多个 stage,放大更严重。
优化方向(按收益排序):
1. 减少 shuffle(map 端预聚合、分区裁剪、广播小表)
2. 减少落盘(Spark 的 pipeline、避免中间结果物化)
3. 提高压缩(shuffle 文件压缩,见 44.4 压缩与编码)
代码:MapReduce 的三段结构
// map:拆词 → (word, 1)
map(k1, v1) → for word in split(v1): emit(word, 1)
// shuffle/sort:按 key 分组(框架完成,代价最大的一步)
// reduce:累加
reduce(k2, [1,1,1...]) → emit(k2, sum)
Spark 里同样的事只需一行:sc.textFile(...).flatMap(_.split(" ")).map((_,1)).reduceByKey(_+_)——但底层的 shuffle 与落盘一点没少,只是写法短了。
反模式
| 反模式 | 后果 |
|---|---|
| 把 Hadoop 当 “一个数据库” 用 | 点查延迟高,随机写基本不可用 |
| 小文件直接进 HDFS | NameNode 内存爆(见 10.5 分布式文件系统) |
| 用 Hive 跑高并发小查询 | 每次启动作业,延迟分钟级 |
| 不做分区/分桶 | 全表扫描,分区裁剪失效 |
| 所有作业共用一个队列 | 大作业堵死小作业 |
| 只扩计算不扩存储带宽 | shuffle 成为瓶颈,加机器无效 |
三条铁律
🔑 HDFS 为大文件顺序读设计;小文件与随机写要换存储。
🔑 Hive 慢通常是 “翻译出的作业差”,不是 Hive 本身。
🔑 批处理优化的第一优先级是 “减少 shuffle 与落盘”。
自检
Q1 为什么 “Hive 查询慢” 常常要去看 MapReduce/Spark 的作业,而不是调 Hive 参数?
▸ 答案:因为 Hive 只是把 SQL 翻译成作业,真正的时间花在作业的 shuffle 与落盘上。调 Hive 参数(如并行度、内存)只能在既定执行计划上做微调;如果 SQL 写法导致全表扫描或大 shuffle,参数怎么调都救不回来。
Q2 小文件为什么是 HDFS 的 “死穴”?
▸ 答案:因为每个文件/目录/块都在 NameNode 内存里占一条记录(约 150 字节量级):1 亿小文件 ≈ 15GB 堆,NameNode 会被打爆。而且每个小文件都对应一次 map 任务,调度开销远超数据处理本身。
Q3 YARN 与 K8s 在 “大数据” 场景的分工怎么理解?
▸ 答案:两者都是资源调度层,区别在生态:YARN 为 “长时间运行的大数据作业” 设计(队列、预留、数据本地性),K8s 为 “容器化微服务” 设计。趋势是 “Spark on K8s”,用 K8s 的统一调度替代 YARN,代价是要自己解决数据本地性与队列隔离。
Q4 为什么 “数据本地性” 在 Hadoop 里很重要?
▸ 答案:因为搬数据比搬计算贵:把计算调度到数据所在节点,可以避免跨网络读 HDFS 块。Hadoop 会尽量把 map 任务分配到 “数据所在节点/机架”,本地性差时性能可能下降数倍。计算存储分离(对象存储)后,本地性消失,靠 “高带宽 + 缓存” 弥补。
Q5 HBase 与 HDFS 的关系是什么?
▸ 答案:HBase 把数据存在 HDFS 上:它自己提供 “按 rowkey 随机读写 + 列族”,底层用 HDFS 做持久化与副本。所以 HBase 的写要先写 WAL(HDFS 上的日志)再写 MemStore,读要查 BlockCache 与 HFile——延迟远高于 “纯内存 KV”。
Q6 为什么 “计算存储分离” 会成为趋势?
▸ 答案:因为弹性:存储独立扩展(对象存储近乎无限),计算按需启停(按量付费),不用 “存算按同一比例扩容”。代价是失去数据本地性、依赖高带宽网络,且对象存储的 list/rename 语义弱,需要新的表格式(Iceberg/Hudi)来补。
Q7 一个批处理作业从 30 分钟变 3 小时,先查什么?
▸ 答案:先看输入数据量是否变了(分区/文件数暴涨)、再看shuffle 量(是否数据倾斜,见 61.4 数据倾斜与调优)、最后看资源(队列排队、被抢占)。三步能覆盖 80% 的 “突然变慢”:数据、倾斜、资源。