想象一个巨大的文档分类任务,一个人处理要一个月。MapReduce 把这个任务分配给一百个人,每人处理一小部分,最后把结果汇总,半小时就完成了!这就是 MapReduce 的威力!
📑 目录
- [MapReduce 概述](#MapReduce 概述)
- 名词解释(命令与术语)
- [Map 阶段详解](#Map 阶段详解)
- [Shuffle 阶段详解](#Shuffle 阶段详解)
- [Reduce 阶段详解](#Reduce 阶段详解)
- [完整 WordCount 示例](#完整 WordCount 示例)
- 高级特性
- [MapReduce 与 YARN](#MapReduce 与 YARN)
- 监控与调试
- 性能优化
- 总结
- 官方文档与参考
🎯 MapReduce 概述
什么是 MapReduce?
MapReduce 是一种分布式计算框架,用于处理和生成大数据集。它将复杂的并行计算过程抽象为两个阶段:Map(映射)和 Reduce(归约)。
官方定义简述 (来源:Apache Hadoop MapReduce):MapReduce 是建立在 YARN 之上的并行处理大数据集 的编程模型。用户实现 Mapper 和 Reducer,框架负责把输入切分为分片、调度 Task、处理 Shuffle 与容错,适合批处理场景。
为什么需要 MapReduce?
想象一个图书馆要统计所有书中的单词出现次数:
- 单机处理 = 一个图书管理员一本本翻,可能要几个月
- MapReduce = 一百个管理员每人负责几本书,最后汇总结果,几天就完成
📂 大数据集
📤 自动分发
🗺️ Map 并行处理
🔀 Shuffle 数据交换
📊 Reduce 汇总结果
📁 最终输出
MapReduce 的核心思想
| 概念 | 说明 | 生活类比 |
|---|---|---|
| 分而治之 | 大任务拆成小任务 | 公司分成部门处理不同业务 |
| 移动计算而非数据 | 计算靠近数据 | 去仓库处理货物,而非把货物搬到办公室 |
| 容错性 | 任务失败自动重试 | 员工请假有人顶替 |
MapReduce vs 传统计算
MapReduce
📂 读取数据
🗺️ Map 并行
🔀 Shuffle 排序
📊 Reduce 汇总
📁 输出结果
传统计算
📂 读取数据
🖥️ 单机处理
📁 输出结果
| 特性 | 传统计算 | MapReduce |
|---|---|---|
| 数据规模 | GB 级别 | TB/PB 级别 |
| 计算方式 | 单机串行 | 分布式并行 |
| 容错能力 | 无 | 自动重试 |
| 适用场景 | 小数据 | 大数据 |
| 硬件要求 | 高性能服务器 | 普通机器集群 |
Map 与 Reduce 对比(相近概念辨析)
| 维度 | Map | Reduce |
|---|---|---|
| 输入 | 一个 InputSplit,一条条 (key, value) | 同一 key 的所有 value 的迭代器 |
| 输出 | 若干 (key, value),可多可少 | 通常每个 key 输出一条(或几条) |
| 并行度 | 由分片数决定,通常很多 | 由 setNumReduceTasks 决定 |
| 是否必须 | 必须有 | 可以设为 0(仅 Map 作业) |
| 生活类比 | 各部门各自统计原始数据 | 总部把各部门结果按类别汇总成总表 |
📖 名词解释(命令与术语)
以下对文档中出现的命令、概念、配置项做简要解释,并配上生活例子与「为什么」,便于记忆与理解。
常用命令
| 命令/名称 | 含义 | 说明 | 生活例子 | 为什么? |
|---|---|---|---|---|
| hadoop jar | 提交 MapReduce 作业 | hadoop jar <jar> <MainClass> <input> <output>,把打包好的 Job 提交到集群运行 |
像「把任务单和工具箱交给工地负责人」:jar 是工具箱,MainClass 是任务单上的负责人 | 为什么用 jar?因为集群各节点要能加载同一份代码,jar 便于分发和指定入口类 |
| mapred job -list | 列出作业 | 查看当前或历史 MapReduce 作业列表 | 像「看工地任务看板」:哪些任务在跑、哪些已完成 | 为什么需要?方便排查卡住的任务、查作业 ID 看日志 |
| mapred job -status | 查看作业状态 | 根据 job_id 查看该作业的进度、计数器、失败信息 | 像「查某一份任务单的当前进度」 | 为什么重要?失败时要看在 Map 还是 Reduce、哪个 Task 出错 |
| mapred job -kill | 杀死作业 | 终止正在运行的作业 | 像「叫停一个任务」:发现跑错或不需要时及时停 | 为什么有时要 kill?错误参数、重复提交、或为腾出资源给更重要的任务 |
| hdfs dfs -put | 上传文件到 HDFS | 将本地文件或目录放入 HDFS 路径 | 像「把材料搬进仓库」:MapReduce 读的是 HDFS 上的数据 | 为什么数据要在 HDFS?计算靠近数据、多副本可靠、与 MapReduce 同生态 |
| hdfs dfs -cat | 查看 HDFS 文件内容 | 输出 HDFS 上文件内容到标准输出 | 像「打开仓库里某箱货看一眼」 | 为什么用 cat?快速看输出目录里的 part-r-00000 等结果文件 |
| FileInputFormat.addInputPath | 设置作业输入路径 | 指定 Job 从哪些 HDFS 路径读入数据,可多次调用或传目录 | 像「指定从哪几个仓库取料」 | 为什么可以传目录?框架会递归列出目录下文件并切成 InputSplit,便于多文件一批处理 |
核心概念
| 名词 | 含义 | 生活例子 | 为什么? |
|---|---|---|---|
| InputSplit | 输入分片,逻辑上的一段输入,对应一个 Map Task | 像「把一本大书拆成若干章,每人负责一章」 | 为什么按块(如 128MB)切?和 HDFS 块对齐便于数据本地化,且单 Task 不宜过大否则失败成本高 |
| RecordReader | 记录读取器,把 InputSplit 读成一条条 (key, value) | 像「章节负责人按行/按条读出内容」 | 为什么需要?Map 只认识键值对,不同文件格式(文本、SequenceFile)用不同 RecordReader 解析 |
| Partitioner | 分区器,决定每条 (k,v) 去哪个 Reduce | 像「按姓氏首字母分到不同收银台结账」 | 为什么默认用 hash?保证同一 key 一定进同一 Reduce,且尽量均衡;自定义可解决数据倾斜 |
| Spill(溢写) | 内存缓冲区满时把数据排序后写到本地磁盘 | 像「桌上摆满了就先整理成几摞放进抽屉,桌上继续接新单」 | 为什么不全放内存?数据可能远超内存,溢写避免 OOM,且排序后便于后续 Merge 与 Reduce 拉取 |
| Shuffle | Map 输出到 Reduce 输入之间的数据搬运、排序、分组 | 像「各部门交表后,总部按姓氏排序、装订成册再分给不同汇总员」 | 为什么叫 Shuffle?像洗牌一样把数据重新打乱再按 key 归拢,是 MR 里最耗时的阶段之一 |
| Combiner | Map 端局部聚合,在 Shuffle 前先做一次「小 Reduce」 | 像「各部门先把自己部门的数加总,再交表,而不是交一摞原始单子」 | 为什么能减少网络?同一 key 的多个 value 在 Map 端先合并,传的数据量变小;但必须满足结合律(如求和) |
| Container | YARN 的资源容器,封装 CPU+内存,跑一个 Task | 像「一个工位:一张桌子+一台电脑」 | 为什么需要?YARN 按 Container 分配资源,Map/Reduce Task 都在 Container 里跑,便于隔离与调度 |
| ApplicationMaster (AM) | 每个 Job 一个,向 RM 要资源、管理 Task 生命周期 | 像「项目经理:向公司要人、派活、盯进度」 | 为什么每个 Job 一个?不同 Job 的 Map/Reduce 数量、依赖不同,AM 负责本 Job 的调度与容错 |
配置参数(为什么这样设?)
| 参数 | 典型含义 | 为什么? |
|---|---|---|
| mapreduce.job.reduces / setNumReduceTasks | Reduce 任务数 | 太少则单 Reduce 成瓶颈;太多则小文件多、启动开销大;0 表示只有 Map 无 Reduce |
| io.sort.mb(旧)/ mapreduce.task.io.sort.mb | Map 端环形缓冲区大小 | 越大溢写次数越少,但占内存;默认约 100MB 是经验折中 |
| mapreduce.map.output.compress | 是否压缩 Map 输出 | 压缩减少 Shuffle 网络和磁盘 IO,用少量 CPU 换大量 IO,通常值得开 |
| mapreduce.map.speculative | Map 是否推测执行 | 慢 Task 会拖慢整个 Job;再起一份备份谁先完成用谁,可防「拖后腿」节点 |
🗺️ Map 阶段详解
Map 的作用
Map 阶段负责将输入数据拆分成独立的键值对,并进行初步处理。
生活类比:
- 公司年终报告
- Map = 各部门各自统计自己的数据
- Reduce = 汇总各部门数据形成总报告
Map 工作流程
📂 输入数据
InputSplit
📖 RecordReader
读取记录
✏️ Map 函数
用户自定义
🔀 Partition
分区
📋 Sort
排序
💾 Spill
溢写磁盘
📦 Merge
合并文件
Map 阶段详细说明
1. InputSplit(输入分片)
📂 大文件 1TB
📄 分片1: 128MB
📄 分片2: 128MB
📄 分片3: 128MB
📄 ...共 8000+ 分片
InputSplit 是 MapReduce 对输入数据的逻辑划分,每个分片由一个 Map Task 处理。
为什么默认 128MB? 与 HDFS 块大小(默认 128MB)对齐,这样多数分片可以数据本地化(Task 和块在同一节点),减少网络拉取。分片太大则单 Task 耗时长、失败重试成本高;太小则 Task 数过多、调度与启动开销大。
| 属性 | 说明 | 默认值 |
|---|---|---|
| 分片大小 | 每个 Split 的数据量 | 128MB (HDFS 块大小) |
| 分片数量 = 总数据量 / 分片大小 | 决定 Map 任务数 |
2. RecordReader(记录读取器)
将输入分片转换为键值对记录。为什么键是偏移量(如 LongWritable)? 文本按行切时,框架需要唯一标识每一行,用「文件内字节偏移」可以保证不重复且便于定位;若不需要 key,Map 里可以忽略不用。
java
// TextInputFormat 示例
// 输入文件:
// Hello World
// Hello Hadoop
// 输出键值对:
// (0, "Hello World") <- 键是行首偏移量,值是行内容
// (12, "Hello Hadoop") <- 键是第二行的偏移量
3. Map 函数
用户自定义的核心处理逻辑。
java
// WordCount 的 Map 函数
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 {
// 输入:行偏移量, 行内容
// "Hello World Hello"
String line = value.toString();
String[] words = line.split("\\s+");
for (String w : words) {
word.set(w);
// 输出:(Hello, 1), (World, 1), (Hello, 1)
context.write(word, one);
}
}
}
Map 输出示例:
输入: "Hello World Hello"
输出: (Hello, 1)
(World, 1)
(Hello, 1)
4. Partition(分区)
决定每个键值对发送到哪个 Reducer。为什么必须按 key 分区? 因为 Reduce 是按 key 分组汇总的,同一 key 的所有 value 必须送到同一个 Reducer,否则结果会错(例如 WordCount 里 "Hello" 被拆到两个 Reduce 就得不到正确总数)。
java
// 默认的 HashPartitioner
public class HashPartitioner<K, V> extends Partitioner<K, V> {
@Override
public int getPartition(K key, V value, int numReduceTasks) {
// 根据键的哈希值决定分区
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}
5. Sort(排序)
在每个分区内按键排序。
排序前:
(Apple, 1)
(Banana, 1)
(Apple, 1)
(Cherry, 1)
(Banana, 1)
排序后:
(Apple, 1)
(Apple, 1)
(Banana, 1)
(Banana, 1)
(Cherry, 1)
6. Spill(溢写)
当内存缓冲区满时(默认 100MB),数据溢写到磁盘。为什么是 80% 就溢写? 留出 20% 空间给新来的数据,避免写盘期间缓冲区被撑爆;溢写与 Map 输出可并行,提高吞吐。
达到 80%
后台线程
🔄 环形缓冲区
100MB
💾 溢写到磁盘
📋 排序+分区
📁 生成溢写文件
7. Merge(合并)
将多个溢写文件合并成一个大文件。为什么先 Spill 再 Merge? 因为 Map 输出可能很大,无法一次性在内存排序;先按 80% 阈值一段段排序后写盘,最后多路归并成一个大有序文件,Reduce 拉取时只需顺序读。
🔀 Shuffle 阶段详解
什么是 Shuffle?
Shuffle 是 MapReduce 的核心,负责将 Map 的输出传输到 Reduce。它是 Map 和 Reduce 之间的"数据搬运工"。
为什么叫 Shuffle?
就像洗牌一样,把数据重新排列组合。
Shuffle 工作流程
Reduce端
数据传输
Map端
🗺️ Map 输出
💾 内存缓冲区
🔀 分区排序
💾 溢写磁盘
📦 合并文件
📤 HTTP 拉取
🌐 网络传输
💾 内存缓冲
📋 归并排序
📊 Reduce 输入
Map 端 Shuffle
java
// Map 端 Shuffle 配置优化
// 1. 环形缓冲区大小(默认 100MB)
conf.set("io.sort.mb", "200");
// 2. 溢写阈值(默认 80%)
conf.set("io.sort.record.percent", "0.9");
// 3. 溢写工作线程数
conf.set("io.sort.factor", "10");
// 4. 压缩 Map 输出
conf.set("mapreduce.map.output.compress", "true");
conf.set("mapreduce.map.output.compress.codec",
"org.apache.hadoop.io.compress.SnappyCodec");
Reduce 端 Shuffle
为什么 Reduce 要主动拉取(Pull)而不是 Map 主动推(Push)? Pull 模式下 Reduce 按自己进度拉取,避免 Map 输出爆满时拖垮 Map 端;且多个 Reduce 可并行从多个 Map 拉,负载更均衡。
🗺️ Map Task 3 🗺️ Map Task 2 🗺️ Map Task 1 📊 Reducer 🗺️ Map Task 3 🗺️ Map Task 2 🗺️ Map Task 1 📊 Reducer 启动拷贝线程 启动拷贝线程 启动拷贝线程 输出文件 1 输出文件 2 输出文件 3 💾 磁盘合并 📋 归并排序 ✏️ 送给 Reduce 函数
Shuffle 性能优化
| 优化点 | 配置参数 | 说明 |
|---|---|---|
| 压缩 | mapreduce.map.output.compress | 减少 IO 和网络传输 |
| 缓冲区大小 | mapreduce.task.io.sort.mb | 增大减少溢写次数 |
| 并行拷贝 | mapreduce.reduce.shuffle.parallelcopies | 增加拷贝线程 |
| 预取 | mapreduce.reduce.shuffle.input.buffer.percent | 提前拉取数据 |
📊 Reduce 阶段详解
Reduce 的作用
Reduce 阶段负责接收 Map 的输出,按键分组,进行最终汇总计算。
生活类比:
- 各省(Map)统计完人口后
- 中央统计局(Reduce)汇总成全国总人口
Reduce 工作流程
🔀 Shuffle 输出
已排序的键值对
📂 Grouping
按键分组
✏️ Reduce 函数
用户自定义
📁 OutputFormat
写入文件系统
Reduce 函数示例
java
// WordCount 的 Reduce 函数
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 {
// 输入:(Hello, [1, 1, 1, 1, ...])
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
// 输出:(Hello, 1000)
context.write(key, result);
}
}
Reduce 输入结构
输入格式:键 + 值迭代器
示例:
键:"Hello"
值:[1, 1, 1, 1, 1, 1, 1, 1, 1, 1] (10个1)
处理逻辑:
遍历所有值,求和
输出:(Hello, 10)
Reduce 任务数量
bash
# 设置 Reduce 任务数量
conf.setNumReduceTasks(3);
# 如何确定 Reduce 数量?
# - 太少:单个 Reduce 负担重,成为瓶颈
# - 太多:小文件增多,集群开销大
# - 经验公式:0.95 * 集群节点数 * 每节点任务槽位数
为什么 0 个 Reduce 也可以? 有些作业只需要做 Map(如过滤、格式转换),不需要按 key 汇总,设 setNumReduceTasks(0) 即可,输出直接写 HDFS,文件数为 Map 数。
| 数量 | 适用场景 | 注意事项 |
|---|---|---|
| 0 个 | 只需 Map,不需要汇总 | 只有 Map,没有 Reduce |
| 1 个 | 小数据集 | 所有数据到一个 Reduce |
| 多个 | 大数据集 | 需要合理分区 |
🚀 完整 WordCount 示例
完整代码
java
import java.io.IOException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCount {
// Mapper 类
public static class TokenizerMapper
extends Mapper<LongWritable, Text, Text, IntWritable>{
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
@Override
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 输入:(行偏移量, 行内容)
String line = value.toString();
String[] tokens = line.split("\\s+");
for (String token : tokens) {
if (token.length() > 0) {
word.set(token);
context.write(word, one);
}
}
}
}
// Reducer 类
public static class IntSumReducer
extends Reducer<Text,IntWritable,Text,IntWritable> {
private IntWritable result = new IntWritable();
@Override
public 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);
}
}
// Driver 类
public static void main(String[] args) throws Exception {
// 创建配置
Configuration conf = new Configuration();
// 创建 Job
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
// 设置 Mapper 和 Reducer
job.setMapperClass(TokenizerMapper.class);
job.setReducerClass(IntSumReducer.class);
// 设置输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 设置输入输出路径
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 等待任务完成
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
编译和运行
bash
# 1. 编译
hadoop com.sun.tools.javac.Main WordCount.java
jar cf wc.jar WordCount*.class
# 2. 创建测试数据
hdfs dfs -mkdir /user/input
echo "Hello World Hello Hadoop" > /tmp/test.txt
hdfs dfs -put /tmp/test.txt /user/input/
# 3. 运行
hadoop jar wc.jar WordCount /user/input /user/output
# 4. 查看结果
hdfs dfs -cat /user/output/part-r-00000
# 输出:
# Hadoop 1
# Hello 2
# World 1
🔧 高级特性
Combiner(规约器)
什么是 Combiner?
Combiner 是 Map 端的"小 Reduce",在 Shuffle 前先进行局部汇总。
是
否
🗺️ Map 输出
有 Combiner?
📊 局部汇总
减少网络传输
🔀 直接 Shuffle
生活类比:
- 没有 Combiner = 每个人把自己统计的纸条全部送到总部
- 有 Combiner = 每个人先汇总自己的结果,只送汇总后的数据
java
// 设置 Combiner(与 Reducer 相同)
job.setCombinerClass(IntSumReducer.class);
// WordCount 示例:
// Map 输出:(Hello, 1), (Hello, 1), (Hello, 1), (Hello, 1), (Hello, 1)
// 有 Combiner:(Hello, 5) <- 只传输 5 而不是 5 个 1
// 没有 Combiner:传输 (Hello, 1) 五次
| 场景 | 可以用 Combiner | 不能用 Combiner |
|---|---|---|
| 求和/计数 | ✅ 可交换结合 | - |
| 最大/最小值 | ✅ 可交换结合 | - |
| 平均值 | ❌ 不能直接用 | 需要特殊处理 |
| 去重 | ❌ 不能直接用 | 会丢失数据 |
为什么平均值不能直接用 Combiner? 因为 (sum1/count1 + sum2/count2)/2 ≠ (sum1+sum2)/(count1+count2) 的等价形式不能简单「先平均再平均」;若用 Combiner,应输出 (局部 sum, 局部 count),在 Reduce 里再总 sum / 总 count。
数据本地化(Data Locality)
为什么需要数据本地化?
网络传输比磁盘读取慢很多,让计算靠近数据可以显著提升性能。
理想
次优
最差
📂 数据在 Node A
任务调度
✅ Task 在 Node A 执行
DATA_LOCAL
🟡 Task 在同机架执行
RACK_LOCAL
❌ Task 在远程执行
OFF_SWITCH
| 本地化级别 | 说明 | 性能 |
|---|---|---|
| NODE_LOCAL | 任务和数据在同一节点 | 最快 |
| RACK_LOCAL | 任务和数据在同一机架 | 较快 |
| OFF_SWITCH | 跨机架执行 | 较慢 |
推测执行(Speculative Execution)
什么是推测执行?
当一个任务运行明显慢于其他任务时,Hadoop 会在另一个节点上启动一个相同任务的备份,哪个先完成就采用哪个的结果。

bash
# 启用推测执行
<property>
<name>mapreduce.map.speculative</name>
<value>true</value>
</property>
<property>
<name>mapreduce.reduce.speculative</name>
<value>true</value>
</property>
为什么任务必须幂等? 因为同一份数据可能被原始 Task 和备份 Task 各写一次(或先写后杀),若写库、发消息等有副作用,就会重复;只读或「同一 key 多次写同一结果」则安全。
自定义 Partitioner
java
// 自分区示例:按首字母分区
public static class FirstLetterPartitioner
extends Partitioner<Text, IntWritable> {
@Override
public int getPartition(Text key, IntWritable value, int numPartitions) {
String word = key.toString();
char firstChar = word.charAt(0);
// A-M 到 partition 0
if (firstChar >= 'A' && firstChar <= 'M') {
return 0;
}
// N-Z 到 partition 1
else {
return 1 % numPartitions;
}
}
}
// 设置自定义 Partitioner
job.setPartitionerClass(FirstLetterPartitioner.class);
job.setNumReduceTasks(2);
📊 MapReduce 与 YARN
YARN 架构
YARN
提交 Job
👤 Client
🏢 ResourceManager
📅 Scheduler
资源调度
📋 ApplicationsManager
应用管理
🖥️ NodeManager 1
🖥️ NodeManager 2
🖥️ NodeManager 3
📌 ApplicationMaster
每个 Job 一个
📦 Container 1
Map Task
📦 Container 2
Reduce Task
YARN 组件说明
| 组件 | 职责 | 生活类比 |
|---|---|---|
| ResourceManager | 全局资源管理 | 公司总经理 |
| Scheduler | 分配资源 | 人事经理 |
| NodeManager | 管理单个节点资源 | 部门主管 |
| ApplicationMaster | 管理单个应用 | 项目经理 |
| Container | 资源容器 | 办公工位 |
🔍 监控与调试
常用监控命令
bash
# 1. 查看运行中的任务
mapred job -list
# 2. 查看任务详情
mapred job -status job_1234567890000_0001
# 3. 杀死任务
mapred job -kill job_1234567890000_0001
# 4. 查看任务日志
mapred job -logs job_1234567890000_0001
# 5. 查看任务计数器
mapred job -counter job_1234567890000_0001
| 命令 | 为什么常用? |
|---|---|
| mapred job -list | 作业卡住时先看是否在跑、拿到 job_id 才能查 status/logs |
| mapred job -status | 看进度百分比、Map/Reduce 完成数、失败 Task 列表,定位是否数据倾斜或单 Task 失败 |
| mapred job -logs | 失败时看具体异常栈和业务日志,不加 container 会列出所有 Task 的 log 路径 |
| mapred job -counter | 看 MAP_INPUT_RECORDS 等是否异常(如为 0 可能是输入路径或格式问题) |
Web UI
bash
# 访问 MapReduce Web UI
http://resourcemanager:8088/
# 查看的信息:
# - 运行中的任务
# - 历史任务
# - 任务统计信息
# - 日志查看
常见计数器
| 计数器组 | 计数器 | 说明 |
|---|---|---|
| MapReduce 框架 | MAP_INPUT_RECORDS | Map 读取的记录数 |
| MAP_OUTPUT_RECORDS | Map 输出的记录数 | |
| REDUCE_INPUT_RECORDS | Reduce 输入的记录数 | |
| REDUCE_OUTPUT_RECORDS | Reduce 输出的记录数 | |
| 文件系统 | BYTES_READ | 读取的字节数 |
| BYTES_WRITTEN | 写入的字节数 |
🚨 性能优化
优化策略
🚀 MapReduce 性能优化
🔀 减少 Shuffle
⚖️ 优化数据倾斜
⚙️ 调整参数
📊 合理分区
📊 使用 Combiner
🗜️ 压缩中间数据
📉 减少 Map 输出
🔀 自定义 Partitioner
📌 预处理倾斜 Key
💾 调整缓冲区大小
📈 调整并行度
📊 合理设置 Reduce 数
📁 避免小文件
优化配置
xml
<!-- mapred-site.xml -->
<!-- 启用压缩 -->
<property>
<name>mapreduce.map.output.compress</name>
<value>true</value>
</property>
<!-- Compressor codec -->
<property>
<name>mapreduce.map.output.compress.codec</name>
<value>org.apache.hadoop.io.compress.SnappyCodec</value>
</property>
<!-- 增大 Shuffle 缓冲区 -->
<property>
<name>mapreduce.reduce.shuffle.input.buffer.percent</name>
<value>0.7</value>
</property>
<!-- 并行拷贝数 -->
<property>
<name>mapreduce.reduce.shuffle.parallelcopies</name>
<value>10</value>
</property>
<!-- 推测执行 -->
<property>
<name>mapreduce.map.speculative</name>
<value>true</value>
</property>
🎯 总结
核心要点记忆口诀
MapReduce 大数据
Map 处理分而治
Shuffle 传数据排序
Reduce 汇总出结果
Combiner 减传输
本地化提性能
推测执行防慢节点
YARN 管理调度灵
WordCount 完整流程
📂 输入文件
📄 Split 1
📄 Split 2
🗺️ Map 1
🗺️ Map 2
📊 Combiner
🔀 Shuffle & Sort
📊 Reduce
📁 输出文件
生活类比总结
- Map = 各部门统计数据
- Shuffle = 把各部门数据汇总整理
- Reduce = 总部形成最终报告
- Combiner = 各部门先汇总再上报
- 数据本地化 = 就地处理,不搬砖
- 推测执行 = 慢了就叫人帮忙一起做
最后提醒:MapReduce 适合批处理,不适合低延迟交互式查询!对于实时处理,考虑使用 Spark 或 Flink!
📚 官方文档与参考
| 资源 | 链接 | 说明 |
|---|---|---|
| MapReduce Tutorial | Apache Hadoop MapReduce Tutorial | 官方入门教程与 WordCount 示例 |
| Hadoop 文档首页 | Apache Hadoop | 当前版本文档,含 HDFS、YARN、MapReduce |
| MapReduce API | org.apache.hadoop.mapreduce | Mapper、Reducer、Job、InputFormat 等 API 说明 |
MapReduce 与其它计算框架对比(相近方案)
| 维度 | MapReduce | Spark | Flink |
|---|---|---|---|
| 计算模型 | 批处理,Map+Reduce 两阶段 | 批/流统一,内存迭代 DAG | 流优先,批为流特例 |
| 延迟 | 高(多轮落盘) | 中(内存为主) | 低(流式、增量) |
| 适用场景 | 大批量、一次性 ETL、历史分析 | 迭代 ML、交互查询、批处理 | 实时流、事件驱动、精确一次 |
| 生活类比 | 月底统一盘点、报表 | 随时查库存、多轮试算 | 流水线边生产边统计 |