MapReduce原理
一个完整的MapReduce程序在分布式运行时有三类实例进程:
- MrAppMaster:负责整个程序的过程调度及状态协调。
- MapTask:负责Map阶段的整个数据处理流程。
- ReduceTask:负责Reduce阶段的整个数据处理流程。
1. InputFormat数据输入
1.1 切片与MapTask并行度机制
- 切片数决定MapTask个数,MapTask个数决定了并行度
- **数据块:**Block是HDFS物理上把数据分成一块一块,默认128M一块。数据块是HDFS存储数据单位。
- **数据切片:**数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储,相当于使用索引记录切片。数据切片是MapReduce程序计算输入数据的单位,一个切片会对应启动一个MapTask。
- 一个Job的Map阶段并行度由客户端在提交Job时的切片数决定
- 每一个Split切片分配一个MapTask并行实例处理
- 默认情况下切片大小=Block块大小,可以通过配置修改
- 提交多个数据(多个文件)时,切片时不考虑数据集整体,而是以文件为单位进行切片
- Job的提交,提交三样东西:job.xml(默认参数信息),切片信息,jar包(集群模式才有)
1.2 Job提交流程源码和切片源码详解
1.2.1 Job提交流程源码详解
waitForCompletion()
submit();
// 1建立连接
connect();
// 1)创建提交Job的代理
new Cluster(getConfiguration());
// (1)判断是本地运行环境还是yarn集群运行环境
initialize(jobTrackAddr, conf);
// 2 提交job
submitter.submitJobInternal(Job.this, cluster)
// 1)创建给集群提交数据的Stag路径
Path jobStagingArea = JobSubmissionFiles.getStagingDir(cluster, conf);
// 2)获取jobid ,并创建Job路径
JobID jobId = submitClient.getNewJobID();
// 3)拷贝jar包到集群(没有集群就不提交)
copyAndConfigureFiles(job, submitJobDir);
rUploader.uploadFiles(job, jobSubmitDir);
// 4)计算切片,生成切片规划文件
writeSplits(job, submitJobDir);
maps = writeNewSplits(job, jobSubmitDir);
input.getSplits(job);
// 5)向Stag路径写XML配置文件
writeConf(conf, submitJobFile);
conf.writeXml(out);
// 6)提交Job,返回提交状态
status = submitClient.submitJob(jobId, submitJobDir.toString(),job.getCredentials());
Job提交流程
1.2.2 FileInputFormat切片源码解析(input.getSplits(job))
-
程序先找到数据存储的目录
-
开始遍历处理(规划切片)目录下的每一个文件
-
遍历第一个文件ss.txt
-
获取文件大小fs.sizeOf(ss.txt)
-
计算切片大小computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
-
默认情况下,切片大小=blocksize
-
开始切,形成第1个切片:ss.txt—0:128M 第2个切片ss.txt—128:256M 第3个切片ss.txt—256M:300M
(每次切片时,都要判断切完剩下的部分是否大于块的1.1倍,不大于1.1倍就划分一块切片)
-
将切片信息写到一个切片规划文件中
-
整个切片的核心过程在getSplit()方法中完成
-
InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。
-
-
提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数。
1.3 FileInputFormat 切片机制
- 简单的按照文件的内容长度进行切片
- 切片的大小,默认等于Block大小
- 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
源码中计算切片大小的公式:
- Math.max(minSize, Math.min(maxSize, blockSize));
- mapreduce.input.fileinputformat.split.minsize=1 默认值为1
- mapreduce.input.fileinputformat.split.maxsize= Long.MAXValue 默认值Long.MAXValue;因此,默认情况下,切片大小=blocksize。
切片大小的设置:
maxsize(切片最大值):参数如果比blocksize小,切片会等于这个值,即切片变小
minsize(切片最小值):参数如果比blocksize大,则可以让切片比blocksize大
切片信息的获取:
// 获取切片的文件名称
String name = inputSplit.getPath().getName();
// 根据文件类型获取切片信息
FileSplit inputSplit = (FileSplit)context.getInputSplit();
1.3.1 TextInputFormat
- TextInputFormat是Hadoop默认的InputFormat。
- FileInputFormat 常见的接口实现类包括:TextInputFormat<LongWritable, Text>、KeyValueTextInputFormat<Text,Text>、 NLineInputFormat、CombineTextInputFormat<LongWritable, Text> 和自定义 InputFormat 等。
TextInputFormat源码
@InterfaceAudience.Public
@InterfaceStability.Stable
public class TextInputFormat extends FileInputFormat<LongWritable, Text> {
@Override
//核心方法
public RecordReader<LongWritable, Text>
createRecordReader(InputSplit split,
TaskAttemptContext context) {
String delimiter = context.getConfiguration().get(
"textinputformat.record.delimiter");
byte[] recordDelimiterBytes = null;
if (null != delimiter)
recordDelimiterBytes = delimiter.getBytes(Charsets.UTF_8);
//返回一个按行读取的RecordReader
return new LineRecordReader(recordDelimiterBytes);
}
@Override//判断文件是否可切割
protected boolean isSplitable(JobContext context, Path file) {
final CompressionCodec codec =
new CompressionCodecFactory(context.getConfiguration()).getCodec(file);
if (null == codec) {
return true;
}
return codec instanceof SplittableCompressionCodec;
}
}
1.3.2 CombineTextInputFormat 切片机制
- 框架默认的 TextInputFormat 切片机制是对任务按文件规划切片,不管文件多小,都会 是一个单独的切片,都会交给一个 MapTask,这样如果有大量小文件,就会产生大量的 MapTask,处理效率极其低下。
- CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到 一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。
- 虚拟存储切片最大值设置 CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m 注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
- 切片机制分为:虚拟存储过程和切片过程二部分。
- 虚拟存储过程: 将输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize 值比较,如果不 大于设置的最大值,逻辑上划分一个块。如果输入文件大于设置的最大值且大于两倍, 那么以最大值切割一块;当剩余数据大小超过设置的最大值且不大于最大值 2 倍,此时 将文件均分成 2 个虚拟存储块(防止出现太小切片)。 例如 setMaxInputSplitSize 值为 4M,输入文件大小为 8.02M,则先逻辑上分成一个 4M。剩余的大小为 4.02M,如果按照 4M 逻辑划分,就会出现 0.02M 的小的虚拟存储 文件,所以将剩余的 4.02M 文件切分成(2.01M 和 2.01M)两个文件。
- 切片过程:
- 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值,大于等于则单独 形成一个切片。
- 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
- 测试举例:有 4 个小文件大小分别为 1.7M、5.1M、3.4M 以及 6.8M 这四个小 文件,则虚拟存储之后形成 6 个文件块,大小分别为: 1.7M,(2.55M、2.55M),3.4M 以及(3.4M、3.4M) 最终会形成 3 个切片,大小分别为: (1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
1.4 MapReduce工作流程
1.5 Shuffle机制
- Map方法之后,Reduce方法之前的数据处理过程称为Shuffle。
- Shuffle 中的缓冲区大小会影响到 MapReduce 程序的执行效率,原则上说,缓冲区 越大,磁盘 io 的次数越少,执行速度就越快。
- 缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb 默认 100M。
map( )方法后,getPartition方法标记数据的分区编号,进入默认为100M的环形缓冲区,左侧存储索引,右侧存储数据,当存储80%时进行反向溢写,80%是为了给溢写留时间,不会等待从而可以一直高效运转,利用率更高;从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件;多个溢出文件会被合并成大的溢出文件;溢写过程中和合并过程中会调用Partition进行分区和针对数据的key的索引按照字典顺序进行快速排序;溢写后会产生两个文件(spill.index,Spill.out);对溢写的文件会进行归并排序,压缩,写出到磁盘上,等待ReduceTask端拉取数据。ReduceTask根据分区号去各个MapTask拉取相应的结果分区数据并存储在内存,内存不够才会溢写到磁盘,接着对数据进行归并排序(合并),按照key相同进行分组后,再进入Reduce( )方法
1.6 Partition分区
默认分区是根据key的hashCode对ReduceTasks个数取模得到的。
自定义Partition:
- 自定义类继承Partitioner,重写getPartition()方法
- 在Job驱动中,设置自定义Partition: job.setPartitionerClass();
- 根据自定义Partitioner的逻辑设置相应数量的ReduceTask: job.setNumReduceTask( 5);
- 对于job.setNumReduceTask( ?)的总结:
- 如果ReduceTask的数量> getPartition的结果数,则会多产生几个空的输出文件part-r-000xx;
- 如果1<ReduceTask的数量<getPartition的结果数,则有一部分数据没处存储,会报错
- 如 果ReduceTask的数量=1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个 ReduceTask,最终也就只会产生一个结果文件 part-r-00000;
- 分区号必须从零开始,逐一累加。
此处继续使用案例《Hadoop序列化案例》,https://blog.csdn.net/Gooooot/article/details/127212956
第一步:自定义类继承Partitioner,重写getPartition()方法
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
//<Text, FlowBean>是Mapper输出的<K,V>
public class ProvincePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
//text是手机号
int partition;
String phone = text.toString();
String prePhone = phone.substring(0, 3);//(0,3]
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;
}
}
第二步:在Job驱动中,设置自定义Partition: job.setPartitionerClass();
第三步:根据自定义Partitioner的逻辑设置相应数量的ReduceTask: job.setNumReduceTask( 5);
//设置分区
job.setPartitionerClass(ProvincePartitioner.class);
//设置ReduceTask个数
job.setNumReduceTasks(5);
1.7 排序
1.7.1 概述
MapTask和ReduceTask均会对数据按 照key进行排序。该操作属于 Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
- Map阶段:
- 环形缓冲区溢写之前进行一次快速排序;
- 产生多次的溢写文件后,进行归并排序
- Reduce阶段:
- 拉取对应分区的数据后会进行归并排序
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件,如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
1.7.2 排序分类
- 部分排序:MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。(上述案例即为内部排序)
- 全排序:最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在 处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。(一般不采用)
- 辅助排序(GroupingComparator分组):在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
- 二次排序:在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
1.7.3 自定义排序WritableComparable以及二次排序
以bean对象作为key传输,需要实现WritableComparable接口并重写compareTo方法
具体案例见《WritableComparable排序案例分析(全排序)》https://blog.csdn.net/Gooooot/article/details/127214465
1.7.4 区内排序
在案例《WritableComparable排序案例分析(全排序)》https://blog.csdn.net/Gooooot/article/details/127214465的基础上,进行区内排序,需求:以手机号前三位相同的输出到同一文件内,且每个文件进行排序
第一步:设置Partiotioner类
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
//<FlowBean, Text>是Mapper传递过来的
public class ProvincePartiotioner extends Partitioner<FlowBean, Text> {
@Override
public int getPartition(FlowBean flowBean, Text text, int numPartitions) {
String phone = text.toString();
//手机号前三位代表省份
String prePhone = phone.substring(0, 3);
if ("136".equals(prePhone)) {
return 0;
} else if ("137".equals(prePhone)) {
return 1;
} else if ("138".equals(prePhone)) {
return 2;
} else if ("139".equals(prePhone)) {
return 3;
} else {
return 4;
}
}
}
第二步:在JobDriver添加如下:
//设置分区
job.setPartitionerClass(ProvincePartiotioner.class);
//设置ReduceTask个数
job.setNumReduceTasks(5);
1.8 Combiner
- Combiner是MR程序中Mapper和Reducer之外的一种组件。
- Combiner组件的父类就是Reducer。
- Combiner和Reducer的区别在于运行的位置
- Combiner在环形缓冲区溢写之前,是在每一个MapTask所在的节点运行,只负责一个MapTask的数据;
- Reducer在ReduceTask,负责所有MapTask的数据;
- Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
- Combiner能够应用的前提是不能影响最终的业务逻辑(例如求和),而且,Combiner的输出kv 应该跟Reducer的输入kv类型要对应起来。
1.9 OutPutFormat
默认的输出格式是TextOutPutFormat;
2 MapTask工作机制
-
Read阶段:
- 待处理文件,经由客户端进行切片划分
- 提交Yarn:Job.split(切片),Jar包和Job.xml
- Yarn计算出MapTask数量,开启MrAppMaster和对应的MapTask
- MapTask通过InputFormat(默认TextInputFormat(k偏移量,v一行内容))调用RecorderReader的reader( )读取待处理文件并返回给MapTask,进入到用户定义的Mapper;
-
Map阶段:
通过用户定义的Mapper的map( )的context.write(k,v),输出到对应的环形缓冲区;
-
Collect阶段:
环形缓冲区一侧存储元数据(索引,分区等),一侧存储数据,默认100M,到达80%时进行反向溢写,同时环形缓冲区所有的数据按照分区的方式存储,存储到一定阈值进行溢写,溢写前进行快速排序(以key按照字典顺序),并在必要时对数据进行合并、压缩等操作;
-
Spill溢写阶段:
MapReduce会将数据写到本地磁盘上,生成一个临时文件。
- 步骤1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排 序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据 按照key有序。
- 步骤2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文件 output/spillN.out(N表示当前溢写次数)中。如果用户设置了Combiner,则写入文件之前,对每 个分区中的数据进行一次聚集操作。
- 步骤3:将分区数据的元信息写到内存索引数据结构SpillRecord中,其中每个分区的元信息包括在临时 文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大小超过1MB,则将内存索引写 到文件output/spillN.out.index中。
-
Merge阶段:对溢写文件进行归并排序,确保一个MapTask只生成一个文件,持久化到磁盘,等待Reducer端拉取数据
3.ReduceTask工作机制
- Copy阶段:拉取指定分区的数据,并针对某一片数据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
- Merge阶段:在远程拷贝数据的同时,ReduceTask启动了两个后台线程对内存和磁盘上的文件进 行合并,以防止内存使用过多或磁盘上文件过多。
- Sort阶段:按照MapReduce语义,用户编写reduce()函数输入数据是按key进行聚集的一组数据。为了将key相同的数据聚在一起,Hadoop采用了基于排序的策略。由于各个MapTask已经实现对 自己的处理结果进行了局部排序,因此,ReduceTask只需对所有数据进行一次归并排序即可。
- Reduce阶段:reduce()函数将计算结果写到HDFS上。
3.1 ReduceTask并行度决定机制
MapTask 并行度由切片个数决定,切片个数由输入文件和切片规则决定。
ReduceTask 的并行度同样影响整个 Job 的执行并发度和执行效率,但与 MapTask 的并 发数由切片数决定不同,ReduceTask 数量的决定是可以直接手动设置;
- ReduceTask=0,表示没有Reduce阶段,直接输出Mapper阶段结果,输出文件个数和Map个数一致。
- ReduceTask默认值就是1,所以输出文件个数为一个。
- 如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜 。
- ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask。
- 具体多少个ReduceTask,需要根据集群性能而定。
- 如果分区数不是1,但是ReduceTask为1,是否执行分区过程。答案是:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1 肯定不执行。