MapReduce框架原理深度解析 | Shuffle机制、切片分区、Join全攻略

前言

作为大数据开发者,深入理解MapReduce的框架原理至关重要。本文将从InputFormat数据输入Shuffle机制Partition分区Combiner合并Join应用数据压缩六大核心模块,结合源码与架构图,带你彻底搞懂MapReduce的底层设计。


一、MapReduce核心架构回顾

MapReduce程序运行时有三类实例进程:

进程 职责
MrAppMaster 负责整个程序的过程调度及状态协调(ApplicationMaster的子类)
MapTask 负责Map阶段的整个数据处理流程
ReduceTask 负责Reduce阶段的整个数据处理流程

注意:在查看进程时,MapTask和ReduceTask显示为yarnchild,是YARN的子进程。


二、InputFormat数据输入与切片机制

2.1 数据块 vs 数据切片

概念 说明 特点
数据块(Block) HDFS物理上把数据分成一块一块 物理切割,默认128M,存储在不同DataNode
数据切片(Split) 逻辑上对输入进行分片 逻辑切割,不实际切分文件,决定MapTask数量

关键原则:

  • 一个Job的Map阶段并行度由切片数决定
  • 每一个Split切片分配一个MapTask并行实例处理
  • 默认情况下,切片大小 = BlockSize(128M)
  • 切片时不考虑数据集整体 ,而是逐个针对每一个文件单独切片

2.2 切片源码解析

java 复制代码
// FileInputFormat.getSplits() 核心逻辑
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
// 其中 computeSplitSize = Math.max(minSize, Math.min(maxSize, blockSize))

参数优先级:

复制代码
minSize(1) < splitSize < maxSize(Long.MAX_VALUE)

默认配置下:splitSize = blockSize = 128M

2.3 切片大小设置

xml 复制代码
<!-- mapred-site.xml -->
<property>
    <name>mapreduce.input.fileinputformat.split.minsize</name>
    <value>1</value>
</property>
<property>
    <name>mapreduce.input.fileinputformat.split.maxsize</name>
    <value>Long.MAX_VALUE</value>
</property>

生产环境中,若磁盘传输速度快(如SSD),可将BlockSize和切片大小调整为256M。

2.4 FileInputFormat实现类

实现类 输入K,V类型 特点
TextInputFormat LongWritable, Text 默认实现,按行读取,K为偏移量
KeyValueTextInputFormat Text, Text 按分隔符切割,第0位为K,第1位为V
NLineInputFormat LongWritable, Text 一次读取N行,效率更高
CombineTextInputFormat LongWritable, Text 解决小文件问题,合并多个文件为一个切片

2.5 CombineTextInputFormat解决小文件问题

问题场景:大量1KB小文件,每个文件开启一个MapTask(占用1G内存+1个CPU),资源严重浪费。

解决方案:将多个小文件合并成一个切片处理。

java 复制代码
// 在Driver中设置
job.setInputFormatClass(CombineTextInputFormat.class);
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304); // 4MB

虚拟存储与切片过程:

示例:4个小文件(1.7M、5.1M、3.4M、6.8M),设置maxSize=4M

复制代码
虚拟存储后:1.7M, (2.55M, 2.55M), 3.4M, (3.4M, 3.4M)
最终切片:(1.7+2.55)M, (2.55+3.4)M, (3.4+3.4)M  →  3个切片,3个MapTask

三、MapReduce完整工作流程

3.1 工作流程详解

阶段 操作
① 切片分析 客户端对原始文件进行切片,200M文件分成2片(0-128M, 128-200M)
② 提交三样东西 Jar包、切片规划文件、job.xml(配置参数)
③ 启动MrAppMaster YARN启动MrAppMaster,读取切片信息,决定MapTask数量
④ MapTask处理 用InputFormat读取数据(默认TextInputFormat→LineRecordReader),K为偏移量,V为行内容
⑤ Mapper业务逻辑 用户自定义map()方法处理数据
⑥ 输出到环形缓冲区 数据进入环形缓冲区,左侧存索引,右侧存数据
⑦ Shuffle阶段 分区→排序→溢写→合并→压缩→Reduce拉取→归并排序→分组
⑧ Reduce处理 用户自定义reduce()方法聚合数据
⑨ OutputFormat输出 默认TextOutputFormat,按行写入HDFS

四、Shuffle机制(面试重点)

Shuffle是MapReduce的核心与灵魂,是Map方法之后、Reduce方法之前的数据处理过程。

4.1 环形缓冲区(Map端)

环形缓冲区结构:

  • 左侧:存储索引(Metadata)------ 分区号、keystart、valstart、vallen
  • 右侧:存储真实数据(Record)------ K,V序列化后的字节
  • 默认大小:100M
  • 溢写阈值:80%(反向溢写,预留20%给溢写线程)

