💝💝💝首先,欢迎各位来到我的博客,很高兴能够在这里和您见面!希望您在这里不仅可以有所收获,同时也能感受到一份轻松欢乐的氛围,祝你生活愉快!
💝💝💝如有需要请大家订阅我的专栏【大数据系列】哟!我会定期更新相关系列的文章
💝💝💝关注!关注!!请关注!!!请大家关注下博主,您的支持是我不断创作的最大动力!!!
文章目录
-
- 引言
- [一、MapReduce 模型回顾](#一、MapReduce 模型回顾)
- [二、Map 阶段:数据切分与局部计算的起点](#二、Map 阶段:数据切分与局部计算的起点)
-
- [2.1 Map 阶段的核心作用](#2.1 Map 阶段的核心作用)
- [2.2 Map 阶段的执行流程(四步曲)](#2.2 Map 阶段的执行流程(四步曲))
-
- 步骤1:输入分片(InputSplit)
- 步骤2:记录读取(RecordReader)
- [步骤3:执行 map() 函数](#步骤3:执行 map() 函数)
- 步骤4:中间结果处理(分区、排序、溢写)
- [2.3 Map 阶段关键参数](#2.3 Map 阶段关键参数)
- [三、Reduce 阶段:全局聚合与结果输出](#三、Reduce 阶段:全局聚合与结果输出)
-
- [3.1 Reduce 阶段的核心作用](#3.1 Reduce 阶段的核心作用)
- [3.2 Reduce 阶段的执行流程(三步曲)](#3.2 Reduce 阶段的执行流程(三步曲))
-
- [步骤1:Shuffle 之数据拷贝(Copy Phase)](#步骤1:Shuffle 之数据拷贝(Copy Phase))
- [步骤2:归并排序(Merge/Sort Phase)](#步骤2:归并排序(Merge/Sort Phase))
- [步骤3:执行 reduce() 函数](#步骤3:执行 reduce() 函数)
- [3.3 Reduce 阶段关键参数](#3.3 Reduce 阶段关键参数)
- 四、完整执行流程图解
- [五、代码案例:WordCount 中的 Map 和 Reduce 详解](#五、代码案例:WordCount 中的 Map 和 Reduce 详解)
-
- [5.1 Mapper 实现(Map 阶段的核心)](#5.1 Mapper 实现(Map 阶段的核心))
- [5.2 Reducer 实现(Reduce 阶段的核心)](#5.2 Reducer 实现(Reduce 阶段的核心))
- [5.3 作业配置中的 Map 和 Reduce 相关设置](#5.3 作业配置中的 Map 和 Reduce 相关设置)
- 六、优化策略与常见陷阱
-
- [6.1 Map 阶段优化](#6.1 Map 阶段优化)
- [6.2 Reduce 阶段优化](#6.2 Reduce 阶段优化)
- [6.3 常见陷阱](#6.3 常见陷阱)
- 七、总结
引言
MapReduce 的名字本身就揭示了它的核心:Map(映射)和Reduce(归约)。这两个阶段并非简单的两步,每个阶段内部都隐藏着精心设计的子流程------从输入分片、记录读取,到分区、排序、合并,再到最终的归约输出。
本文将带你深入 Map 和 Reduce 的内部世界,用源码级的流程图和完整的代码案例,把这两个阶段的作用与执行流程彻底讲透。
一、MapReduce 模型回顾
在深入之前,先明确两个阶段的定位:
| 阶段 | 输入 | 输出 | 核心作用 |
|---|---|---|---|
| Map 阶段 | 原始数据分片(如HDFS Block) | 中间键值对列表 | 数据切分、局部计算、初步过滤/转换 |
| Reduce 阶段 | 中间键值对(按key分组后) | 最终结果键值对 | 全局聚合、汇总、最终计算 |
一句话总结:Map 负责"分而治之"中的"分"和"局部治",Reduce 负责"合"和"全局治"。
Reduce阶段
Map阶段
输入分片
RecordReader
Map函数
环形缓冲区
分区/排序/溢写
Shuffle: 拷贝
归并排序
Reduce函数
输出
二、Map 阶段:数据切分与局部计算的起点
2.1 Map 阶段的核心作用
- 将输入数据切分成逻辑分片(InputSplit),每个分片对应一个 Map Task
- 对每个分片内的数据应用用户定义的
map()函数,进行过滤、转换、提取等操作 - 输出中间键值对(Intermediate Key-Value Pairs),为 Reduce 阶段做准备
Map 阶段输出的中间数据不会立即写入 HDFS,而是先写入本地磁盘(因为中间数据通常不需要持久化,且拷贝到 Reduce 端后即可删除)。
2.2 Map 阶段的执行流程(四步曲)
步骤1:输入分片(InputSplit)
- InputFormat 负责将输入数据切分成若干个 InputSplit
- 每个 Split 包含一个 HDFS Block 的部分或全部数据(默认一个 Split = 一个 Block)
- Split 只是逻辑概念,不包含数据实体,只包含元数据(文件路径、起始偏移量、长度等)
java
// 自定义 InputFormat 示例(关键方法)
@Override
public List<InputSplit> getSplits(JobContext job) throws IOException {
List<InputSplit> splits = new ArrayList<>();
// 获取输入目录下的所有文件
FileStatus[] files = listStatus(job);
for (FileStatus file : files) {
long blockSize = file.getBlockSize();
long length = file.getLen();
long numSplits = (length + blockSize - 1) / blockSize;
for (int i = 0; i < numSplits; i++) {
long start = i * blockSize;
long splitSize = Math.min(blockSize, length - start);
splits.add(new FileSplit(file.getPath(), start, splitSize, null));
}
}
return splits;
}
步骤2:记录读取(RecordReader)
- RecordReader 将 InputSplit 解析成
<key, value>记录 - 默认的 TextInputFormat 中,key 是行偏移量(LongWritable),value 是一行文本(Text)
- RecordReader 会逐条调用
map()函数
java
// TextInputFormat 的 RecordReader 核心逻辑(简化)
public boolean nextKeyValue() {
line = reader.readLine();
if (line == null) return false;
key.set(pos); // 偏移量作为 key
value.set(line); // 行内容作为 value
pos += line.length();
return true;
}
步骤3:执行 map() 函数
- 用户编写的 Mapper 类中的
map()方法被调用 - 每条输入记录调用一次
- 输出通过
Context.write(key, value)写入中间结果
步骤4:中间结果处理(分区、排序、溢写)
这是 Map 阶段最复杂的部分,Hadoop 在内存中做了一系列优化:
- 环形缓冲区:Map 的输出结果先写入内存中的环形缓冲区(默认 100MB)
- 分区(Partition) :每条记录计算
partition = hash(key) % numReduceTasks,决定该键值对由哪个 Reducer 处理 - 排序(Sort) :当缓冲区达到溢写阈值(默认 80%)时,会先对缓冲区内的数据按 分区号 → key 进行排序
- 溢写(Spill):排序后将数据写入本地磁盘的一个溢写文件
- 归并(Merge):多次溢写会产生多个文件,最终会将这些文件归并成一个大的文件(已分区且内部有序)
溢写阈值触发
多次溢写
map输出
环形缓冲区
KV + 元数据
排序
分区内按key排序
溢写到磁盘
归并合并
最终文件
分区有序
2.3 Map 阶段关键参数
| 参数 | 默认值 | 说明 |
|---|---|---|
mapreduce.task.io.sort.mb |
100 | 环形缓冲区大小(MB) |
mapreduce.map.sort.spill.percent |
0.80 | 缓冲区溢写阈值 |
mapreduce.map.output.compress |
false | 是否压缩中间输出 |
mapreduce.job.maps |
自动 | 用户可设 Map 数量(不推荐) |
三、Reduce 阶段:全局聚合与结果输出
3.1 Reduce 阶段的核心作用
- 拉取(Fetch):从所有 Map Task 的输出中,拉取属于自己分区的数据
- 归并排序:将拉取的数据按 key 进行归并排序,使相同 key 的 value 聚集在一起
- 执行 reduce() 函数:对每个分组(key, values[])执行用户定义的聚合逻辑
- 输出最终结果:通常写入 HDFS
3.2 Reduce 阶段的执行流程(三步曲)
步骤1:Shuffle 之数据拷贝(Copy Phase)
- Reduce Task 启动后,会启动多个拷贝线程(默认 5 个)
- 向 JobTracker(或 ResourceManager)询问所有 Map Task 的完成状态
- 从每个 Map Task 所在的节点,通过 HTTP 拉取属于自己分区的数据
- 拉取的数据先放入内存缓冲区(默认
mapreduce.reduce.shuffle.input.buffer.percent比例的内存)
Map Task 2 Map Task 1 Reduce Task Map Task 2 Map Task 1 Reduce Task GET /mapOutput?partition=R 返回数据(分区R) GET /mapOutput?partition=R 返回数据
步骤2:归并排序(Merge/Sort Phase)
- 当内存缓冲区内的数据量达到阈值,会触发溢写(和 Map 端类似)
- 从多个 Map 端拉取的小文件,会在 Reduce 端进行多路归并
- 最终形成一个已按 key 排序的大文件
在这个排序过程中,同一个 key 的所有 value 会被连续放在一起,形成 (key, [v1, v2, ...]) 的迭代器传递给 reduce 函数。
步骤3:执行 reduce() 函数
- 对每个唯一的 key,调用一次
reduce(key, Iterable<value>, context) - 用户编写聚合逻辑(如求和、求平均、连接等)
- 输出通过
context.write(key, value)写入最终结果文件
结果文件默认存储为 part-r-xxxxx(xxxxx 为 Reduce 编号),写入 HDFS。
3.3 Reduce 阶段关键参数
| 参数 | 默认值 | 说明 |
|---|---|---|
mapreduce.reduce.shuffle.parallelcopies |
5 | 并行拷贝线程数 |
mapreduce.reduce.shuffle.input.buffer.percent |
0.70 | 内存缓冲区比例 |
mapreduce.reduce.merge.inmem.threshold |
1000 | 内存中归并的极限次数 |
mapreduce.task.io.sort.factor |
10 | 归并时一次打开的文件数 |
四、完整执行流程图解
下面用一个完整的执行图,将 Map 和 Reduce 的所有子步骤串联起来:
Reduce阶段
Shuffle阶段
Map阶段
溢写
溢写
InputSplit
RecordReader
map函数
环形缓冲区
分区+排序
多次溢写文件
归并生成
最终中间文件
Reduce拷贝线程
拉取分区数据
内存缓冲区
归并排序
reduce函数
OutputFormat
HDFS输出文件
五、代码案例:WordCount 中的 Map 和 Reduce 详解
我们以经典的 WordCount 为例,逐行剖析 map 和 reduce 函数在流程中的角色。
5.1 Mapper 实现(Map 阶段的核心)
java
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// key: 行偏移量 (LongWritable)
// value: 一行文本 (Text)
String line = value.toString();
StringTokenizer tokenizer = new StringTokenizer(line);
while (tokenizer.hasMoreTokens()) {
word.set(tokenizer.nextToken());
// 输出中间结果 (word, 1)
context.write(word, one);
}
}
}
Map 阶段在这个代码中的体现:
- 每条输入记录调用一次
map方法 - 对每行文本进行 tokenization,生成多个
(word, 1)键值对 context.write将中间结果送入环形缓冲区,后续由框架完成分区、排序和溢写
5.2 Reducer 实现(Reduce 阶段的核心)
java
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
Reduce 阶段在这个代码中的体现:
- 框架自动将 Shuffle 后的数据按 key 分组,每传入一个 key 就调用一次
reduce Iterable<IntWritable> values包含该 key 下所有 Map 输出的 value(对于 WordCount 就是多个 1)- 迭代求和后输出
(word, totalCount)
5.3 作业配置中的 Map 和 Reduce 相关设置
java
// 设置 Mapper 和 Reducer 类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 设置 Map 输出类型(与最终输出可能不同)
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 设置 Reduce 任务数量(决定分区数)
job.setNumReduceTasks(3);
// 可选:设置 Combiner(即 Map 端的局部 Reducer)
job.setCombinerClass(WordCountReducer.class);
这里 numReduceTasks 直接影响分区数(默认使用 HashPartitioner)。如果设为 0,则没有 Reduce 阶段,Map 输出直接作为最终结果。
六、优化策略与常见陷阱
6.1 Map 阶段优化
- 使用 Combiner:减少 Shuffle 数据传输量。要求操作满足交换律和结合律(如求和、最大值)。
- 压缩中间输出 :设置
mapreduce.map.output.compress=true,极大减小网络传输。 - 合理调整缓冲区大小 :增大
sort.mb可以减少溢写次数,提高效率。
6.2 Reduce 阶段优化
- 设置合适的 Reduce 数量 :太少导致负载不均,太多产生大量小文件。一般公式:
0.95 * 集群节点数 * 容器数或1.75 * 集群节点数 * 容器数。 - 优化 Shuffle 参数 :增加
shuffle.parallelcopies加快数据拉取速度。 - 慢启动(Slow Start) :调整
mapreduce.job.reduce.slowstart.completedmaps(默认 0.05),即当 5% Map 完成后就开始调度 Reduce,避免 Reduce 长时间空等。
6.3 常见陷阱
| 陷阱 | 现象 | 解决办法 |
|---|---|---|
| 数据倾斜 | 某个 Reduce 任务执行时间远长于其他 | 分析 key 分布,采用自定义分区器或加盐打散 |
| Mapper 过多 | 大量小文件产生数千个 Map,任务调度过重 | 使用 CombineFileInputFormat 合并小文件 |
| Reducer 过多 | 输出大量小文件,NameNode 压力大 | 控制输出文件数量,或后续合并 |
| OOM in Map/Reduce | 缓冲区溢出或 object 积累 | 调整 sort.mb 或减少 mapreduce.map.java.opts 中的堆内存 |
七、总结
| 阶段 | 核心作用 | 输入 | 输出 | 关键流程 |
|---|---|---|---|---|
| Map | 数据本地化处理,并行生成中间结果 | InputSplit + RecordReader | 分区且有序的中间键值对(本地磁盘) | 分片 → 读记录 → map() → 分区排序溢写 → 归并 |
| Reduce | 数据全局聚合,输出最终结果 | Map 输出的中间数据(Shuffle) | 最终结果(HDFS) | 拷贝 → 归并排序 → reduce() → 输出 |
理解 Map 和 Reduce 两个阶段的内部运作,是写出高效 MapReduce 作业的基础。掌握了环形缓冲区、分区排序、归并等细节,你不仅能用好 MapReduce,也能更轻松地理解 Spark、Flink 等引擎的类似设计。
你曾经在处理数据时遇到过度倾斜导致 Reduce 任务卡住的情况吗?如何定位和解决的?欢迎分享你的实战经验~
❤️❤️❤️觉得有用的话点个赞 👍🏻 呗。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄
💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍
🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