前哨站/第6章 MapReduce基础与编程
第6章 MapReduce基础与编程

第6章 MapReduce基础与编程

MapReduce 怎么把一个大任务拆成很多小任务、Shuffle 到底做了什么、两个案例代码怎么落地。

计算模型Shuffle案例编程

本章学习目标

  • 能说清 MapReduce 的分布式并行计算思想与特点
  • 能完整描述 MapReduce 的工作流程(切片→Map→Shuffle→Reduce)
  • 能独立完成单词统计程序的设计、编码与运行
  • 能完成二次排序案例并说明其设计思路

本章导览

核心内容

6.1

MapReduce概述

MapReduce 的核心思想是"分而治之 + 计算向数据靠拢":把大任务拆成互不依赖的小任务并行执行,再把结果汇总;同时尽量把计算放到数据所在的节点上,减少网络传输。

6.1.1

分布式并行计算

  • 数据被切成多个分片(InputSplit),每个分片交给一个 Map 任务
  • Map 任务在数据所在节点本地执行,避免搬运数据
  • 多个 Reduce 并行处理分区后的中间结果
  • 框架负责调度、容错与重试,用户只写 map() 与 reduce()
6.1.3

MapReduce的特点

优点局限
易于编程:只写两个函数不适合实时计算(秒级延迟做不到)
良好扩展性:加机器即扩容不适合流式计算(数据要先落盘)
高容错:任务失败自动重跑不适合有向图/迭代计算(多轮落盘慢)
适合海量数据离线批处理不适合大量小文件
6.2

MapReduce的工作流程

一次作业的完整链路:<b>输入分片 → Map → Shuffle(分区/排序/合并)→ Reduce → 输出</b>。其中 Shuffle 是性能关键,也是调优的主要战场。

6.2.1

输入分片和数据格式化

  • InputSplit 是逻辑分片,默认与 block 大小一致(128MB)
  • InputFormat 决定"怎么切、怎么读",常用 TextInputFormat 按行读
  • RecordReader 把分片解析成 <key, value> 交给 Mapper
分片数 = Map 任务数。想提高并行度可调小 mapreduce.input.fileinputformat.split.maxsize。
6.2.3

Shuffle过程

Shuffle 横跨 Map 端与 Reduce 端,是 MapReduce 中数据流动最复杂的阶段。

阶段做了什么
Map 端:溢写结果先写环形缓冲区(默认 100MB,80% 触发溢写),溢写到磁盘
Map 端:分区排序按 key 分区(Partitioner)、分区内按 key 排序
Map 端:合并多个溢写文件合并(Combiner 可提前聚合,减少数据量)
Reduce 端:拉取各 Reduce 从所有 Map 拉取自己分区的数据
Reduce 端:归并拉取的数据做归并排序,形成有序的 <key, 迭代器>

💡 调优三板斧:① 加 Combiner 减少传输;② 调大缓冲区/溢写阈值;③ 合理设置 Reduce 数(默认 1,公式见 6.3.6)。

6.2.4

Reduce过程

  • 对每个 key 调用一次 reduce(),处理该 key 的所有 value
  • 输出通过 OutputFormat 写入 HDFS(默认 part-r-00000 等文件)
  • Reduce 数决定输出文件数:几个 Reduce 就有几个 part 文件
6.3

案例解析——单词统计编程

教材把这个案例拆成"案例描述 → 设计思路 → 执行过程 → 编程实现 → 运行程序 → 数据优化"六步,是 MapReduce 的标准讲法。

6.3.4

编程实现(Mapper/Reducer/Driver)

JAVA
// Mapper:每行拆词,输出 (word, 1)
public class WcMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    private Text k = new Text();
    private IntWritable v = new IntWritable(1);
    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        String[] words = value.toString().split("\\s+");
        for (String w : words) {
            if (w.isEmpty()) continue;
            k.set(w);
            context.write(k, v);       // (hello,1)
        }
    }
}

// Reducer:同一 key 的 value 累加
public class WcReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    private IntWritable out = new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context)
            throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable v : values) sum += v.get();
        out.set(sum);
        context.write(key, out);       // (hello,2)
    }
}

