什么是InputSplit
InputSplit是指分片,在MapReduce当中作业中,作为map task最小输入单位。分片是基于文件基础上出来的而来的概念,通俗的理解一个文件可以切分为多少个片段,每个片段包括了<文件名,开始位置,长度,位于哪些主机>等信息。map task的数量由输入文件总大小和分片大小确定的;hadoop2.2版本hdfs的数据块默认是128M。若一个文件大于128M,通过将大文件分解得到若干个数据块;若一个文件小于128M,则按它的实际大小组块存储;
因此可以解释为什么hdfs喜欢海量的大文件,而不喜欢海量的小文件:
(1)海量的小文件需要namenode存储较多的元数据信息记录块的位置(假如一个64M的数据块(大文件分解的块)需要1k的元数据信息,那么1M的数据块(小文件的块)仍需要1k的元数据信息)
(2)HDFS就是为流式访问大文件而设计的,如果访问大量小文件,需要不断的从一个datanode跳到另一个datanode,严重影响性能。
hdfs的一个数据块一定属于一个文件;
这里又有个问题:为什么map task分片大小最好和hdfs设置的块大小一致呢?
我们综合考虑两点:
(1)map task的个数=输入文件总大小(totalSize)/分片尺寸(splitSize)。也就是说分片尺寸越大,map task的个数就越少=>系统执行的开销越小,系统管理分片的开销越小。
(2)网络传输开销,如果分片太大以至于一个分片要跨越多个HDFS块,则一个map任务必须要由多个块通过网络传输,所以分片大小的上限是HDFS块的大小。
所以,map任务时的分片大小设置为HDFS块的大小是最佳选择。
流程
FileInputFormat.getSplits返回文件的分片数目,这部分将介绍其运行流程,后面将粘贴其源码并给出注释
- 通过listStatus()获取输入文件列表files,其中会遍历输入目录的子目录,并过滤掉部分文件,如文件_SUCCESS
- 获取所有的文件大小totalSIze
- goalSIze=totalSize/numMaps。numMaps是用户指定的map数目
- files中取出一个文件file
- 计算splitSize。splitSize=max(minSplitSize,min(file.blockSize,goalSize)),其中minSplitSize是允许的最小分片大小,默认为1B
- 后面根据splitSize大小将file分片。在分片的时候,如果剩余的大小不大于splitSize*1.1,且大于0B的时候,会将该区域整个作为一个分片。这样做是为了防止一个mapper处理的数据太小
- 将file的分片加入到splits中
- 返回4,直到将files遍历完
- 结束,返回splits
-
其实流程算起来也不算复杂,所以就直接用代码注释来做吧
这里边涉及这么几个方法:
1、public List
getSplits(JobContext job), 这个由客户端调用来获得当前Job的所有分片(split),然后发送给JobTracker(新API中应该是ResourceManager),而JobTracker根据这些分片的存储位置来给TaskTracker分配map任务去处理这些分片。这个方法用到了后边的listStatus,然后根据得到的这些文件信息,从FileSystem那里去拉取这些组成这些文件的块的信息(BlockLocation),使用的是getFileBlockLocation(file,start,len),这个方法是与使用的文件系统实现相关的(FileSystem,LocalFileSystem,DistributedFileSystem) /** * Generate the list of files and make them into FileSplits. * @param job the job context * @throws IOException */ public List
getSplits(JobContext job) throws IOException { //根据默认配置与job所配置minInputSplitSize取得最小切片大小 long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job)); //根据job所配置maxInputSplitSize取得最大切片大小 long maxSize = getMaxSplitSize(job); //保存生成的split信息 List splits = new ArrayList (); List files = listStatus(job);//获取job所配置的文件列表 //对每个文件进行切片 for (FileStatus file: files) { Path path = file.getPath(); long length = file.getLen(); if (length != 0) { BlockLocation[] blkLocations; if (file instanceof LocatedFileStatus) { blkLocations = ((LocatedFileStatus) file).getBlockLocations(); } else { FileSystem fs = path.getFileSystem(job.getConfiguration()); blkLocations = fs.getFileBlockLocations(file, 0, length); } if (isSplitable(job, path)) {//验证非二进制文件可切分(二进制文件不允许切分) long blockSize = file.getBlockSize(); long splitSize = computeSplitSize(blockSize, minSize, maxSize);//计算分块尺寸 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())); bytesRemaining -= splitSize; } //对尾部不足一个分片大小的也生成一个分片 if (bytesRemaining != 0) { int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining); splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining, blkLocations[blkIndex].getHosts())); } } else { // not splitable //不可分块文件整体作为一块 splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts())); } } 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()); LOG.debug("Total # of splits: " + splits.size()); return splits; } 2、protected List
listStatus(JobContext job), 先根据“mapred.input.dir”的配置值去得到用户指定的所有Path。然后根据这个JobContext的Configuration得到FileSystem(当然,更可能是 DistributedFileSystem )。最后应用用户可能设置了的PathFilter,通过FileSystem获取所有这些Path所代表的File(FileStatus)。注:这个方法的东西相当多,很多内容还十分陌生。
3、protected long computeSplitSize(long blockSize, long minSize, long maxSize),计算出当前Job所配置的分片最大尺寸。/** List input directories. * Subclasses may override to, e.g., select only files matching a regular * expression. * * @param job the job to list input paths for * @return array of FileStatus objects * @throws IOException if zero items. */ protected List
listStatus(JobContext job ) throws IOException { List result = new ArrayList (); Path[] dirs = getInputPaths(job); if (dirs.length == 0) { throw new IOException("No input paths specified in job"); } // get tokens for all the required FileSystems.. TokenCache.obtainTokensForNamenodes(job.getCredentials(), dirs, job.getConfiguration()); // Whether we need to recursive look into the directory structure boolean recursive = getInputDirRecursive(job); List errors = new ArrayList (); // creates a MultiPathFilter with the hiddenFileFilter and the // user provided one (if any). List filters = new ArrayList (); filters.add(hiddenFileFilter); PathFilter jobFilter = getInputPathFilter(job); if (jobFilter != null) { filters.add(jobFilter); } PathFilter inputFilter = new MultiPathFilter(filters); for (int i=0; i < dirs.length; ++i) { Path p = dirs[i]; FileSystem fs = p.getFileSystem(job.getConfiguration()); FileStatus[] matches = fs.globStatus(p, inputFilter); if (matches == null) { errors.add(new IOException("Input path does not exist: " + p)); } else if (matches.length == 0) { errors.add(new IOException("Input Pattern " + p + " matches 0 files")); } else { for (FileStatus globStat: matches) { if (globStat.isDirectory()) { RemoteIterator iter = fs.listLocatedStatus(globStat.getPath()); while (iter.hasNext()) { LocatedFileStatus stat = iter.next(); if (inputFilter.accept(stat.getPath())) { if (recursive && stat.isDirectory()) { addInputPathRecursively(result, fs, stat.getPath(), inputFilter); } else { result.add(stat); } } } } else { result.add(globStat); } } } } if (!errors.isEmpty()) { throw new InvalidInputException(errors); } LOG.info("Total input paths to process : " + result.size()); return result; }
4、protected int getBlockIndex(BlockLocation[] blkLocations, long offset), 由于组成文件的块的信息已经获得了,只需要根据offset来计算所在的那个块就行了。protected long computeSplitSize(long blockSize, long minSize, long maxSize) { return Math.max(minSize, Math.min(maxSize, blockSize)); }
5、通过源码的分析我们知道可以通过Configuration conf设置 “mapred.max.split.size”和“mapred.min.split.size”或者Job上设置maxInputSplitSize和minInputSplitSize的值进行调整InputSplit切片的大小最终达到调整map task数量的目的。protected int getBlockIndex(BlockLocation[] blkLocations, long offset) { for (int i = 0 ; i < blkLocations.length; i++) { // is the offset inside this block? if ((blkLocations[i].getOffset() <= offset) && (offset < blkLocations[i].getOffset() + blkLocations[i].getLength())){ return i; } } BlockLocation last = blkLocations[blkLocations.length -1]; long fileLength = last.getOffset() + last.getLength() -1; throw new IllegalArgumentException("Offset " + offset + " is outside of file (0.." + fileLength + ")"); }
实例
下面通过一个简单实例测试
三个文件mapperstest1-01.txt,mapperstest01-02.txt,mapperstest1-03.txt

1、没配置相关minSplitSize和maxSplitSize,即使用默认配置,splitSize=blockSize=128M。