MapReduce 核心阶段深度解析:Map 阶段与 Reduce 阶段的作用及执行流程

💝💝💝首先,欢迎各位来到我的博客,很高兴能够在这里和您见面!希望您在这里不仅可以有所收获,同时也能感受到一份轻松欢乐的氛围,祝你生活愉快!

💝💝💝如有需要请大家订阅我的专栏【大数据系列】哟!我会定期更新相关系列的文章
💝💝💝关注!关注!!请关注!!!请大家关注下博主,您的支持是我不断创作的最大动力!!!

文章目录

    • 引言
    • [一、MapReduce 模型回顾](#一、MapReduce 模型回顾)
    • [二、Map 阶段:数据切分与局部计算的起点](#二、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 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

相关推荐
步步为营DotNet2 小时前
深入剖析.NET 11 中 Semantic Kernel 于智能后端集成的创新实践
前端·.net·easyui
@大迁世界2 小时前
33.如何在 React 中使用内联样式(inline styles)?
前端·javascript·react.js·前端框架·ecmascript
CodeSheep2 小时前
DeepSeek的最新招人标准,太讽刺了。
前端·后端·程序员
不法2 小时前
vue 地图路线渲染
前端·vue.js·ubuntu
GISer_Jing2 小时前
从“工具应用”到“系统重构”:AI时代前端研发的范式转移与哲学思辨
前端·人工智能·学习
我家媳妇儿萌哒哒2 小时前
Element ui el-dialog 在一个有滚动条的页面,打开一个弹框,完了再打开一个弹框后,滚动条可以滚动,怎么限制不能滚动。
前端·vue.js·ui
得想办法娶到那个女人2 小时前
Vite + Vue 项目打包为 Electron 桌面应用 完整指南
前端·vue.js·electron
Sailing2 小时前
🚀🚀CLI 为什么在 2025 年突然复兴?看懂 Agent、Skill、MCP、CLI 四层架构
前端·agent·ai编程
ZC跨境爬虫2 小时前
Apple官网复刻第二阶段day_3:(还原苹果官网iPhone顶部标准文案区块,一次编写全局复用)
前端·css·ui·html·iphone