org.apache.hadoop.mapred.FileInputFormat中268行,getSplits方法实现:
public InputSplit[] getSplits(JobConf job, int numSplits)
throws IOException {
Stopwatch sw = new Stopwatch().start();
FileStatus[] files = listStatus(job);
// Save the number of input files for metrics/loadgen
job.setLong(NUM_INPUT_FILES, files.length);
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();
}
long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
long minSize = Math.max(job.getLong(org.apache.hadoop.mapreduce.lib.input.
FileInputFormat.SPLIT_MINSIZE, 1), minSplitSize);
// generate splits
ArrayList<FileSplit> splits = new ArrayList<FileSplit>(numSplits);
NetworkTopology clusterMap = new NetworkTopology();
for (FileStatus file: files) {
Path path = file.getPath();
long length = file.getLen();
if (length != 0) {
FileSystem fs = path.getFileSystem(job);
BlockLocation[] blkLocations;
if (file instanceof LocatedFileStatus) {
blkLocations = ((LocatedFileStatus) file).getBlockLocations();
} else {
blkLocations = fs.getFileBlockLocations(file, 0, length);
}
if (isSplitable(fs, path)) {
long blockSize = file.getBlockSize();
long splitSize = computeSplitSize(goalSize, minSize, blockSize);
long bytesRemaining = length;
while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,
length-bytesRemaining, splitSize, clusterMap);
splits.add(makeSplit(path, length-bytesRemaining, splitSize,
splitHosts[0], splitHosts[1]));
bytesRemaining -= splitSize;
}
if (bytesRemaining != 0) {
String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
- bytesRemaining, bytesRemaining, clusterMap);
splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
splitHosts[0], splitHosts[1]));
}
} else {
String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations,0,length,clusterMap);
splits.add(makeSplit(path, 0, length, splitHosts[0], splitHosts[1]));
}
} else {
//Create empty hosts array for zero length files
splits.add(makeSplit(path, 0, length, new String[0]));
}
}
sw.stop();
if (LOG.isDebugEnabled()) {
LOG.debug("Total # of splits generated by getSplits: " + splits.size()
+ ", TimeTaken: " + sw.elapsedMillis());
}
return splits.toArray(new FileSplit[splits.size()]);
}
主要信息
totalSize:是job所有输入的总大小。
numSplits:自己设定的split个数。
goalSize:是输入总大小与提示Map task数量的比值,即期望每个Mapper处理多少的数据,仅仅是期望,具体处理的数据数由下面的computeSplitSize决定。
minSplitSize:“private long minSplitSize = 1”。
minSize:取的1和『job.getLong(“mapred.min.split.size”, 1)是获取配置文件中设置的值,若没有设置,则取1』中较大的一个。
blockSize:hadoop 2.x 默认为128M。
splitSize:就是最终每个Split的大小。
分析
先看computeSplitSize方法的实现:首先在goalSize和blockSize中取较小的,然后与minSize相比取较大的。
protected long computeSplitSize(long goalSize, long minSize,long blockSize) {
return Math.max(minSize, Math.min(goalSize, blockSize));
}
一个splits即一个map,只要搞清楚片的大小,就能计算出运行时的map数。而一个split的大小是由goalSize, minSize, blockSize这三个值决定的。computeSplitSize的逻辑是,先从goalSize和blockSize两个值中选出最小的那个(比如一般不设置map数,这时blockSize为当前文件的块size;而goalSize是文件大小除以用户设置的map数得到的,如果没设置的话,默认是1),在默认的大多数情况下,blockSize比较小。然后再取blockSize和minSize中最大的那个。而minSize如果不设置的话一般为1,这样得出的一个splits的size就是blockSize,即一个块一个map,有多少块就有多少map。
input_file_num : 输入文件的个数
(1)默认map个数
如果不进行任何设置,默认的map个数是和blcok_size相关的。
default_num = total_size / block_size;
(2)期望map数量
可以通过参数mapred.map.tasks来设置程序员期望的map个数,但是这个个数只有在大于default_num的时候,才会生效。
goal_num =mapred.map.tasks;
(3)设置处理的文件大小
可以通过mapred.min.split.size 设置每个task处理的文件大小,但是这个大小只有在大于
block_size的时候才会生效。
split_size = max(mapred.min.split.size,block_size);
split_num = total_size / split_size;
(4)计算的map个数
compute_map_num = min(split_num, max(default_num, goal_num))
除了这些配置以外,mapreduce还要遵循一些原则。 mapreduce的每一个map处理的数据是不能跨越文件的,也就是说max_map_num <= input_file_num。 所以,最终的map个数应该为:
final_map_num = min(compute_map_num, input_file_num)
经过以上的分析,在设置map个数的时候,可以简单的总结为以下几点:
i)如果想增加map个数,则设置mapred.map.tasks 为一个较大的值。
ii)如果想减小map个数,则设置mapred.min.split.size 为一个较大的值。
数据以单文件且文件内容以一行的形式表现
单位,只是一个逻辑概念,每个InputSplit并没有对文件进行实际的切割,只是记录了要处理的数据的位置(包括文件的path和hosts)和长度(由start和length决定)。因此以行记录形式的文本,可能存在一行记录被划分到不同的block,甚至不同的DataNode上去。通过分析getSplits方法,可以得出,某一行记录同样也可能被划分到不同的InputSplit。但是不会对map之类的操作造成影响,原因是与FileInputFormat关联的RecordReader中的readLine方法在读取数据的时候不会受到InputSplit划分的限制,当读取一行的时候会一直读取直到读取到行结束符为止,因此其能够读取不同的InputSplit,直到把这一行数据读取完成。且当下一次在另外一个InputSplit中再次读取到该行的数据时不会再次进行计算。
举例
默认情况下,HadoopRDD一个分区最大是blockSize=128M,假设一个文件大小是totalSize=1G+10M,但是我设置4个分区(goalSize=258M),根据Math.max(minSize, Math.min(goalSize, blockSize),那么在计算分区个数的时候会计算为9个分区,最后一个分区大小为10M。
相关参考:
《Spark以textFile方式读取文件源码》
https://www.cnblogs.com/dhName/p/11068582.html
《Spark没有读取HDFS文件的方法?那textFile是怎么读的?》
https://blog.youkuaiyun.com/qq_41946557/article/details/103113304