目录
FileINputFormat的切片机制
FileInputFormat是MapReduce中用于处理文件输入的基类,它定义了输入文件的切片规则,并提供了默认的切片实现。具体来说,FileInputFormat会根据输入文件的大小和块大小等因素计算出每个切片的起始位置和长度,然后将这些切片封装成InputSplit对象,并将它们作为Mapper的输入。
继承了FileInputFormat的类都使用了FileInputFormat的切片机制。
切片机制
- 简单按照文件的内容长度进行切片
- 集群模式下,切片大小默认=HDFS块大小
- 切片时不是非要把一个切片占满,而是根据文件进行单独切片(不管文件多小都会单独占用一个切片)
- 如果文件>块大小且文件大小/片大小<=1.1,则多余部分不再多划分一个InputSplit片,而是直接合并到一个InputSplit中。
比如,一个文件大小为140MB的文本文件需要进行wordcount,在经过InputFormat切片处理后,它并不会启动两个MapTask去执行它,而是仅仅启动一个MapTask去执行。这是因为140/128=1.09375 < 1.1,这是为了防止在切分的过程中出现较小的切片导致MapReduce执行效率低下。我们在源码中也可以看到:其中的 SPLIT_SLOP 表示的就是InputSplit的切分大小的最大偏差比例。
public List<InputSplit> getSplits(JobContext job) throws IOException {
StopWatch sw = new StopWatch().start();
//获取InputSplit的size的最小值minSize和最大值maxSize
/*
getFormatMinSplitSize()=1
getMinSplitSize(job)=0
所以最终minSize=1
*/
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
/*
getMaxSplitSize(job)=Long.MAX_VALUE
所以maxSize等于Long的最大值
*/
long maxSize = getMaxSplitSize(job);
// 创建List,准备保存生成的InputSplit
List<InputSplit> splits = new ArrayList<InputSplit>();
//获取输入文件列表
List<FileStatus> files = listStatus(job);
/*
!getInputDirRecursive(job) = !false = true
job.getConfiguration().getBoolean(INPUT_DIR_NONRECURSIVE_IGNORE_SUBDIRS, false) = false
所以ignoreDirs=false
*/
boolean ignoreDirs = !getInputDirRecursive(job)
&& job.getConfiguration().getBoolean(INPUT_DIR_NONRECURSIVE_IGNORE_SUBDIRS, false);
//迭代输入文件列表
for (FileStatus file: files) {
//是否忽略子目录,默认不忽略
if (ignoreDirs && file.isDirectory()) {
continue;
}
//获取 文件/目录 路径
Path path = file.getPath();
//获取 文件/目录 长度
long length = file.getLen();
if (length != 0) {
//保存文件的Block块所在的位置
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
FileSystem fs = path.getFileSystem(job.getConfiguration());
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
//判断文件是否支持切割,默认为true
if (isSplitable(job, path)) {
//获取文件的Block大小,默认128M
long blockSize = file.getBlockSize();
//计算split的大小
/*
内部使用的公式是:Math.max(minSize, Math.min(maxSize, blockSize))
splitSize = Math.max(1, Math.min(Long.MAX_VALUE, 128))=128M=134217728字节
所以我们说默认情况下split逻辑切片的大小和Block size相等
*/
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
//还需要处理的文件剩余字节大小,其实就是这个文件的原始大小
long bytesRemaining = length;
//
/*
SPLIT_SLOP = 1.1
文件剩余字节大小/1134217728【128M】 > 1.1
意思就是当文件剩余大小bytesRemaining与splitSize的比值还大于1.1的时候,就继续切分,
否则,剩下的直接作为一个InputSplit
敲黑板,划重点:只要bytesRemaining/splitSize<=1.1就会停止划分,将剩下的作为一个InputSplit
*/
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
//组装InputSplit
/*
生成InputSplit
path:路径
length-bytesRemaining:起始位置
splitSize:大小
blkLocations[blkIndex].getHosts()和blkLocations[blkIndex].getCachedHosts():所在的 host(节点) 列表
makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts())
*/
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
bytesRemaining -= splitSize;
}
//最后会把bytesRemaining/splitSize<=1.1的那一部分内容作为一个InputSplit
if (bytesRemaining != 0) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining,
blkLocations[blkIndex].getHosts(),
blkLocations[blkIndex].getCachedHosts()));
}
} else { // not splitable
//如果文件不支持切割,执行这里
if (LOG.isDebugEnabled()) {
// Log only if the file is big enough to be splitted
if (length > Math.min(file.getBlockSize(), minSize)) {
LOG.debug("File is not splittable so no parallelization "
+ "is possible: " + file.getPath());
}
}
//把不支持切割的文件整个作为一个InputSplit
splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts(),
blkLocations[0].getCachedHosts()));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
// Save the number of input files for metrics/loadgen
job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.now(TimeUnit.MILLISECONDS));
}
return splits;
}
案例
输入目录下有3个文件:
a.txt 200MB
b.txt 120MB
c.txt 10MB
我们hadoop3.x默认的HDFS块大小=128MB,则切片如下:
a.txt.split1-- 0~128
a.txt.split2-- 128~200
b.txt.split1-- 0~120
c.txt.split1-- 0~10
TextInputFormat、KeyValueTextInputFormat、NLineInputFormat都继承于FilenputFormat,所以它们都有相同的切片机制。不同的是ConbineTextInputFormat,它继承CombineFileInputFormat,而CombineFileInputFormat又继承了FileInputFormat,但是它自己有一套自己的切片机制。
TextInputFormat
TextInputFormat是MapReduce默认的实现类,也就是数据的默认的输入到map函数的格式。TextInputFormat是安航读取文件的内容。其中,键是存储每一行的字节偏移量,是LongWritable类型。值是这一行的文本内容(不包括换行符和回车符)。
案例
输入文件a.txt:
hello world
hello hadoop,java
I like java a lot
该文本文件对应的键值对如下:
key value
-------------
0 hello world
12 hello hadoop,java
30 I like java a lot
其中,0、12、13是每一行文本的键,而每一行的内容就是该键所对应的值。
实例:之前的worldcount、流量统计。
KeyValueTextInputFormat
每一行均为一条记录,被分隔符分隔为kry,value。默认的分隔符为 "\t" ,也就是Tab键。可以通过驱动类来设置默认的分隔符,如下:
conf.set(KeyValueLineRecordReader.KRY_VALUE_SEPERATOR,"\t");
分隔符之前的为key,分隔符之后的内容为value,它们都是Text类型。
案例
输入文件:a.txt
hello world
hello java
hello Hadoop,Java
该文本文件对应的键值对如下:
key value
-------------
hello world
hello java
hello Hadoop,Java
其中,每一行的key均为hello,其余内容为值。
NLineInputFormat
如果使用NLineInputFormat,代表每个map进程处理的InputSplit不再按Block块去划分,而是按照NLineInputFormat指定的行数N去划分。也就是说,文件的总行数/N=切片数,如果不能整除,则切片数=商+1。
案例
输入文件:a.txt
hello hadoop
hello world
hello java
hello spark
hello storm
比如N=2,则对应的分片为两个,同时将开启两个MapTask:
分片1:
key value
-------------
0 hello hadoop
13 hello world
25 hello java
分片2:
key value
-------------
35 hello java
46 hello storm
这里的键和TextInputFormat一致。
ConbineTextInputFormat
MapReduce默认的TextInputFormat的切片机制是按文件进行规划切片,集群模式下,默认的切片大小=HDFS块大小。但一般不管文件多小,都会给它分配一个MapTask去执行,这样如果有大量的小文件,就需要开启大量的MapTask,极大地耗费资源。
比如我们的输入目录下有多个文件:
a.txt 1MB
b.txt 1.2MB
c.txt 0.5MB
d.txt 4MB
显然不值得为每一个文件去开启一个MapTask,所以我们使用ConbineTextInputFormat来解决,他可以将多个小文件从逻辑上划分到一个切片当中(逻辑的意思就是并不是把这些小文件合并成一个文件),这样,多个文件就可以交给一个MapTask去执行。
虚拟存储值
ConbineTextINputFormat的切片机制:
虚拟存储过程:
将输入目录下的文件依次与设置的最大分片值进行比较。
- 如果文件大小<=分片最大值,则单独划分为一个虚拟存储块。
- 如果文件大小>分片最大值并且<2*分片最大值,则将文件一分为二,形成两个虚拟存储块。
- 如果文件大小>分片最大值并且>2*分片最大值时,以最大值切割一块。
切片过程:
- 判断虚拟存储文件的大小是否 >= 分片最大值,大于等于则单独形成一个切片。
- 如果不大于则和下一个虚拟存储我呢间共同进行合并,共同形成一个切片。
案例
输入4个文件:
a.txt 1.7MB
b.txt 2.0MB
c.txt 6.8MB
d.txt 5.2MB
虚拟存储过程:
a.txt(1.7MB) < 4MB 划分一块
b.txt(2.0MB) < 4MB 划分一块
c.txt(6.8MB) > 4MB 划分为两块各自3.4MB
d.txt(5.2MB) > 4MB 划分为两块各自2.6MB
最终的虚拟存储文件:
1.7MB
2.0MB
3.4MB
3.4MB
2.6MB
2.6MB
切片过程:首先根据文件名的字典序对文件进行排序,再对虚拟存储文件进行合并
切片1:1.7MB+2.0MB
切片2:3.4MB+3.4MB
切片3:2.6MB+2.6MB
最终切片数从4->3,仍然需要开启3个MapTask,依旧占用资源。这是因为我们 设置的切片最大值过小,如果我们设置切片最大值=20MB,则:
a.txt(1.7MB) < 20MB 划分一块
b.txt(2.0MB) < 20MB 划分一块
c.txt(6.8MB) < 20MB 划分一块
d.txt(5.2MB) < 20MB 划分一块
最终的虚拟存储文件
1.7MB
2.0MB
6.8MB
5.2MB
切片:
切片1:(1.7+2.0+6.8+5.2)
切片数:4->1,只需要开启1个MapTask。
设置数据输入格式
不需要修改代码,只需要在Driver类中修改Job的属性:
默认的输入格式为TextInputFormat,如果是多个小文件的情况,我们需要设置输入格式:
//设置切片规则-默认为TextInputFormat.class
job.setInputFormatClass(CombineTextInputFormat.class);
设置虚拟存储的最大值
//设置虚拟存储切片为4MB-根据实际需求灵活调整大小
// CombineTextInputFormat.setMaxInputSplitSize(job,4194304);
//设置虚拟存储切片为20MB
CombineTextInputFormat.setMaxInputSplitSize(job,20971520);