为什么80%就溢写?如果等到100%再溢写,必须等所有数据写完磁盘才能继续写入缓冲区,效率低。80%时开启溢写线程,边写边处理。

4.2 Map端Shuffle详细流程

复制代码
Map输出 → 分区(Partition) → 进入环形缓冲区 → 达到80% → 快排(按K索引排序) 
→ 溢写到磁盘(spill.out + spill.index) → 多次溢写文件 → 归并排序(Merge) 
→ Combiner(可选) → 压缩(可选) → 等待Reduce拉取

关键细节:

  • 排序不是移动数据 :快排是对索引排序,不是移动真实的K,V数据
  • 分区内部有序:溢写前按分区号排序,同一分区内按Key字典序排序
  • 溢写文件结构:一个文件包含多个分区数据,通过index文件记录每个分区的偏移量

4.3 Reduce端Shuffle详细流程

复制代码
ReduceTask启动 → 从各MapTask拉取指定分区数据 → 内存缓冲/磁盘溢写 
→ 归并排序所有拉取的数据 → 按Key分组 → 相同Key进入同一个Reduce方法

Reduce拉取特点:

  • 主动拉取:ReduceTask主动从MapTask拉取数据,不是MapTask推送
  • 内存+磁盘:拉取的数据先放内存,不够则溢写到磁盘
  • 全局排序:对所有拉取的数据进行一次归并排序,保证相同Key连续

4.4 Shuffle参数调优

xml 复制代码
<!-- 环形缓冲区大小,默认100M -->
<property>
    <name>mapreduce.task.io.sort.mb</name>
    <value>100</value>
</property>

<!-- 溢写阈值百分比,默认0.8 -->
<property>
    <name>mapreduce.map.sort.spill.percent</name>
    <value>0.80</value>
</property>

<!-- 归并时一次合并的文件数,默认10 -->
<property>
    <name>mapreduce.task.io.sort.factor</name>
    <value>10</value>
</property>

五、Partition分区机制

5.1 默认分区器 HashPartitioner

java 复制代码
public class HashPartitioner<K, V> extends Partitioner<K, V> {
    public int getPartition(K key, V value, int numReduceTasks) {
        return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
    }
}

原理key.hashCode() & Integer.MAX_VALUE 防止负数,再对ReduceTask个数取余。

5.2 自定义分区案例

需求:将手机号按归属地输出到不同文件(136、137、138、139、其他)

java 复制代码
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
    @Override
    public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
        String phone = text.toString();
        String prePhone = phone.substring(0, 3);
        int partition;
        if("136".equals(prePhone)) partition = 0;
        else if("137".equals(prePhone)) partition = 1;
        else if("138".equals(prePhone)) partition = 2;
        else if("139".equals(prePhone)) partition = 3;
        else partition = 4;
        return partition;
    }
}

Driver设置:

java 复制代码
job.setPartitionerClass(ProvincePartitioner.class);
job.setNumReduceTasks(5);  // 必须等于分区数!

5.3 分区数与ReduceTask数的关系

ReduceTask数 结果
= 分区数 正常执行,每个分区一个输出文件
< 分区数 IOException,数据无处写入
> 分区数 正常执行,多余ReduceTask输出空文件
= 1 不执行分区过程,所有数据进入0号分区,输出一个文件

源码验证:MapTask中执行分区的前提是 numReduceTasks > 1


六、WritableComparable排序机制

6.1 排序发生的时机

MapReduce中共发生3次排序

阶段 排序类型 算法 说明
Map端 快速排序 QuickSort 环形缓冲区溢写前,对索引排序
Map端 归并排序 MergeSort 多个溢写文件合并时
Reduce端 归并排序 MergeSort 拉取所有Map数据后,全局排序

6.2 为什么必须排序?

核心原因:让相同Key的数据连续排列,Reduce只需顺序扫描即可判断Key是否相同,无需全量遍历。

复制代码
未排序:(a,1) (b,1) (a,1) (d,1)  →  需要遍历全部判断相同Key
已排序:(a,1) (a,1) (b,1) (d,1)  →  顺序扫描,遇到不同Key即停止

6.3 自定义排序实现

需求:按总流量倒序排序

java 复制代码
public class FlowBean implements WritableComparable<FlowBean> {
    private long upFlow;
    private long downFlow;
    private long sumFlow;
    
    // 实现compareTo方法
    @Override
    public int compareTo(FlowBean o) {
        // 总流量倒序
        if(this.sumFlow > o.sumFlow) return -1;
        else if(this.sumFlow < o.sumFlow) return 1;
        else {
            // 二次排序:总流量相同,按上行流量正序
            if(this.upFlow > o.upFlow) return 1;
            else if(this.upFlow < o.upFlow) return -1;
            else return 0;
        }
    }
}

