💝💝💝首先,欢迎各位来到我的博客,很高兴能够在这里和您见面!希望您在这里不仅可以有所收获,同时也能感受到一份轻松欢乐的氛围,祝你生活愉快!
💝💝💝关注!关注!!请关注!!!请大家关注下博主,您的支持是我不断创作的最大动力!!!
文章目录
-
- 引言
- 一、编程模型总览
- [二、Mapper 类:数据的局部处理器](#二、Mapper 类:数据的局部处理器)
-
- [2.1 Mapper 的作用](#2.1 Mapper 的作用)
- [2.2 Mapper 类的完整签名](#2.2 Mapper 类的完整签名)
- [2.3 四个泛型参数的含义](#2.3 四个泛型参数的含义)
- [2.4 Mapper 的生命周期方法](#2.4 Mapper 的生命周期方法)
- [2.5 Mapper 的经典代码示例](#2.5 Mapper 的经典代码示例)
- [2.6 Mapper 中泛型的关键注意点](#2.6 Mapper 中泛型的关键注意点)
- [三、Reducer 类:全局数据的聚合器](#三、Reducer 类:全局数据的聚合器)
-
- [3.1 Reducer 的作用](#3.1 Reducer 的作用)
- [3.2 Reducer 类的完整签名](#3.2 Reducer 类的完整签名)
- [3.3 四个泛型参数的含义](#3.3 四个泛型参数的含义)
- [3.4 Reducer 的生命周期](#3.4 Reducer 的生命周期)
- [3.5 Reducer 的数量控制](#3.5 Reducer 的数量控制)
- [3.6 Combiner:特殊的 Reducer](#3.6 Combiner:特殊的 Reducer)
- [四、Driver 类:作业的指挥官](#四、Driver 类:作业的指挥官)
-
- [4.1 Driver 的作用](#4.1 Driver 的作用)
- [4.2 Driver 的标准结构](#4.2 Driver 的标准结构)
- [4.3 Driver 中的关键配置方法](#4.3 Driver 中的关键配置方法)
- [4.4 输入格式与输出格式](#4.4 输入格式与输出格式)
- 五、完整实战案例:WordCount
-
- [5.1 Mapper 类](#5.1 Mapper 类)
- [5.2 Reducer 类](#5.2 Reducer 类)
- [5.3 Driver 类](#5.3 Driver 类)
- [5.4 打包与运行](#5.4 打包与运行)
- 六、进阶案例:自定义类型与多阶段处理
-
- [6.1 自定义 Writable 类型](#6.1 自定义 Writable 类型)
- [6.2 多阶段作业链接(Job Chaining)](#6.2 多阶段作业链接(Job Chaining))
- 七、泛型参数常见错误与调试
- 八、总结:三类组件的职责与关系
引言
MapReduce 程序看似复杂,但骨架只有三个部分:Mapper 、Reducer 和 Driver。理解这三个组件各自的职责和它们之间的数据流转,就能写出任何 MapReduce 作业。
本文将全面解析这三类核心组件的设计理念、生命周期、泛型参数含义,并通过完整的 WordCount 案例和多个进阶示例,让你彻底掌握 MapReduce 编程。
一、编程模型总览
MapReduce 将数据处理抽象为两个阶段:Map 和 Reduce。用户只需要实现这三个类,框架自动处理分布式执行的细节。
配置并提交
配置并提交
原始数据
Mapper
Shuffle & Sort
Reducer
最终结果
Driver
| 组件 | 核心职责 | 运行位置 | 执行次数 |
|---|---|---|---|
| Mapper | 将输入键值对转换为中间键值对 | 数据所在节点(本地性) | 每个输入分片一个 |
| Reducer | 将同一键的所有值聚合为最终结果 | 任意节点 | 每个分区一个 |
| Driver | 配置作业、设置参数、提交任务 | 客户端机器 | 1 次 |
二、Mapper 类:数据的局部处理器
2.1 Mapper 的作用
Mapper 负责处理输入数据的一个分片。框架会为每个输入分片(InputSplit)启动一个独立的 Mapper 任务。Mapper 的输出是一组中间键值对,这些数据会经过 Shuffle 阶段传递到 Reducer。
2.2 Mapper 类的完整签名
java
public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
protected void setup(Context context) throws IOException, InterruptedException {}
protected void map(KEYIN key, VALUEIN value, Context context) throws IOException, InterruptedException {}
protected void cleanup(Context context) throws IOException, InterruptedException {}
public void run(Context context) throws IOException, InterruptedException {
setup(context);
while (context.nextKeyValue()) {
map(context.getCurrentKey(), context.getCurrentValue(), context);
}
cleanup(context);
}
}
2.3 四个泛型参数的含义
| 参数 | 名称 | 说明 | 常见类型 |
|---|---|---|---|
KEYIN |
输入键类型 | 输入记录中键的类型 | LongWritable(行偏移量) |
VALUEIN |
输入值类型 | 输入记录中值的类型 | Text(行内容) |
KEYOUT |
输出键类型 | map 方法输出的键的类型 | Text(单词) |
VALUEOUT |
输出值类型 | map 方法输出的值的类型 | IntWritable(计数1) |
关键理解 :输入类型由 InputFormat 决定,默认 TextInputFormat 产生 <LongWritable, Text>。输出类型由业务逻辑决定,可以是任何 Hadoop Writable 类型或自定义类型。
2.4 Mapper 的生命周期方法
Mapper 的生命周期由框架控制,包含三个可重写的方法:
1. setup():在 map 方法之前调用一次。用于一次性初始化操作,如建立数据库连接、加载配置文件、初始化计数器等。
java
@Override
protected void setup(Context context) throws IOException, InterruptedException {
// 从配置中读取参数
Configuration conf = context.getConfiguration();
String filterWord = conf.get("filter.word");
// 初始化资源
this.filter = filterWord;
}
2. map():对输入分片中的每条记录调用一次。核心逻辑所在地。
java
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 处理一行数据
String line = value.toString();
// 输出中间结果
context.write(outputKey, outputValue);
}
3. cleanup():在所有 map 调用结束后执行一次。用于释放资源、输出统计信息等。
java
@Override
protected void cleanup(Context context) throws IOException, InterruptedException {
// 关闭连接、输出汇总日志
context.getCounter("MyCounters", "total_lines").increment(totalLines);
}
2.5 Mapper 的经典代码示例
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 {
String[] words = value.toString().split("\\s+");
for (String w : words) {
word.set(w);
context.write(word, one);
}
}
}
2.6 Mapper 中泛型的关键注意点
- Writable 要求 :所有键值类型必须实现
Writable接口,键还需实现WritableComparable(用于排序)。 - 序列化性能 :Hadoop 使用自己的序列化框架而非 Java 原生 Serializable,因此不能用
String或Integer,必须用Text、IntWritable等。 - 对象重用 :
map方法中的 key 和 value 对象会被框架重用,不要在外部持有它们的引用。
三、Reducer 类:全局数据的聚合器
3.1 Reducer 的作用
Reducer 接收所有 Mapper 输出中属于同一分区的数据,并按照键进行分组。框架保证每个 Reducer 收到的数据是按键有序的,且同一个键的所有值会连续传递。
3.2 Reducer 类的完整签名
java
public class Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
protected void setup(Context context) throws IOException, InterruptedException {}
protected void reduce(KEYIN key, Iterable<VALUEIN> values, Context context)
throws IOException, InterruptedException {}
protected void cleanup(Context context) throws IOException, InterruptedException {}
public void run(Context context) throws IOException, InterruptedException {
setup(context);
while (context.nextKey()) {
reduce(context.getCurrentKey(), context.getValues(), context);
}
cleanup(context);
}
}
3.3 四个泛型参数的含义
| 参数 | 名称 | 说明 | 来源 |
|---|---|---|---|
KEYIN |
输入键类型 | map 输出键的类型 | 与 Mapper 的 KEYOUT 一致 |
VALUEIN |
输入值类型 | map 输出值的类型 | 与 Mapper 的 VALUEOUT 一致 |
KEYOUT |
输出键类型 | reduce 方法输出的键的类型 | 业务决定 |
VALUEOUT |
输出值类型 | reduce 方法输出的值的类型 | 业务决定 |
重要:Reducer 的输入类型必须与 Mapper 的输出类型完全匹配。
3.4 Reducer 的生命周期
1. setup():在 reduce 方法之前调用一次。通常用于初始化聚合器或加载全局资源。
2. reduce() :对每个唯一的键调用一次。Iterable<VALUEIN> values 包含该键对应的所有值。迭代器只能遍历一次,需要累积时及时保存。
java
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
context.write(key, new IntWritable(sum));
}
3. cleanup():在所有 reduce 调用后执行一次。用于最后的输出或资源清理。
3.5 Reducer 的数量控制
Reducer 的数量决定最终输出文件的数量。在 Driver 中设置:
java
job.setNumReduceTasks(3); // 启动 3 个 Reducer,产生 3 个输出文件
设置原则:
- 设为 0:无 Reduce 阶段,Mapper 输出直接作为结果(适合只做过滤、转换的作业)
- 设为 1:所有数据进入一个 Reducer,产生一个输出文件,但会成为性能瓶颈
- 设为 N:产生 N 个输出文件,适合后续需要并行读取的场景
3.6 Combiner:特殊的 Reducer
Combiner 是在 Mapper 端运行的"微型 Reducer",用于减少 Shuffle 数据量。它必须继承 Reducer 类,且逻辑需满足交换律和结合律。
java
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
// 逻辑与 Reducer 完全相同
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) sum += val.get();
context.write(key, new IntWritable(sum));
}
}
// 在 Driver 中设置
job.setCombinerClass(WordCountCombiner.class);
四、Driver 类:作业的指挥官
4.1 Driver 的作用
Driver 是 MapReduce 作业的客户端程序,负责:
- 创建和配置
Job对象 - 设置输入/输出路径
- 指定 Mapper、Reducer、Combiner 类
- 设置输出键值类型
- 提交作业并监控执行状态
4.2 Driver 的标准结构
java
public class WordCountDriver {
public static void main(String[] args) throws Exception {
// 1. 获取配置对象
Configuration conf = new Configuration();
// 2. 创建 Job 实例
Job job = Job.getInstance(conf, "word count");
// 3. 设置主类(通过 Jar 方式运行)
job.setJarByClass(WordCountDriver.class);
// 4. 设置 Mapper 和 Reducer 类
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
// 5. 设置 Combiner(可选)
job.setCombinerClass(WordCountReducer.class);
// 6. 设置输出类型(必须设置)
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 7. 设置 Map 输出类型(如果与最终输出不同,必须单独设置)
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 8. 设置输入输出路径
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 9. 提交作业并等待完成
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
4.3 Driver 中的关键配置方法
| 方法 | 作用 | 是否必须 |
|---|---|---|
setJarByClass() |
指定包含 Mapper/Reducer 的 Jar,用于分发到集群 | 是 |
setMapperClass() |
指定 Mapper 类 | 是 |
setReducerClass() |
指定 Reducer 类 | 否(可设 Reduce 数为 0) |
setOutputKeyClass() / setOutputValueClass() |
设置最终输出类型 | 是 |
setMapOutputKeyClass() / setMapOutputValueClass() |
设置 Mapper 输出类型 | 若与最终输出不同则必须 |
setNumReduceTasks() |
设置 Reducer 数量 | 默认 1 |
setCombinerClass() |
设置 Combiner 类 | 可选 |
setPartitionerClass() |
设置自定义分区器 | 可选 |
setSortComparatorClass() |
设置自定义排序比较器 | 可选 |
setGroupingComparatorClass() |
设置自定义分组比较器 | 可选 |
4.4 输入格式与输出格式
Driver 中可以自定义输入输出格式:
java
// 设置输入格式(默认 TextInputFormat)
job.setInputFormatClass(TextInputFormat.class);
// 设置输出格式(默认 TextOutputFormat)
job.setOutputFormatClass(TextOutputFormat.class);
// 使用 SequenceFile 输入/输出
job.setInputFormatClass(SequenceFileInputFormat.class);
job.setOutputFormatClass(SequenceFileOutputFormat.class);
五、完整实战案例:WordCount
下面给出一个完整的 WordCount 程序,包含 Mapper、Reducer 和 Driver。
5.1 Mapper 类
java
package com.example.wordcount;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
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 {
String line = value.toString();
String[] words = line.split("\\s+");
for (String w : words) {
word.set(w);
context.write(word, one);
}
}
}
5.2 Reducer 类
java
package com.example.wordcount;
import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
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);
}
}
5.3 Driver 类
java
package com.example.wordcount;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
public class WordCountDriver {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: WordCountDriver <input path> <output path>");
System.exit(-1);
}
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCountDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setCombinerClass(WordCountReducer.class);
job.setReducerClass(WordCountReducer.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);
}
}
5.4 打包与运行
bash
# 编译打包
mvn clean package
# 准备输入文件
hdfs dfs -mkdir -p /wordcount/input
hdfs dfs -put /path/to/textfile.txt /wordcount/input
# 运行作业
hadoop jar target/wordcount-1.0.jar com.example.wordcount.WordCountDriver \
/wordcount/input /wordcount/output
# 查看结果
hdfs dfs -cat /wordcount/output/part-r-00000
六、进阶案例:自定义类型与多阶段处理
6.1 自定义 Writable 类型
java
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class FlowBean implements WritableComparable<FlowBean> {
private long upFlow; // 上行流量
private long downFlow; // 下行流量
private long sumFlow; // 总流量
// 无参构造(必须)
public FlowBean() {}
public FlowBean(long upFlow, long downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFlow = upFlow + downFlow;
}
// 序列化方法
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}
// 反序列化方法(顺序与序列化一致)
@Override
public void readFields(DataInput in) throws IOException {
this.upFlow = in.readLong();
this.downFlow = in.readLong();
this.sumFlow = in.readLong();
}
// 排序比较(按总流量降序)
@Override
public int compareTo(FlowBean o) {
return Long.compare(o.sumFlow, this.sumFlow);
}
// getters/setters 省略
}
6.2 多阶段作业链接(Job Chaining)
复杂业务可以通过多个 MapReduce 作业串联完成,使用 JobControl 管理依赖关系:
java
import org.apache.hadoop.mapreduce.lib.jobcontrol.ControlledJob;
import org.apache.hadoop.mapreduce.lib.jobcontrol.JobControl;
public class JobChainDriver {
public static void main(String[] args) throws Exception {
Configuration conf1 = new Configuration();
Job job1 = Job.getInstance(conf1, "stage1");
// 配置 job1 ...
Configuration conf2 = new Configuration();
Job job2 = Job.getInstance(conf2, "stage2");
// 配置 job2 ... 依赖 job1 的输出
ControlledJob ctrlJob1 = new ControlledJob(conf1);
ControlledJob ctrlJob2 = new ControlledJob(conf2);
ctrlJob2.addDependingJob(ctrlJob1);
JobControl jobControl = new JobControl("myJobChain");
jobControl.addJob(ctrlJob1);
jobControl.addJob(ctrlJob2);
Thread thread = new Thread(jobControl);
thread.start();
while (!jobControl.allFinished()) {
Thread.sleep(1000);
}
jobControl.stop();
}
}
七、泛型参数常见错误与调试
| 错误现象 | 常见原因 | 解决方案 |
|---|---|---|
java.lang.ClassCastException |
Mapper 输出类型与 Reducer 输入类型不匹配 | 检查四个泛型参数是否对齐 |
Type mismatch in key from map |
未设置 setMapOutputKeyClass() |
当 Mapper 输出与最终输出不同时必须设置 |
No enum constant |
自定义类型缺少无参构造或序列化方法 | 添加无参构造,实现 readFields/write |
Object cannot be cast to WritableComparable |
键类型未实现 WritableComparable |
让自定义键类实现该接口 |
Input path does not exist |
输入路径错误 | 检查 HDFS 路径是否存在 |
调试技巧
在 Mapper/Reducer 中使用计数器:
java
context.getCounter("MyGroup", "map_input_records").increment(1);
查看作业日志:
bash
yarn logs -applicationId application_xxx
八、总结:三类组件的职责与关系
| 组件 | 核心方法 | 输入 → 输出 | 运行次数 |
|---|---|---|---|
| Mapper | map(key, value, context) |
原始记录 → 中间键值对 | 每个输入分片一次 |
| Reducer | reduce(key, values, context) |
中间键值对(按key分组)→ 最终结果 | 每个唯一键一次 |
| Driver | main() |
配置参数 → 提交作业 | 一次 |
泛型对应关系:
- Mapper 的
KEYOUT,VALUEOUT= Reducer 的KEYIN,VALUEIN - Mapper 的
KEYIN,VALUEIN由输入格式决定 - Reducer 的
KEYOUT,VALUEOUT由输出格式决定
掌握这三个类的规范写法,就掌握了 MapReduce 编程的全部骨架。剩下的就是根据业务逻辑填充 map 和 reduce 方法,以及灵活运用配置来优化性能。
你在写 MapReduce 程序时遇到过最诡异的类型错误是什么?欢迎在评论区分享你的踩坑经验~
❤️❤️❤️觉得有用的话点个赞 👍🏻 呗。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄
💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍
🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