MapReduce 是分而治之的分布式计算模型,分为两大阶段:Map 阶段(映射)和Reduce 阶段(归约),中间由 Shuffle 洗牌过程衔接。
整体流程:输入分片 → Map任务 → Shuffle(分区、排序、合并) → Reduce任务 → 输出结果
1. Input 输入阶段
- 把 HDFS 上的输入文件,按块切分成InputSplit 输入分片。分片是逻辑划分,不等于 HDFS 块。
- 每个分片对应 1 个 Map Task。
- RecordReader 读取分片,解析成
<key,value>键值对,传给 map 函数。
key:通常是偏移量;value:一行原始数据。
2. Map 阶段(并行,map task)
- 用户自定义
map()函数,处理输入的<k1,v1>,输出中间结果<k2,v2>。 - map 输出不会直接写 HDFS,先写入内存环形缓冲区。
- 缓冲区满后溢写到本地磁盘,生成溢写 spill 文件;溢写时执行分区 partition、局部排序 sort。
- 多次溢写会产生多个 spill 文件,map 任务结束时执行merge 合并,生成 map 最终的本地输出文件。
map 输出在本地磁盘,不是 HDFS!
3. Shuffle 洗牌阶段【MR 核心,最容易发生数据倾斜】
作用:把 map 输出数据,按照 key 分发到对应 Reduce。分为Map 端 shuffle和Reduce 端 shuffle。
Map 端
- Partition 分区:根据 key 计算分区号,决定这条数据交给哪个 Reduce。默认 HashPartitioner:
key.hashCode() % reduceNum。 - 分区 + 排序,写出 spill 溢写文件,最后合并成有序分区文件。
Reduce 端
- Fetch 拉取:各个 Reduce Task 通过 HTTP,拉取所有 Map 节点上属于自己分区的数据。
- 拉取的数据一部分放内存,多了溢写到磁盘。
- Merge 归并排序:把来自不同 map 的多个文件,合并成全局有序的数据流。
- 排序完成后,相同 key 的所有 value 会聚合在一起,交给 reduce 函数。
⚠️ 数据倾斜根源:hash 分区时,某一个 key 数量巨大,全部落到同一个 reduce,导致单个 reduce 处理量暴涨。
4. Reduce 阶段
- 用户自定义
reduce(k2, 迭代器<v2>),接收同一个 key 的全部 value 集合。 - 执行业务逻辑处理。
- reduce 输出
<k3,v3>,写入 HDFS 输出目录。
reduce 输出直接写 HDFS,写的是最终结果。
5. 精简
文件切分片,分片启 Map;map 输出写缓冲区,溢写磁盘分区排序;shuffle 阶段 reduce 拉取各 map 的分区数据,归并排序,相同 key 聚合,执行 reduce 函数,结果输出 HDFS。
关键要点
- Map 输出存本地磁盘,Reduce 输出存HDFS。
- Shuffle 包含:分区、排序、溢写、合并、拉取。
- 排序是 MR 固有特性,shuffle 过程一定会排序。
- 分区规则决定数据去到哪个 reduce,分区不合理就会数据倾斜。
- map task 数量由输入分片数决定;reduce task 数量用户指定。
简易流程图文字版
HDFS输入文件 ↓ 切InputSplit MapTask 并行执行 ↓ map()输出 <k2,v2> 环形缓冲区 → spill溢写(分区+排序) → merge合并本地文件 ↓ Shuffle ReduceTask 远程拉取对应分区数据 → merge归并排序 ↓ reduce(k,values) HDFS输出目录常见问题
- MapTask 数量由什么决定?由输入分片 InputSplit 数量,以及文件大小、块大小共同决定。
- ReduceTask 数量设置有什么影响?
- 0:没有 reduce 阶段,只跑 map。
- 太少:少数 reduce 压力大,容易倾斜。
- 太多:产生大量小输出文件,namenode 压力大。
- Shuffle 一定会排序吗?MR 默认 shuffle 一定会对 key 排序;如果不需要排序,用 Spark 可以避免。
- 数据倾斜发生在哪个阶段?Shuffle 阶段,分区后大量相同 key 落到同一个 reduce。