6.4 排序类型总结

排序类型 说明
部分排序 对每个输出文件内部排序(默认行为)
全排序 所有数据全局排序,通常只有1个ReduceTask
二次排序 排序条件有两个,第一个条件相同按第二个排
区内排序 每个分区内部独立排序

七、Combiner合并机制

7.1 Combiner的作用

场景 :Map端输出(a,1)出现1万次,不开启Combiner需传输1万条到Reduce;开启后传输(a,10000)只需1条。

核心价值:减少Map到Reduce的网络传输量,提前局部聚合。

7.2 Combiner使用前提

必须满足:Combiner的逻辑不影响最终结果。

场景 是否可用 原因
求和 可用 (a,1)+(a,1)=(a,2),结果正确
求平均值 不可用 (1+2)/2=1.5(1+2+3+4)/4=2.5
求最大值 可用 max(max(a,b),c) = max(a,b,c)

7.3 Combiner代码实现

java 复制代码
// 方式一:自定义Combiner类
public class WordCountCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
    private IntWritable outV = new IntWritable();
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) 
            throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable value : values) {
            sum += value.get();
        }
        outV.set(sum);
        context.write(key, outV);
    }
}

// Driver中设置
job.setCombinerClass(WordCountCombiner.class);

// 方式二:若Combiner逻辑与Reducer完全相同,直接用Reducer
job.setCombinerClass(WordCountReducer.class);

注意:若setNumReduceTasks(0),则不执行Shuffle阶段,Combiner也不生效。


八、OutputFormat数据输出

8.1 默认输出 TextOutputFormat

将每个K,V对按行输出到文本文件。

8.2 自定义OutputFormat案例

需求 :将包含atguigu的日志输出到atguigu.log,其他输出到other.log

java 复制代码
// 1. 自定义OutputFormat
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
    @Override
    public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext job) 
            throws IOException, InterruptedException {
        return new LogRecordWriter(job);
    }
}

// 2. 自定义RecordWriter
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
    private FSDataOutputStream atguiguOut;
    private FSDataOutputStream otherOut;
    
    public LogRecordWriter(TaskAttemptContext job) {
        FileSystem fs = FileSystem.get(job.getConfiguration());
        atguiguOut = fs.create(new Path("d:/hadoop/atguigu.log"));
        otherOut = fs.create(new Path("d:/hadoop/other.log"));
    }
    
    @Override
    public void write(Text key, NullWritable value) throws IOException {
        String log = key.toString();
        if (log.contains("atguigu")) {
            atguiguOut.writeBytes(log + "\n");
        } else {
            otherOut.writeBytes(log + "\n");
        }
    }
    
    @Override
    public void close(TaskAttemptContext context) throws IOException {
        IOUtils.closeStream(atguiguOut);
        IOUtils.closeStream(otherOut);  // 必须关流!否则文件为空
    }
}

// 3. Driver设置
job.setOutputFormatClass(LogOutputFormat.class);

九、MapTask与ReduceTask工作机制源码解析

9.1 MapTask五个阶段

阶段 操作
Read 通过RecordReader从InputSplit解析K,V
Map 调用用户自定义map()函数处理
Collect 调用OutputCollector.collect(),分区后写入环形缓冲区
Spill 缓冲区满80%后溢写到磁盘,先快排,再按分区写入
Merge 所有溢写文件归并成一个最终文件,每轮合并10个文件

9.2 ReduceTask三个阶段

阶段 操作
Copy 从各MapTask拉取指定分区数据,内存不够则溢写磁盘
Sort 对内存和磁盘上的文件进行归并排序
Reduce 调用用户自定义reduce()函数,结果写入HDFS

9.3 ReduceTask并行度决定

java 复制代码
// 手动设置ReduceTask数量
job.setNumReduceTasks(4);

实验结论(1GB数据,16个MapTask):

ReduceTask 1 5 10 15 16 20 25 30 45 60
总时间(s) 892 146 110 92 88 100 128 101 145 104

最佳实践:ReduceTask数量并非越多越好,需根据数据量和集群资源调优。一般设置为节点数的0.95倍或1.75倍。


十、Join应用

10.1 Reduce Join

原理:Map端为数据打标签区分来源,以连接字段为Key输出;Reduce端按Key分组,合并不同来源的数据。

缺点:合并操作在Reduce端完成,Reduce端压力大,易产生数据倾斜。

java 复制代码
// Mapper中通过setup获取文件名,打标签
@Override
protected void setup(Context context) throws IOException {
    InputSplit split = context.getInputSplit();
    FileSplit fileSplit = (FileSplit) split;
    filename = fileSplit.getPath().getName();
}

