在技术领域,尤其是开源技术领域,版本变迁速度极快,有些介绍原理机制的文章可能并不能适合当前的技术版本,有时候找到一篇文章中关于某问题的解决方案,可能并不奏效,其原因何在?版本不同!
在文章中说清楚技术原理分析针对的版本是一件极其道德的事情。
本文论述的原理针对以下版本:
hadoop:2.7.3
1 问题
在进行map计算之前,MapReduce框架会根据输入文件计算输入数据分片(input split),每个数据分片针对一个map任务,数据分片存储的并非数据本身,而是一个分片长度和一个记录数据的位置的数组。
那么数据分成多少片(how much splits)是如何确定的呢?
2 MapTask数量影响因素
影响map个数(split个数)的主要因素有:
- 文件的大小。当块(dfs.block.size)为128m时,如果输入文件为128m,会被划分为1个split;当块为256m,会被划分为2个split。
- 文件的个数。FileInputFormat按照文件分割split,并且只会分割大文件,即那些大小超过HDFS块的大小的文件。如果HDFS中dfs.block.size设置为128m,而输入的目录中文件有100个,则划分后的split个数至少为100个。
- splitSize的大小。分片是按照splitszie的大小进行分割的,一个split的大小在没有设置的情况下,默认等于hdfs block的大小。
已知以下参数
input_file_num : 输入文件的个数
block_size : hdfs的文件块大小,2.7.3默认为128M,可以通过hdfs-site.xml中的dfs.block.size
参数进行设置
total_size : 输入文件整体的大小,由框架循环叠加计算
MapTask的数量计算原则为:
(1)默认map个数
如果不进行任何设置,默认的map个数是和blcok_size相关的。
default_num = total_size / block_size;
(2)自定义设置分片的minSize、maxSize
如果在MapReduce的驱动程序(main方法)中通过以下方法设置了分片的最小或最大的大小
//设置最小分片大小,单位byte
FileInputFormat.setMinInputSplitSize(job,1024*1024*10L); //10MB
//设置最大分片大小,单位byte
FileInputFormat.setMaxInputSplitSize(job,1024L); //1KB
应用程序可以通过数据分片的最大和最小大小两个参数来对splitsize进行调节,则计算方式就变成了
splitSize=Math.max(minSize, Math.min(maxSize, blockSize)
其中maxSize即方法setMaxInputSplitSize
设置的值,minSize即方法·setMinInputSplitSize·设置的值。
其设置原则就是
要增加map的个数,调整maxSize<blockSize;
要减小map的个数,调整minSize>blockSize。
如何查看Job的数据分片数?
方式1:在客户端提交任务时可在日志中查看,如图
方式2:如果开启了Hadoop的History Server,可在UI界面中看到MapTask的数量。
3. MapTask数量确定的原理分析
先来了解两个概念
block块(数据块,物理划分)
block是HDFS中的基本存储单位,hadoop1.x默认大小为64M而hadoop2.x默认块大小为128M。文件上传到HDFS,就要划分数据成块,这里的划分属于物理的划分(实现机制也就是设置一个read方法,每次限制最多读128M的数据后调用write进行写入到hdfs),块的大小可通过 dfs.block.size
配置。block采用冗余机制保证数据的安全:默认为3份,可通过dfs.replication
配置。
注意:当更改块大小的配置后,新上传的文件的块大小为新配置的值,以前上传的文件的块大小为以前的配置值。
split分片(数据分片,逻辑划分)
Hadoop中split划分属于逻辑上的划分,目的只是为了让map task更好地获取数据。split是通过hadoop中的InputFormat接口中的getSplits()方法得到的。
splitSize由以下变量共同决定:
(1)totalSize:整个mapreduce job输入文件的总大小,框架循环计算输入目录下的所有文件的大小之和。
long totalSize = 0; // compute total size
for (FileStatus file: files) { // check we have valid files
if (file.isDirectory()) {
throw new IOException("Not a file: "+ file.getPath());
}
totalSize += file.getLen();
}
(2)minSize:数据分片的最小值,默认为1
(3)maxSize:数据分片的最大值,默认为Integer最大值,即Integer.MAX_VALUE
Hadoop2.7.3版本分片大小计算源码如下:
org.apache.hadoop.mapreduce.lib.input.FileInputFormat
long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
long maxSize = getMaxSplitSize(job);
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(blockSize, minSize, maxSize);
protected long computeSplitSize(long blockSize, long minSize,long maxSize) {
return Math.max(minSize, Math.min(maxSize, blockSize));
}
分片确定的临界问题
分片大小的数量一定是按照公式
Math.max(minSize, Math.min(maxSize, blockSize))
计算的吗?
可做以下试验:文件大小 297M(311349250),块大小128M
测试代码
FileInputFormat.setMinInputSplitSize(job, 301349250);
FileInputFormat.setMaxInputSplitSize(job, 10000);
由上面分片公式算出分片大小为301349250, 比 311349250小, 理论应该为2个map, 但实际测试后Map个数为1, 这是为什么呢?
分片的计算中,会考虑空间利用问题,每次分出一个分片后,都会判断剩下的数据能否在一定的比率(slop变量,默认10%)内压缩到当前分片中,如果不大于默认比率1.1,则会压缩到当前分片中。源码如下
private static final double SPLIT_SLOP = 1.1; // 10% slop
long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
splits.add(makeSplit(path, length-bytesRemaining, splitSize,blkLocations[blkIndex].getHosts(),blkLocations[blkIndex].getCachedHosts()));
bytesRemaining -= splitSize;
}
4. 应用
4.1 MapTask数量设置不当带来的问题
Mapper数据过大的话,会产生大量的小文件,过多的Mapper创建和初始化都会消耗大量的硬件资源 。
Mapper数太小,并发度过小,Job执行时间过长,无法充分利用分布式硬件资源。
4.2MapTask数量控制方法
(1)如果想增加map个数,则通过FileInputFormat.setMaxInputSplitSize(job,long bytes)
方法设置最大数据分片大小为一个小于默认blockSize的值,越小map数量越多。
(2)如果想减小map个数,则FileInputFormat.setMinInputSplitSize(job,long bytes)
方法设置最小数据分片大小为一个大于默认blockSize的值,越大map数量越少。
(3)如果输入中有很多小文件,依然想减少map个数,则需要将小文件使用HDFS提供的API合并为大文件,然后使用方法2。
5. ReduceTask的数量如何确定?
Reduce任务是一个数据聚合的步骤,数量默认为1。使用过多的Reduce任务则意味着复杂的shuffle,并使输出文件数量激增。而reduce的个数设置相比map的个数设置就要简单的多,只需要设置在驱动程序中通过job.setNumReduceTasks(int n)
即可。