// Driver:组装作业
public class WcDriver {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        conf.set("fs.defaultFS", "hdfs://hadoop1:9000");
        Job job = Job.getInstance(conf, "wordcount");
        job.setJarByClass(WcDriver.class);
        job.setMapperClass(WcMapper.class);
        job.setReducerClass(WcReducer.class);
        job.setCombinerClass(WcReducer.class);      // 关键:本地先聚合
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.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);
    }
}
Combiner 本质是"在 Map 端先跑一次 Reduce",能大幅减少 Shuffle 数据量;但它要求运算满足结合律与交换律(求和可以,求平均值不行)。
6.3.5

运行程序

SHELL
# 1) 打成 jar 并上传
mvn clean package
scp target/wc.jar hadoop@hadoop1:~/

# 2) 准备数据并提交
hdfs dfs -mkdir -p /wc/input
hdfs dfs -put w.txt /wc/input/
hadoop jar wc.jar com.demo.WcDriver /wc/input /wc/output

# 3) 看结果
hdfs dfs -cat /wc/output/part-r-00000
6.3.6

数据优化

  • 加 Combiner 减少网络传输
  • 调 Reduce 个数:<code class="inl">reduce数 ≈ 输入数据量 / 每个Reduce处理量(默认 256MB)</code>
  • 开启 Map 端压缩:<code class="inl">mapreduce.map.output.compress=true</code>
  • 小文件先合并,避免过多 Map 任务
  • 数据倾斜时给 key 加随机前缀做两阶段聚合
6.4

案例解析——二次排序编程

二次排序 = 先按第一列排序,第一列相同时再按第二列排序。核心是把两个字段组合成一个可比较的 key。

6.4.3

编程实现(自定义 WritableComparable + 分区 + 分组)

JAVA
// 1) 组合 key:年+温度,实现比较规则
public class YearTempKey implements WritableComparable<YearTempKey> {
    private int year; private int temp;
    @Override public int compareTo(YearTempKey o) {
        int r = Integer.compare(year, o.year);      // 先按年
        return r != 0 ? r : Integer.compare(o.temp, this.temp);  // 同年按温度降序
    }
    // write/readFields 省略
}

// 2) 分区器:按年分区,保证同一年进入同一 Reduce
public class YearPartitioner extends Partitioner<YearTempKey, Text> {
    @Override public int getPartition(YearTempKey k, Text v, int num) {
        return Math.abs(k.getYear()) % num;
    }
}

// 3) 分组比较器:同一个年进入同一次 reduce 调用
job.setGroupingComparatorClass(YearGroupingComparator.class);
job.setPartitionerClass(YearPartitioner.class);

💡 二次排序的三个配件:组合 key(排序规则) + Partitioner(进哪个 Reduce) + GroupingComparator(谁归一组)。这一套也是 MapReduce 实现 TopN 的基础。

本章小结

本章从"分而治之 + 计算向数据靠拢"的思想出发,讲清 MapReduce 的工作流程(切片 → Map → Shuffle → Reduce),重点剖析 Shuffle 的溢写、分区、排序、合并与拉取归并,随后用单词统计和二次排序两个案例完成"设计思路 → 编程实现 → 运行 → 优化"的完整闭环。

思考与练习 先自己做,再点开答案

01

Map 任务的数量由什么决定?

查看参考答案

由输入分片(InputSplit)数量决定。分片默认与 HDFS block 大小一致(128MB),可通过 split.maxsize 等参数调整,一个分片对应一个 Map 任务。

02

为什么 Combiner 不能用于求平均值?

查看参考答案

Combiner 要求运算满足结合律与交换律。平均数不满足——先各自求平均再平均,结果与整体平均不同;它只能在 Map 端做"局部聚合",不能改变整体语义。

03

Reduce 个数设置得越多越好吗?

查看参考答案

不是。Reduce 太少会导致单任务数据量大、耗时长;太多则产生大量小文件、增加调度开销。经验公式:Reduce 数 ≈ 输入数据量 ÷ 每个 Reduce 的建议处理量(默认 256MB),一般控制在集群可用槽位的 0.95 倍以内。

04

二次排序需要哪三个关键组件?

查看参考答案

① 自定义 WritableComparable 组合 key(定义排序规则);② Partitioner(决定同组数据进哪个 Reduce);③ GroupingComparator(决定哪些 key 归入同一次 reduce 调用)。