@Override
protected void map(LongWritable key, Text value, Context context) {
    if(filename.contains("order")) {
        // 订单表处理,flag="order"
    } else {
        // 商品表处理,flag="pd"
    }
}

10.2 Map Join(推荐)

适用场景:一张表很小(可放入内存),一张表很大。

优势 :在Map端缓存小表,提前处理Join逻辑,无需Reduce阶段,避免数据倾斜。

java 复制代码
// Driver中加载缓存文件
job.addCacheFile(new URI("file:///D:/input/tablecache/pd.txt"));
job.setNumReduceTasks(0);  // 不需要Reduce

// Mapper中setup读取缓存
@Override
protected void setup(Context context) throws IOException {
    URI[] cacheFiles = context.getCacheFiles();
    Path path = new Path(cacheFiles[0]);
    FileSystem fs = FileSystem.get(context.getConfiguration());
    FSDataInputStream fis = fs.open(path);
    BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
    String line;
    while (StringUtils.isNotEmpty(line = reader.readLine())) {
        String[] split = line.split("\t");
        pdMap.put(split[0], split[1]);  // pid -> pname
    }
}

@Override
protected void map(LongWritable key, Text value, Context context) {
    // 直接通过pdMap获取pname,无需Reduce
    String pname = pdMap.get(fields[1]);
}

十一、Hadoop数据压缩

11.1 压缩算法对比

压缩格式 自带 算法 可切片 压缩率 速度 适用场景
Gzip DEFLATE 一般 归档
Bzip2 bzip2 最高 冷数据
LZO LZO 一般 需建索引
Snappy Snappy 一般 最快 热数据、实时

11.2 压缩位置选择

复制代码
输入文件 → [InputFormat] → Map输出 → [Shuffle] → Reduce输出 → [OutputFormat] → HDFS
              ↑压缩可选      ↑强烈建议压缩      ↑压缩可选

11.3 压缩参数配置

java 复制代码
// Map输出端压缩
conf.setBoolean("mapreduce.map.output.compress", true);
conf.setClass("mapreduce.map.output.compress.codec", 
              BZip2Codec.class, CompressionCodec.class);

// Reduce输出端压缩
FileOutputFormat.setCompressOutput(job, true);
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);

十二、MapReduce开发总结

组件 核心要点
InputFormat 默认TextInputFormat;CombineTextInputFormat解决小文件
Mapper 实现map()、setup()、cleanup()三个方法
Partitioner 默认HashPartitioner;自定义需配合setNumReduceTasks
Comparable Key必须实现WritableComparable,重写compareTo()
Combiner 提前聚合减少IO,但需保证不影响最终结果
Reducer 实现reduce()、setup()、cleanup()三个方法
OutputFormat 默认TextOutputFormat;可自定义输出到数据库等

十三、常见错误及解决方案

错误 原因 解决方案
Illegal partition for xxx Partition和ReduceTask个数不匹配 调整ReduceTask个数等于分区数
类型转换异常 Map输入参数类型错误 Mapper第一个输入必须是LongWritable或NullWritable
输出文件夹已存在 MapReduce不允许覆盖 删除旧目录或更换输出路径
Unsupported major.minor version 52.0 JDK版本不一致 统一Windows和Linux的JDK版本
找不到缓存文件 路径错误或文件名多了.txt 检查路径,使用绝对路径
自定义OutputFormat输出为空 RecordWriter未关闭流 在close()方法中关闭所有输出流
ReduceTask=0时Combiner不生效 无Shuffle阶段 正常现象,Combiner依赖Shuffle
相关推荐
赵渝强老师1 小时前
【赵渝强老师】Hadoop的伪分布部署模式
大数据·hadoop·分布式
沐泽__1 小时前
Elasticsearch 磁盘水位线详解:从触发只读到安全解锁
大数据·安全·elasticsearch
2601_949936961 小时前
2026年中国大数据领域专业认证体系分类、特征及职业适配研究
大数据
淡定一生23331 小时前
spark 3.3+ 之BloomFilter Runtime Filter
大数据·分布式·spark
Sharewinfo_BJ1 小时前
上北智信携“智信BI”闪耀2026上海全球数据周,以灵活部署方案赋能企业数据价值跃升
大数据·人工智能·ai·数据挖掘·微软·powerbi
刀法如飞1 小时前
Ontology本体论是什么?Palantir 技术原理介绍
大数据·人工智能·架构
企微增长观察1 小时前
企业微信AI服务商怎么选?AI Agent与SCRM联动是关键
大数据·人工智能·企业微信
Bechamz1 小时前
大数据开发学习Day30
大数据·学习
精益数智小屋1 小时前
物料管理系统软件有什么用?物料管理系统软件功能详解
大数据·数据库·人工智能·自动化·精益工程