type
status
date
slug
summary
tags
category
icon
password

一、MapReduce介绍
MapReduce 的核心是 “分而治之”,特别适合处理海量数据或复杂任务。
它并不是谷歌首创的思想,但谷歌将其工程化,实现了高效的分布式计算。
- Map(拆分处理):将大任务拆成若干可并行的小任务,每个任务独立处理,无需等待其他任务完成。
- Reduce(汇总整合):将所有小任务的结果收集并合并成最终输出。
形象比喻,假设要统计图书馆里所有的书:
你负责 1 号书架,我负责 2 号书架,更多人负责更多书架——这就是 Map。大家数完后,把各自的结果相加,得到全馆书的总数——这就是 Reduce。这种模式的效率关键在于:拆得足够均匀、合得足够快。
二、MapReduce设计构思
MapReduce是一个分布式运算程序的编程框架,核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在Hadoop集群上。
既然是做计算的框架,那么表现形式就是有个输入(input),MapReduce操作这个输入(input),通过本身定义好的计算模型,得到一个输出(output)。
对许多开发者来说,自己完完全全实现一个并行计算程序难度太大,而MapReduce就是一种简化并行计算的编程模型,降低了开发并行应用的入门门槛。
Hadoop MapReduce构思体现在如下的三个方面:
1.化繁为简,分而治之
对相互间不具有计算依赖关系的大数据,实现并行最自然的办法就是采取分而治之的策略。并行计算的第一个重要问题是如何划分计算任务或者计算数据以便对划分的子任务或数据块同时进行计算。不可分拆的计算任务或相互间有依赖关系的数据无法进行并行计算!
2.构建抽象模型:Map和Reduce
MapReduce借鉴了函数式语言中的思想,用Map和Reduce两个函数提供了高层的并行编程抽象模型。
Map: 对一组数据元素进行某种重复式的处理;
Reduce: 对Map的中间结果进行某种进一步的结果整理。
MapReduce中定义了如下的Map和Reduce两个抽象的编程接口,由用户去编程实现:
map: (k1; v1) → [(k2; v2)]
reduce: (k2; [v2]) → [(k3; v3)]
Map和Reduce为程序员提供了一个清晰的操作接口抽象描述。通过以上两个编程接口,大家可以看出MapReduce处理的数据类型是<key,value>键值对。
3.MapReduce框架结构
一个完整的mapreduce程序在分布式运行时有三类实例进程:
- MR AppMaster:负责整个程序的过程调度及状态协调;
- MapTask:负责map阶段的整个数据处理流程;
- ReduceTask:负责reduce阶段的整个数据处理流程。
三、MapReduce运行机制详解
1.MapTask工作机制
整个Map阶段流程大体如图所示:

简单概述:inputFile通过split被逻辑切分为多个split文件,通过Record按行读取内容给map(用户自己实现的)进行处理,数据被map处理结束之后交给OutputCollector收集器,对其结果key进行分区(默认使用hash分区),然后写入buffer,每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task来拉数据
详细步骤
1读取数据组件 InputFormat (默认 TextInputFormat) 会通过 getSplits 方法对输入目录中文件进行逻辑切片规划得到 block, 有多少个 block就对应启动多少个 MapTask
2将输入文件切分为 block 之后, 由 RecordReader 对象 (默认是LineRecordReader) 进行读取, 以 \n 作为分隔符, 读取一行数据, 返回 <key,value>. Key 表示每行首字符偏移值, Value 表示这一行文本内容
3读取 block 返回 <key,value>, 进入用户自己继承的 Mapper 类中,执行用户重写的 map 函数, RecordReader 读取一行这里调用一次
4Mapper 逻辑结束之后, 将 Mapper 的每条结果通过 context.write 进行collect数据收集. 在 collect 中, 会先对其进行分区处理,默认使用 HashPartitioner
MapReduce 提供 Partitioner 接口, 它的作用就是根据 Key 或 Value 及 Reducer 的数量来决定当前的这对输出数据最终应该交由哪个 Reduce task 处理, 默认对 Key Hash 后再以 Reducer 数量取模. 默认的取模方式只是为了平均 Reducer 的处理能力, 如果用户自己对 Partitioner 有需求, 可以订制并设置到 Job 上。
5接下来, 会将数据写入内存, 内存中这片区域叫做环形缓冲区, 缓冲区的作用是批量收集 Mapper 结果, 减少磁盘 IO 的影响. 我们的 Key/Value 对以及 Partition 的结果都会被写入缓冲区. 当然, 写入之前,Key 与 Value 值都会被序列化成字节数组
环形缓冲区其实是一个数组, 数组中存放着 Key, Value 的序列化数据和 Key, Value 的元数据信息, 包括 Partition, Key 的起始位置, Value 的起始位置以及 Value 的长度. 环形结构是一个抽象概念。
缓冲区是有大小限制, 默认是 100MB. 当 Mapper 的输出结果很多时, 就可能会撑爆内存, 所以需要在一定条件下将缓冲区中的数据临时写入磁盘, 然后重新利用这块缓冲区. 这个从内存往磁盘写数据的过程被称为 Spill, 中文可译为溢写. 这个溢写是由单独线程来完成, 不影响往缓冲区写 Mapper 结果的线程. 溢写线程启动时不应该阻止 Mapper 的结果输出, 所以整个缓冲区有个溢写的比例 spill.percent. 这个比例默认是 0.8, 也就是当缓冲区的数据已经达到阈值 buffer size * spill percent = 100MB * 0.8 = 80MB, 溢写线程启动, 锁定这 80MB 的内存, 执行溢写过程. Mapper 的输出结果还可以往剩下的 20MB 内存中写, 互不影响。
6当溢写线程启动后, 需要对这 80MB 空间内的 Key 做排序 (Sort). 排序是 MapReduce 模型默认的行为, 这里的排序也是对序列化的字节做的排序
如果 Job 设置过 Combiner, 那么现在就是使用 Combiner 的时候了. 将有相同 Key 的 Key/Value 对的 Value 合并在起来, 减少溢写到磁盘的数据量. Combiner 会优化 MapReduce 的中间结果, 所以它在整个模型中会多次使用 \ 那哪些场景才能使用 Combiner 呢? 从这里分析, Combiner 的输出是 Reducer 的输入, Combiner 绝不能改变最终的计算结果. Combiner 只应该用于那种 Reduce 的输入 Key/Value 与输出 Key/Value 类型完全一致, 且不影响最终结果的场景. 比如累加, 最大值等. Combiner 的使用一定得慎重, 如果用好, 它对 Job 执行效率有帮助, 反之会影响 Reducer 的最终结果
7合并溢写文件, 每次溢写会在磁盘上生成一个临时文件 (写之前判断是否有 Combiner), 如果 Mapper 的输出结果真的很大, 有多次这样的溢写发生, 磁盘上相应的就会有多个临时文件存在. 当整个数据处理结束之后开始对磁盘中的临时文件进行 Merge 合并, 因为最终的文件只有一个, 写入磁盘, 并且为这个文件提供了一个索引文件, 以记录每个reduce对应数据的偏移量。
配置 | 默认值 | 解释 |
mapreduce.task.io.sort.mb | 100 | 设置唤醒缓冲区的内存值的大小 |
mapreduce.map.sort.spill.percent | 0.8 | 设置溢写的比例 |
mapreduce.cluster.local.dir | ${hadoop.tmp.dir}/mapred/local | 溢写数据目录 |
mapreduce.task.io.sort.factor | 10 | 设置一次合并多少个溢写文件 |
2.ReduceTask工作机制

Reduce 大致分为 copy、sort、reduce 三个阶段,重点在前两个阶段。copy 阶段包含一个 eventFetcher 来获取已完成的 map 列表,由 Fetcher 线程去 copy 数据,在此过程中会启动两个 merge 线程,分别为 inMemoryMerger 和 onDiskMerger,分别将内存中的数据 merge 到磁盘和将磁盘中的数据进行 merge。待数据 copy 完成之后,copy 阶段就完成了,开始进行 sort 阶段,sort 阶段主要是执行 finalMerge 操作,纯粹的 sort 阶段,完成之后就是 reduce 阶段,调用用户定义的 reduce 函数进行处理
详细步骤
- Copy阶段,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过HTTP方式请求maptask获取属于自己的文件。
- Merge阶段。这里的merge如map端的merge动作,只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端的更为灵活。merge有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map 端类似,这也是溢写的过程,这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的文件。
- 合并排序。把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
- 对排序后的键值对调用reduce方法,键相等的键值对调用一次reduce方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS文件中。
3.Shuffle过程
map 阶段处理的数据如何传递给 reduce 阶段,是 MapReduce 框架中最关键的一个流程,这个流程就叫 shuffle。
shuffle: 洗牌、发牌 ——(核心机制:数据分区,排序,分组,规约,合并等过程)

