MR切片机制及底层源码追踪
一、概述
MR程序在运行的时候需要将文件切分为多个分发给不同MapTask进行处理,那么问题来了,MR底层是如何对文件进行切分的呢?
二、InputFormat切片机制
- Hadoop框架内部定义了一个读取数据的基类流~InputFormat(抽象类),其内部定义了2个抽象方法
快捷键Ctrl + N 搜索InputFormat类,查看该类中方法
public abstract class InputFormat<K, V> {
public abstract List<InputSplit> getSplits(JobContext context); //生成切片,对数据切分
public abstract RecordReader<K,V> createRecordReader(InputSplit split,TaskAttemptContext context); //创建RecordReader对象,真正负责数据读取的对象.
==>真实按行读取是TextInputFormat类中的RecordReader()方法 //--67行
}
- 我们查看InputFormat的具体实现类
快捷键Ctrl + H 查看InputFormat类体系~~~查看具体实现类FileInputFormat(子抽象类)
- FileInputFormat中对getSplits()方法做出了具体实现. --389行
createRecordReader()方法未作任何改变。 --直接定位到父类中的该方法
isSplitable()方法:判断当前输入的数据是否可切分,默认true. --169行
该方法具体实现是在TextInputFormat中的isSplitable()方法,主要针对的是压缩文件
- FileInputFormat是子抽象类,我们看下其具体的实现类,见 上图
我们需要掌握的是TextInputFormat和CombineTextInputFormat
TextInputFormat是MapReduce默认使用的读取数据的类
比如:WordCount和Flow案例使用的均是TextInputFormat完成数据输入的
CombineTextInputFormat用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理,内部改写了切片规则(用来处理小文件过多造成启动多个MapTask运行处理很小的数据量的情况)
三、切片设置
- 块:HDFS存储数据的单位,指把要存储到HDFS文件系统上的文件以设置好的块的大小进行物理切分成N个块(真正的将数据切割)
- 切片:MapReduce计算数据的单位,指把要在MR中计算的数据从逻辑上按照设置的切片大小,划分为N个切片
- 示例:
比如现有一个200M的文件,要读取到MR程序中做运算,假设设置切片大小为100M,那么,逻辑上是将200M的数据切为
0-100M,100-200M两个切片,真实并没有将200M的数据进行切分,200M的文件数据依然保持完整。
生成的切片记录的是:第一个切片文件从0位置读到100M位置 ,第二个切片从100M读取到200M的位置
即记录读数据的时候,从什么位置读取到什么位置。
- 切片大小设置
切片的大小设置并不是随意设置的,要和块的设置大小关联起来
- 比如,现在对一个200M的文件进行操作,上传到HDFS系统,分为0-128M,128-200M两个块,那么假设存储到DN节点上就是dn1存储128M,dn2存储72M,我们希望当MR程序读取数据做运算时,MR可以一次性将节点上的数据读完或者保证一个MapTask只在一个节点上读数据(因为一个切片数据交给一个MapTask处理),所以综合考虑,我们应该将切片大小设置成和块大小一致。
- 默认:将切片大小设置成==block块大小,避免MR程序读取数据时,出现跨节点读取。
四、切片源码解读
//1、getSplits()方法源码解析:
/*
getFormatMinSplitSize() :1
getMinSplitSize(job) :
如果设置mapreduce.input.fileinputformat.split.minsize那么就使用设置的值,如果没有设置
默认是1
*/
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
/*
如果在Configuration中配置了如下内容的值
mapreduce.input.fileinputformat.split.maxsize那么获取的就是配置的值。
如果没有配置那么默认值是Long.MAX_VALUE
*/
long maxSize = getMaxSplitSize(job);
//最终将生成的切片放入到splits集合中.
List<InputSplit> splits = new ArrayList<InputSplit>();
//获取存储文件的详情
List<FileStatus> files = listStatus(job);
//遍历files
for (FileStatus file: files) {
//判断file是否是文件夹
if (ignoreDirs && file.isDirectory()) {
continue;
}
//获取文件路径
Path path = file.getPath();
//获取文件长度
long length = file.getLen();
//判断文件是否可切分
if (isSplitable(job, path)) {
//获取文件的块大小
long blockSize = file.getBlockSize();
//如果是集群环境,获取到的就是集群中设置的块大小,如果是本地环境,本地默认的块大小32M (33554432)
//计算切片大小
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
//查看computeSplitSize()方法
protected long computeSplitSize(long blockSize, long minSize,long maxSize) {
/*
默认块大小=切片大小
设置:
切片大小 < 块大小:修改maxSize的值
切片大小 > 块大小:修改minSize的值
*/
return Math.max(minSize, Math.min(maxSize, blockSize));
}
}
//文件的剩余大小
long bytesRemaining = length;
//只有当剩余文件大小 / 切片大下 其值 > 1.1 时,才会进行切片操作
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
//计算切片时如何去切
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
/*
splits :用来存储切片信息的集合
makeSplit : 用来生成切片信息
length-bytesRemaining : 切片的起始位置
splitSize :切片的偏移量(长度)
*/
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(), //记录的是读取那个文件
blkLocations[blkIndex].getCachedHosts())); //从文件那个位置读取到那个位置
//重新计算剩余文件的大小
bytesRemaining -= splitSize;
}
//把剩余文件大小/splitSize <= 1.1 把满足此条件的剩余文件切成一片
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
/*
splits :用来存储切片信息的集合
makeSplit : 用来生成切片信息
length-bytesRemaining : 切片的起始位置
bytesRemaining :切片的偏移量(长度)
*/
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
结论:
- 如果剩余待切片的数据大小 除以 块大小,大于1.1 ,才会继续切片,如果不大于,则直接将剩余的数据 生成一个切片.
- 真实切片时,是完全按照块大小进行切的,只是针对剩余数据是否仍要切片时,会有一个大于1.1的判断,要做上述判断,避免生成很小的切片,节省MapTask资源
- 切片大小默认等于Block大小128M,避免MR程序读取数据时,出现跨节点读取,节省资源。
- 每个切片都需要由一个MapTask来处理 , 也就意味着在一个MR中,有多少个切片,就会有多少个MapTask。
- 切片的时候每个文件单独切片,不会整体切片.(完全基于单个文件)
- 切片的个数不是越多越好,也不是越少越好,按照实际情况,处理成合适的切片数.
五、CombineTextInputFormat~切片机制
CombineTextInputFormat解决的问题:
用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个MapTask处理。
核心操作:设置虚拟切片最大值
在Driver中设置
job.setInputFormatClass(CombineTextInputFormat.class); --不设置,默认是FileInputFormat
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 假设设置为4M
1、如果文件的大小小于MaxInputSplitSize , 则文件规划成一个
2、如果文件的大小 大于MaxInputSplitSize ,但是小于 MaxInputSplitSize2 , 则文件规划成两个(对半分)
3、如果文件的大小大于 MaxInputSplitSize2 ,先按照MaxInputSplitSize的大小规划一个, 剩余的再进行规划.
4、最终按照 MaxInputSplitSize 大小来生成切片,
5、将规划好的每个虚拟文件逐个累加,只要不超过 MaxInputSplitSize大小,则都是规划到一个切片中的。
举例:
有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
基础链接:
MapReduce核心思想: https://blog.csdn.net/weixin_42796403/article/details/109732859
手写MR程序之wordcount案例: https://blog.csdn.net/weixin_42796403/article/details/109749080
Hadoop切片与MapTask并行度决定机制: https://blog.csdn.net/weixin_42796403/article/details/109753044
MR核心编程思想总结: https://blog.csdn.net/weixin_42796403/article/details/109802287
FileInputFormat切片源码解析: https://blog.csdn.net/weixin_42796403/article/details/109804581
CombineTextInputFormat切片机制: https://blog.csdn.net/weixin_42796403/article/details/109806713
TextInputFormat切片机制: https://blog.csdn.net/weixin_42796403/article/details/109811157