MapReduce
的思想核心是“分而治之”,适用于大规模数据处理场景。Map
负责“分”,即把复杂的任务分解为若干个“简单的任务”来并行处理。可以进行拆分的前提是这些小任务可以并行计算,彼此间几乎没有依赖关系;Reduce
负责“合”,即对map阶段的结果进行全局汇总
Map
和Reduce
为程序员提供了一个清晰的操作接口抽象描述,MapReduce
中定义了如下的Map
和Reduce
两个抽象的编程接口,由用户去编程实现.Map
和Reduce
,MapReduce
处理的数据类型是<key,value>
键值对
Map
: (k1; v1)
→ [(k2; v2)]
Reduce
: (k2; [v2])
→ [(k3; v3)]
一个完整的MapReduce
程序在分布式运行时有三类实例进程:
MRAppMaster
负责整个程序的过程调度及状态协调MapTask
负责Map
阶段的整个数据处理流程ReduceTask
负责Reduce
阶段的整个数据处理流程
Map 阶段 2 个步骤
- 设置
InputFormat
类, 将数据切分为Key-Value(K1和V1)
对, 输入到第二步 - 自定义
Map
逻辑, 将第一步的结果转换成另外的Key-Value(K2和V2)
对, 输出结果
Shuffle阶段四个步骤
- 对输出的
Key-Value
对进行分区 - 对不同分区的数据按照相同的
Key
排序 - (可选) 对分组过的数据初步规约, 降低数据的网络拷贝
- 对数据进行分组, 相同
Key
的Value
放入一个集合中
Reduce 阶段 2 个步骤
- 对多个
Map
任务的结果进行排序以及合并, 编写Reduce
函数实现自己的逻辑, 对输入的
Key-Value
进行处理, 转为新的Key-Value(K3和V3)
输出 - 设置
OutputFormat
处理并保存Reduce
输出的Key-Value
数据
WordCount实操案例
数据格式准备
cd /export/servers
vim wordcount.txt
#向其中放入以下内容并保存
hello,world,hadoop
hive,sqoop,flume,hello
kitty,tom,jerry,world
hadoop
#上传到 HDFS
hdfs dfs -mkdir /wordcount/
hdfs dfs -put wordcount.txt /wordcount/
Mapper
public class WordCountMapper extends Mapper<LongWritable,Text,Text,LongWritable> {
@Override
public void map(LongWritable key, Text value, Context context) throws
IOException, InterruptedException {
String line = value.toString();
String[] split = line.split(",");
for (String word : split) {
context.write(new Text(word),new LongWritable(1));
}
}
}
Reducer
public class WordCountReducer extends
Reducer<Text,LongWritable,Text,LongWritable> {
/**
* 自定义我们的reduce逻辑
* 所有的key都是我们的单词,所有的values都是我们单词出现的次数
* @param key
* @param values
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
long count = 0;
for (LongWritable value : values) {
count += value.get();
}
context.write(key,new LongWritable(count));
}
}
二次排序
默认情况下,在MapReduce
中的shuffer
阶段会自动进行排序,而且是根据key
进行排序的。但是有时候需要对key
排序的同时再对value
进行排序,这时候就要用到二次排序了。
MapReduce过程详解
MapReduce 排序和序列化
序列化 (Serialization) 是指把结构化对象转化为字节流,反序列化 (Deserialization) 是序列化的逆过程. 把字节流转为结构化对象. 当要在进程间传递对象或持久化对象的时候, 就需要序列化对象成字节流, 反之当要将接收到或从磁盘读取的字节流转换为对象, 就要进行反序列化。Writable
是Hadoop
的序列化格式, Hadoop
定义了这样一个Writable
接口. 一个类要支持可序列化只需实现这个接口即可。
Map过程概述
整个Map阶段流程简单概述:inputFile
通过split
被逻辑切分为多个split
文件,通过Record
按行读取内容给map
(用户自己实现的)进行处理,数据被map
处理结束之后交给OutputCollector
收集器,对其结果key
进行分区(默认使用hash分区),然后写入缓冲区每个maptask
都有一个内存缓冲区,存储着map
的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task
结束后再对磁盘中这个map task
产生的所有临时文件做合并,生成最终的正式输出文件,然后等待reduce task
来拉数据。
Mapper
- 读取数据组件
InputFormat
会通过getSplits
方法对输入目录中文件进行逻辑切片规划得到block
, 有多少个block
就对应启动多少个MapTask
- 由
RecordReader
对象进行读取, 以\n
作为分隔符, 读取一行数据, 返回<key,value>
,Key
表示每行首字符偏移值,Value
表示这一行文本内容 - 读取
block
返回<key,value>
, 进入用户自己继承的Mapper
类中,执行用户重写的map
函数,RecordReader
读取一行这里调用一次
分区Partition
Mapper
逻辑结束之后, 将Mapper
的每条结果通过context.write
进行collect
数据收集。 在collect
中, 会先对其进行分区处理,默认使用HashPartitioner
。当map
函数通过context.write()
开始输出数据时,不是单纯地将数据写入到磁盘。为了性能,map
输出的数据会写入到缓冲区,并进行预排序的一些工作
MapReduce
提供Partitioner
接口, 它的作用就是根据Key
或Value
及Reducer
的数量来决定当前的这对输出数据最终应该交由哪个Reducetask
处理, 默认对Key
Hash后再以Reducer
数量取模. 默认的取模方式只是为了平均Reducer
的处理能力, 如果用户自己对Partitioner
有需求, 可以订制并设置到Job上
环形Buffer数据结构
每一个map
任务有一个环形Buffer,map
将输出写入到这个Buffer(之所以MapReduce选择环形缓冲区主要是为了能够并发读写)。环形Buffer是内存中的一种首尾相连的数据结构(环形队列是在实际编程极为有用的数据结构,它是一个首尾相连的FIFO的数据结构,采用数组的线性空间,数据组织简单。能很快知道队列是否满为空。能以很快速度的来存取数据。 因为有简单高效的原因,甚至在硬件都实现了环形队列),专门用来存储Key-Value
格式的数据,Hadoop
中,环形缓冲其实就是一个字节数组,kvbuffer
包含数据区和索引区,数据区是存放数据的,索引区是为了你存放进入的数据还能原样的拿出来,保证数据不出错!这两个区是相邻不重叠的区域,用一个分界点来标识。分界点不是永恒不变的,每次Spill
之后都会更新一次。初始分界点为0,数据存储方向为向上增长,索引存储方向向下.。缓冲区的大小默认为100M。
环形缓冲区其实是一个数组,数组中存放着
key
、value
的序列化数据和key
、value
的元数据信息,key/value
的元数据存储的格式是int
类型,每个key/value
对应一个元数据,元数据由4个int
组成,第一个int
存放value
的起始位置,第二个存放key
的起始位置,第三个存放partition
,最后一个存放value
的长度。key/value
序列化的数据和元数据在环形缓冲区中的存储是由equator
分隔的,key/value
按照索引递增的方向存储,meta
则按照索引递减的方向存储,将其数组抽象为一个环形结构之后,以equator
为界,key/value
顺时针存储,meta
逆时针存储。
Spill溢写
当Mapper
的输出结果很多时, 就可能会撑爆内存, 所以需要在一定条件下将缓冲区中的数据临时写入磁盘, 然后重新利用这块缓冲区. 这个从内存往磁盘写数据的过程被称为Spill
, 中文可译为溢写. 这个溢写是由单独线程来完成, 不影响往缓冲区写Mapper
结果的线程. 溢写线程启动时不应该阻止Mapper
的结果输出, 所以整个缓冲区有个溢写的比例。 这个比例默认是 0.8, 也就是当缓冲区的数据已经达到阈值溢写线程启动,锁定这80MB的内存, 执行溢写过程。Mapper
的输出结果还可以往剩下的20MB内存中写, 互不影响(需要注意的是,在写入文件之前,MapTask会首先对数据进行一次排序,如果用户指定了压缩和combiner函数,那么还会对数据进行压缩和合并)
规约Combiner
每一个Map
都可能会产生大量的本地输出,Combiner
的作用就是对Map
端的输出先做一次合并,以减少在Map
和Reduce
节点之间的数据传输量,以提高网络IO 性能,减少写入磁盘的IO数,是MapReduce
的一种优化手段之一。Combiner
组件的父类就是Reducer
,Combiner
和Reducer
的区别在于运行的位置,Combiner
是在每一个maptask
所在的节点运行,而Reducer
是接收全局所有Map
的输出结果。Combiner
的意义就是对每一个maptask
的输出进行局部汇总,以减小网络传输量。
Combiner需要严谨使用
因为Combiner
在MapReduce
过程中可能调用也肯能不调用,可能调一次也可能调多次,无法确定和控制所以,Combiner
使用的原则是:有或没有都不能影响业务逻辑,使不使用Combiner
都不能影响最终Reducer
的结果。而且,Combiner
的输出kv
应该跟Reducer
的输入kv
类型要对应起来。因为有时使用Combiner
不当的话会对统计结果造成错误的结局,还不如不用。比如对所有数求平均数:
Mapper
端使用Combiner
3、5、7 ->(3 + 5 + 7) / 3 = 5
和 2、6 ->(2 + 6) / 2 = 4
Reducer
(5 + 4) / 2= 9 / 2
不等于 (3 + 5 + 7 + 2 + 6) / 5 = 23 / 5
Combiner既然可以汇总,能不能用Combiner取代Reduce函数?
虽然Combiner
可以帮我们减少Mapper
和Reducer
之间的数据传输量,对Mapper
到Reducer
的数据进行局部汇总,减轻Reducer
的工作量和减少网络IO,但是我们仍然需要用Reduce
函数来处理不同Map
输出的具有相同键的记录。
合并溢写文件
每次溢写会在磁盘上生成一个临时文件 (写之前判断是否有Combiner
), 如果Mapper
的输出结果真的很大, 有多次这样的溢写发生, 磁盘上相应的就会有多个临时文件存在. 当整个数据处理结束之后开始对磁盘中的临时文件进行Merge
合并, 因为最终的文件只有一个, 写入磁盘, 并且为这个文件提供了一个索引文件, 以记录每个reduce
对应数据的偏移量
Reduce过程
Reduce
大致分为copy
、sort
、reduce
三个阶段,重点在前两个阶段。copy
阶段包含一个
eventFetcher
来获取已完成的map
列表,由Fetcher
线程去copy
数据,在此过程中会启动两个merge
线程,分别将内存中的数据merge
到磁盘和将磁盘中的数据进行merge
。待数据copy
完成之后,开始进行sort
,阶段,sort
阶段主要是执行finalMerge
操作,纯粹的sort
阶段,完成之后就是reduce
阶段,调用用户定义的reduce
函数进行处理
- Copy阶段 ,简单地拉取数据。
Reduce
进程启动一些数据copy
线程,通过HTTP方式请求maptask
获取属于自己的文件。 - Merge阶段 。这里的
merge
如map
端的merge
动作,只是数组中存放的是不同map
端copy
来的数值。Copy
过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map
端的更为灵活。merge
有三种形式:内存到内存;内存到磁盘;磁盘到磁盘。默认情况下第一种形式不启用。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge
。与map
端类似,这也是溢写的过程,这个过程中如果你设置有Combiner
,也是会启用的,然后在磁盘中生成了众多的溢写文件。第二种merge
方式一直在运行,直到没有map
端的数据时才结束,然后启动第三种磁盘到磁盘的merge
方式生成最终的文件。 - 合并排序 。把分散的数据合并成一个大的数据后,还会再对合并后的数据排序。
- 对排序后的键值对调用
reduce
方法 ,键相等的键值对调用一次reduce
方法,每次调用会产生零个或者多个键值对,最后把这些输出的键值对写入到HDFS
文件中。
Shuffle过程
shuffle
(洗牌) 核心机制:数据分区,排序,分组,规约,合并等过程
shuffle
是由map
段和reduce
段的shuffle
共同组成的,一般把从Map
产生输出开始到Reduce
取得数据作为输入之前的过程称作Shuffle
- Collect阶段 :将
MapTask
的结果输出到默认大小为100M
的环形缓冲区,保存的是
key/value
,Partition
分区信息等。 - Spill阶段 :当内存中的数据量达到一定的阀值的时候,就会将数据写入本地磁盘,
在将数据写入磁盘之前需要对数据进行一次排序的操作,如果配置了combiner
,还会将
有相同分区号和key
的数据进行排序。 - Merge阶段 :把所有溢出的临时文件进行一次合并操作,以确保一个
MapTask
最终只
产生一个中间数据文件。 - Copy阶段 :
ReduceTask
启动Fetcher
线程到已经完成MapTask
的节点上复制一份属于
自己的数据,这些数据默认会保存在内存的缓冲区中,当内存的缓冲区达到一定的阀值
的时候,就会将数据写到磁盘之上。 - Merge阶段 :在
ReduceTask
远程复制数据的同时,会在后台开启两个线程对内存到本
地的数据文件进行合并操作。 - Sort阶段 :在对数据进行合并的同时,会进行排序操作,由于
MapTask
阶段已经对数
据进行了局部的排序,ReduceTask
只需保证Copy
的数据的最终整体有效性即可。
Reduce 端实现 JOIN
通过将关联的条件作为map
输出的key
,将两表满足join
条件的数据并携带数据所来源的文件信息,发往同一个reduce task
,在reduce
中进行数据的串联
Map端实现 JOIN
适用于关联表中有小表的情形。使用分布式缓存,可以将小表分发到所有的map
节点,这样map
节点就可以在本地对自己所读到的大表数据进行join
并输出最终结果,可以大大提高join
操作的并发度,加快处理速度,先在mapper
类中预先定义好小表,进行join
求共同好友
A: B,C,D,F,E,O
B: A,C,E,K
C: A,B,D,E,I
D: A,E,F,L
给出A-O
个人中每个人的好友列表,求出哪些人两两之间有共同好友,以及他们的共同好友都有谁。注意: 这些人好友都是单向的,可能A是B的好友,但是B不一定是A的好友,这种类似的微博的关注,A关注B,但是B不一定关注了A
思路分析:
- 从上面可以现在我们知道
A-O
每个人拥有哪些好友,但是我们现在是要找出两两之间的人有哪些共同好友,可以逆向思维,第一步找出哪些好友拥有A,哪些好友拥有B…依次找出 - 通过得出上面的数据后,可以对后面的好友进行排序,避免重复,将 “拥有这名朋友的所有人”进行两两配对,并将配对后的字符串当做键,“朋友”当做值输出,即输出<人-人,共同朋友>
自定义InputFormat合并小文件
无论HDFS
还是MapReduce
,对于小文件都有损效率,实践中,又难免面临处理大量小文件的场景,此时,就需要有相应解决方案,小文件的优化无非以下几种方式:
- 在数据采集的时候,就将小文件或小批数据合成大文件再上传
HDFS
- 在业务处理之前,在
HDFS
上使用MapReduce
程序对小文件进行合并 - 在
MapReduce
处理时,可采用combineInputFormat
提高效率
程序的核心机制:自定义一个InputFormat
。改写RecordReader
,实现一次读取一个完整文件封装为KV
,在输出时使用SequenceFileOutPutFormat
输出合并文件
自定义OutputFormat合并小文件
例: 现在有一些订单的评论数据,需求,将订单的好评与差评进行区分开来,将最终的数据分开到不同的文件夹下面去,数据内容参见资料文件夹,其中数据第九个字段表示好评,中评,差评。0:好评
,1:中评
,2:差评
关键点是要在一个MapReduce
程序中根据数据的不同输出两类结果到不同目录,这类灵活的输出需求可以通过自定义outputformat
来实现。实现要点:
- 在
MapReduce
中访问外部资源 - 自定义
outputformat
,改写其中的recordwriter
,改写具体输出数据的方法write()