shuffle 是 Mapreduce 的核心,它分布在 Mapreduce 的 map 阶段和 reduce 阶段。一般把从 Map 产生输出开始到 Reduce 取得数据作为输入之前的过程称作 shuffle。
- Collect阶段:将 MapTask 的结果输出到默认大小为 100M 的环形缓冲区,保存的是 key/value,Partition 分区信息等。
- Spill阶段:当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘,在将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了 combiner,还会将有相同分区号和 key 的数据进行排序。
- Merge阶段:把所有溢出的临时文件进行一次合并操作,以确保一个 MapTask 最终只产生一个中间数据文件。
- Copy阶段:ReduceTask 启动 Fetcher 线程到已经完成 MapTask 的节点上复制一份属于自己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定的阀值的时候,就会将数据写到磁盘之上。
- Merge阶段:在 ReduceTask 远程复制数据的同时,会在后台开启两个线程对内存到本地的数据文件进行合并操作。
- Sort阶段:在对数据进行合并的同时,会进行排序操作,由于 MapTask 阶段已经对数据进行了局部的排序,ReduceTask 只需保证 Copy 的数据的最终整体有效性即可。
Shuffle 中的缓冲区大小会影响到 mapreduce 程序的执行效率,原则上说,缓冲区越大,磁盘io的次数越少,执行速度就越快。缓冲区大小可以通过参数mapreduce.task.io.sort.mb调整,默认100M
4.MapReduce 开发8步骤
MapReduce 的开发一共有八个步骤, 其中 Map 阶段分为 2 个步骤,Shuffle 阶段 4 个步骤,Reduce 阶段分为 2 个步骤
- Map 阶段 2 个步骤:
(1)设置 InputFormat 类, 将数据切分为 Key-Value (K1和V1) 对, 输入到第二步
(2)自定义 Map 逻辑, 将第一步的结果转换成另外的 Key-Value(K2和V2) 对, 输出结果
- Shuffle 阶段 4 个步骤:
(1)对输出的 Key-Value 对进行分区
(2)对不同分区的数据按照相同的 Key 排序
(3)(可选) 对分组过的数据初步规约, 降低数据的网络拷贝
(4)对数据进行分组, 相同 Key 的 Value 放入一个集合中
3.Reduce 阶段 2 个步骤:
(1)对多个 Map 任务的结果进行排序以及合并, 编写 Reduce 函数实现自己的逻辑, 对输入的 Key-Value 进行处理, 转为新的 Key-Value(K3和V3)输出
(2)设置 OutputFormat 处理并保存 Reduce 输出的 Key-Value 数据
5.Reduce侧JOIN
Map:读两张表,按 join key 输出 <key, (tag, row)>;
Shuffle:同 key 汇到一个 Reducer;
Reduce:在 reducer 内按 tag 连接两边。
✔ 优点:无需预排序/对齐;对内存要求低(可边读边写、溢写到磁盘)。
✖ 缺点:全量 shuffle,最慢。
在内存受限、离线批处理场景下,经典 MapReduce 能把 SQL(尤其是 JOIN/聚合)跑出来,但 I/O 重、时延高。若追求速度/易用,优先 Hive(MR/Tez/Spark 引擎)、Spark;若机器内存真吃紧、只求稳妥和可扩展,MR 依然靠谱。
6.Map侧JOIN
方案 A(Broadcast/DistributedCache):小表放分布式缓存,Mapper 内内存哈希表查大表。
方案 B(Partitioned Merge Join):两表都按相同分区与 key 排序,用 CompositeInputFormat 在 Map 端顺序归并。
✔ 优点:几乎无 shuffle,速度快。
✖ 前提:小表能放内存,或两表已按相同规则分区+排序。
适用于关联表中有小表的情形.使用分布式缓存,可以将小表分发到所有的map节点,这样,map节点就可以在本地对自己所读到的大表数据进行join并输出最终结果,可以大大提高join操作的并发度,加快处理速度
注意:
- 能 Map-Side 就绝不 Reduce-Side;
- 小表能放内存 → 广播哈希 Join;
- 两大表且可预处理 → 分区+排序→Map 端归并 Join;
- 两大表、无前置条件 → Reduce-Side Join,配合 加盐/自定义分区/倾斜兜底。
7.Semi-Join / Bloom Filter
先用小表做布隆过滤,再过滤大表
场景:小表 S 只有 join key(或可从 S 抽取),大表 L 很大。
做法:
1.从小表 S 生成 BloomFilter 并写到 HDFS。
2.把 BloomFilter 作为分发文件(DistributedCache)加载到 Mapper,在 Map 端先过滤大表 L,只让“可能命中”的记录进入后续(可直接落盘,或再走 Reduce-Side Join)。
依赖:org.apache.hadoop:hadoop-common 与 org.apache.hadoop:hadoop-mapreduce-client-core(Hadoop 自带 bloom 类:org.apache.hadoop.util.bloom.*)
Job-B 输出的是“已被小表 key 过滤后的大表子集”。你可以直接把它作为后续 Reduce-Side Join 的输入,极大减少 shuffle 量。Bloom 可能有少量误判(哈希碰撞,把不存在当存在),不会漏;后续 join 会自然剔除误判。
8. 预聚合:框架侧和代码侧
1.Combiner 示例(框架层)
场景:对可结合的聚合(sum/count/min/max 等)在 Map 端先局部合并,减少网络传输。
下面是“按 key 求和”的模板(同 WordCount,换一下 value 即可):
注意:Combiner 可能不会被调用(由框架决定),也可能被多次调用;必须保证函数是可结合且交换律成立(sum/count/min/max 一般没问题,avg 直接用不行,需要把 (sum, cnt) 作为 value 再求平均)。
2.In-Mapper Combining(手动在 Mapper 内做局部聚合)
当 key 极多、小 value 成本高、或者你需要强制在 Map 端聚合时,可在 Mapper 中用 HashMap 聚合,最后一次性写出:
1.框架侧预聚合(Combiner)
是MapReduce框架本身提供的一个特性,你只需要实现一个Reducer类,并把它在job.setCombinerClass(...)中注册即可。框架会在Map输出到Shuffle阶段之间,自动调用Combiner来做局部聚合,减少网络传输量。不用关心Combiner的触发时机、调用次数(它可能不调用、可能多次调用),MapReduce会自己调度。优点是省事,缺点是灵活性受限,而且只能用于满足结合律、交换律的运算。
2.代码侧预聚合(In-MapperCombining)
是开发者在Mapper内部完全自定义逻辑,通过一个局部数据结构(比如HashMap)先行聚合数据。最后在cleanup()方法中一次性写出,或者分批flush到下游。这是纯手工实现,MapReduce框架对此没有任何自动支持。优点是灵活,可以精确控制聚合策略、内存使用时机,也能做复杂聚合逻辑;缺点是需要自己管理内存和实现细节。
3.交换律(Commutativity)
运算顺序交换,结果不变,因为Combiner不保证Map输出的key/value对是按固定顺序处理的,如果运算不满足交换律,结果可能会错。比如取最新记录的场景,要统计每个用户最后一次登录时间。如果把“2025-08-01”和“2025-07-15”调换位置,有可能先保留旧的,再把新的覆盖掉,结果就错了
4.结合律(Associativity)
运算分组方式不同,结果不变。Combiner可能会多次、分批执行聚合,如果运算不满足结合律,那么“先局部聚合,再全局聚合”可能会改变最终结果,比如计算增长率场景中先按季度算增长率再平均,和直接按全年算增长率,结果可能差很多。
9.案例:两两好友判定
10.倒排索引创建MR实现
倒排索引(Inverted Index)是搜索引擎的核心数据结构之一。
- 正排索引:文档 → 文档内容,比如:
• 倒排索引:单词 → 出现在哪些文档以及出现次数,比如:
11.升级版wordcount
统计单词频率并按出现次数倒序输出。
12.分组topN
分组 Top N 在实际业务里非常常见,尤其是在按某个分组维度取前 N 个指标值最高的记录的场景:
- 作者:tacjin
- 链接:http://jin.wiki/article/24de55fd-4dcc-80b2-9c3e-c093da82c72c
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。