MapReduce知识点整理
基于版本:Hadoop 2.7.2
序列化
Q: 为什么Hadoop不使用Java自带的序列化?
A: Java自带的序列化框架过于重量级(附带很多额外信息:校验信息、Header、继承体系等),网络传输效率低,所以Hadoop自己实现了序列化机制(Writable
接口).
自定义可序列化类
- 实现
Writable
接口 - 反序列化用到反射,需要调用无参数构造方法,因此自定义的可序列化类需要有无参数构造方法
- 重新
write
方法(序列化)和readFields
方法(反序列化),注意序列化和反序列化时字段顺序需要保持一致 - Hadoop的shuffle过程会对键排序,因此如果需要自定义的可序列化类对象作为键,还需要实现
WritableComparable
接口 - 如果对象需要作为reduce的输出写入文件,需要重写
toString
方法实现其字符串形式的表示,一般采用\t
将字段分开的形式,便于之后的MapReduce任务读取
InputFormat
InputFormat
抽象类InputFormat
是输入格式的基类,主要实现了两个功能:
- 将输入文件切片
- 从切分读入键值对序列
FileInputFormat
FileInputFormat
是针对文件输入的抽象类,主要处理文件切片逻辑- 按输入文件长度切片,默认的切片大小等于
BlockSize
,对于每个输入文件单独切分而不是合并切片 splitSize = Math.max(minSize, Math.min(maxSize, blockSize))
- 通过设置
mapreduce.input.fileinputformat.split.minsize
(默认值为1
)大于BlockSize
,让切片变大 - 通过设置
mapreduce.input.fileinputformat.split.maxsize
(默认值为Long.MAX_VALUE
)小于BlockSize
,让切片变小 - 细节:将文件切片时,每次判断剩余文件大小是否大于切片大小的1.1倍,如果是,则新建切片
TextInputFormat
- 默认的输入格式实现类
- 按行读取文件
- 键:存储在整个文件中的起始字节偏移量,
LongWritable
类型 - 值:本行内容,不包含换行符和回车符,
Text
类型
CombineTextInputFormat
- 功能:合并小文件
- 两阶段切片机制:(1)虚拟存储过程;(2)切片过程
CombineTextInputFormat.setMaxInputSplitSize(job, MAX_SIZE)
将虚拟存储切片最大的大小设置为MAX_SIZE
CombineTextInputFormat.setMaxInputSplitSize(job, MIN_SIZE)
将虚拟存储切片最小的大小设置为MIN_SIZE
OutputFormat
抽象类OutputFormat
是Reduce输出的基类
TextOutputFormat
默认的输出格式,把每行记录作为文本行。内部调用toString
方法把键和值转化为字符串
SequenceFileOutputFormat
当该MapReduce任务的输出被用于下一个MapReduce任务的输入时使用,下一个MapReduce任务使用SequenceFileInputFormat
作为输入格式。优点是合并了小文件,存储格式紧凑,便于压缩。
自定义OutputFormat
- 为了控制输出文件的路径和输出格式,可以自定义OutputFormat. 例如可以用于MapReduce任务结果写入MySQL, redis等存储中
- 自定义一个类继承
FileOutputFormat
- 自定义一个类继承
RecordWriter
,重写write
方法
MapReduce工作流程
MapTask工作机制
- Read阶段
- Map阶段
- Collect阶段
- 把计算好的
<k, v>
对以及元信息放在环形缓冲区,所谓环形缓冲区实际只是一个字节数组,用equator
标明了环的起始位置 <k, v>
对从equator
按照索引递增方向存储- 元信息(
partition
,keystart
,valstart
,vallen
,kvindex
)从equator
按照索引递减方向存储
- 把计算好的
- 溢写阶段:当环形缓冲区的内容占缓冲区大小80%时,会把环形缓冲区的内容快速排序后写入磁盘临时文件(如果有combiner,则写入磁盘前会先经过combiner),当MapTask的所有数据处理完成后,对临时文件中的内容进行归并,排序后的内容写入磁盘(如果有combiner,则写入磁盘前会再次经过combiner)
- Q: 为什么不等环形缓冲区满再溢写? A: 会出现写入环形缓冲区阻塞
- Q: 为什么把
<k, v>
对与元信息放在同一个buffer? A: 提高buffer利用率,减少溢写次数
关于MapTask的源码级别的分析可以参考这篇博客:MapReduce源码解析–Map解析
ReduceTask工作机制
- Copy阶段
- Sort阶段
- Reduce阶段
关于ReduceTask的源码级别的分析可以参考这篇博客:MapReduce源码解析–Reduce解析
Map/Reduce任务的并行度
- map任务并行度等于切片数,默认切片大小等于
BlockSize
- reduce任务并行度由用户指定参数
numReduceTasks
决定,等于输出文件的个数 - reduce任务的并行度默认为1
- 如果不需要reduce任务,则将
numReduceTasks
设为0,此时map任务的输出直接写入输出路径 - 如果自定义分区数大于1而
numReduceTasks
为1,则不会进行分区
Partition
- 默认:HashPartioner
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;
}
}
- 自定义Partitioner的时候,注意同步修改
numReduceTasks
参数,与自定义的分区数保持一致。
排序
MapReduce工作流程排序机制
- 不管程序逻辑是否需要,MapReduce框架的map阶段和reduce阶段都会进行排序,默认情况下按照字典序排序
- 对MapTask, 它会将处理的结果暂时放到内存中的环形缓冲区,当环形缓冲区的使用率达到80%的阈值后,再对缓冲区的数据进行快速排序,并将排序后的数据溢写到磁盘上,当MapTask所有数据处理完毕后,它会将磁盘上所有文件归并,归并过程使用是堆排序(优先队列实现)
- 对ReduceTask, 它从每个MapTask上远程拷贝相应数据文件,如果文件大小超过一定阈值,则溢写到磁盘,否则存储在内存中。如果磁盘上文件数据达到或超过阈值,则进行一次归并合并成一个更大的文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并. 和MapTask一样,归并过程使用的是堆排序
MapReduce排序应用:全排序
单分区
MapReduce框架自动实现输出结果的全排序,缺点:效率低
分区排序
自定义Partitioner,按照需要排序的键分区,不同的键按照顺序进入不同的分区
采样分区
在数据分布不均匀的情况下,按照等距间隔划分Partitioner可能造成数据倾斜,拖慢整个任务的执行速度. 使用InputSampler对数据进行采样,将得到的数据采样分区间隔写入文件,使用TotalOrderPartitioner类读取分隔值作为分区依据
MapReduce排序应用:group逻辑
在MapReduce里实现类似SQL的groupby
的逻辑,可以在自定义map输出键类型的基础上继承GroupingComparator
实现让部分字段相同的键进入同一个reduce方法
Join in MapReduce
MapReduce实现SQL的join
逻辑,有两种方法,一种是Reduce侧join,一种是Map侧join
Reduce侧join
在map里给记录增加一个标签字段,通过获取切片的路径,获取每条记录来自哪一个输入文件/表的信息,给不同输入文件/表的输入打不同标签. map的输出以连接的字段(join on
)为键,其余输入字段和新增的标签字段为值,连接字段相同、来源文件/表不同的记录会在同一个partition中给同一个reduce处理,reduce中将它们连接起来。
reduce侧join适合两表或多表较大,无法放入节点内存的情形,开销较大。
Map侧join
把小表/文件分发到map节点上,在mapper的setup阶段,将小表/文件读入内存,在map中直接和大表join
map侧join适合需要连接的表只有一张表很大,其余表都很小,可以全部放入节点内存的情形,开销小。
调优
输入阶段
输入大量小文件,造成切片过小,map实际运行时间很短。调优方法:
- 用
CombineTextInputFormat
合并大量小文件 - 开启JVM重用:一个map运行在一个JVM上,开启JVM重用,该map运行完毕后,JVM继续运行其他map. 通过设置
mapreduce.job.jvm.numtasks
值实现
map阶段
- 减少溢写次数:调整
io.sort.mb
(默认100M
)以及sort.spill.percent
(默认80%
)增大环形缓冲区以及环形缓冲区发生溢写的阈值 - 减少merge次数:调整
io.sort.factor
参数,增大merge的文件数 - 业务逻辑允许的前提下,使用Combiner减少shuffle阶段的IO
reduce阶段
- 合理设置reduce并行度
- 设置map/reduce共存:调整
slowstart.completedmaps
,使map运行到一定程度后reduce就开始运行 - 设置
mapred.job.reduce.input.buffer.percent
(默认为0.0
),将一定比例的shuffle后的buffer不落盘,直接给reduce任务
IO传输
- 使用Snappy/LZO压缩方式传输map输出
- 使用SequenceFile二进制文件在多个MapReduce任务之间传输
数据倾斜
减小数据倾斜的方法:
- 抽样和范围分区:对原始数据集抽样预设分区边界
- 自定义分区
- 使用Combiner
- 使用map join替代reduce join