MapReduce 编程模型详解:Mapper、Reducer、Driver 三大核心组件

💝💝💝首先,欢迎各位来到我的博客,很高兴能够在这里和您见面!希望您在这里不仅可以有所收获,同时也能感受到一份轻松欢乐的氛围,祝你生活愉快!
💝💝💝关注!关注!!请关注!!!请大家关注下博主,您的支持是我不断创作的最大动力!!!

文章目录

    • 引言
    • 一、编程模型总览
    • [二、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 程序看似复杂,但骨架只有三个部分:MapperReducerDriver。理解这三个组件各自的职责和它们之间的数据流转,就能写出任何 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,因此不能用 StringInteger,必须用 TextIntWritable 等。
  • 对象重用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 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

相关推荐
这是程序猿1 小时前
ComfyUI 教程合集|AI绘图、ControlNet、Lora、IPAdapter、视频生成全攻略
大数据·人工智能·windows·音视频
林恒smileZAZ2 小时前
CSS 滚动驱动动画(scroll-timeline):无 JS 实现滚动特效
前端·javascript·css
懒人村杂货铺2 小时前
Express + TypeScript 后端通用标准规范
javascript·typescript·express
kyriewen2 小时前
你的数据该在哪儿拿?Next.js三种姿势一次讲清
前端·javascript·next.js
凯强同学2 小时前
投资更偏向实践类知识
大数据
傻啦嘿哟2 小时前
管好PPT的“骨架”:用Python控制页面与文档属性
开发语言·javascript·c#
互联网推荐官2 小时前
上海软件定制开发与数字化建设:D-coding 全平台应用架构及实施指南
大数据·运维
csgo打的菜又爱玩2 小时前
9.Dispatcher 启动流程解析
大数据·flink·源代码管理
狂奔蜗牛飙车2 小时前
大数据赛项(中职组)-三个节点的创建及名字网络配置
大数据·大数据入门·大数据竞赛指南(中职组)·centos三节点创建·主机名及ip地址配置