(一) Map输入数据块的切分算法(基于hadoop源码 1.0.1):

 (1)分片算法

    MapTask的个数据主要取决于InputFormat通过对输入数据调用getSplit()方法分割为若干个分片数据,即InputSplit数。

hadoop中切片大小主要由以下几个因素:

blockSize:块大小

minSize:最小分片大小,由参数mapred.min.split.size设置,默认为1

maxSize:最大分片大小,由参数mapred.max.split.size设置,默认Long.MAX-VALUE

   分片大小为:SplitSize=Math.max(minSize, Math.min(maxSize, blockSize));


/**
   * Generate the list of files and make them into FileSplits.
   */
  public List<InputSplit> getSplits(JobContext job
                                    ) throws IOException {
    long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
    long maxSize = getMaxSplitSize(job);
    // generate splits
    List<InputSplit> splits = new ArrayList<InputSplit>();
    List<FileStatus>files = listStatus(job);
    for (FileStatus file: files) {
      Path path = file.getPath();
      FileSystem fs = path.getFileSystem(job.getConfiguration());
      long length = file.getLen();
      BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);
      if ((length != 0) && 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(new FileSplit(path, length-bytesRemaining, splitSize,
                                   blkLocations[blkIndex].getHosts()));
          bytesRemaining -= splitSize;
        }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
        if (bytesRemaining != 0) {
          splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining,
                     blkLocations[blkLocations.length-1].getHosts()));
        }
      } else if (length != 0) {
        splits.add(new FileSplit(path, 0, length, blkLocations[0].getHosts()));
      } else {
        //Create empty hosts array for zero length files
        splits.add(new FileSplit(path, 0, length, new String[0]));
      }
    }
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
    // Save the number of input files in the job-conf
    job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
    LOG.debug("Total # of splits: " + splits.size());
    return splits;
  }

 我们看上面代码的第18行,computeSplitSize(blockSize, minSize, maxSize)方法,进入瞧瞧:


protected long computeSplitSize(long blockSize, long minSize,
                                long maxSize) {
  return Math.max(minSize, Math.min(maxSize, blockSize));
}

 minSize、maxSize、blockSize和上面说明的是一致的,到这里切片的大小计算就相当清楚了。另外,hadoop还维护了一个相当重要的数据结构用于保存每一个map输入文件的切片信息(FileSplit类),其结构:


/** A section of an input file.  Returned by {@link
 * InputFormat#getSplits(JobContext)} and passed to
 * {@link InputFormat#createRecordReader(InputSplit,TaskAttemptContext)}. */
public class FileSplit extends InputSplit implements Writable {
  private Path file;//InputSplit所在的文件
  private long start;//起始位置
  private long length;//InputSplit长度
  private String[] hosts;//inputSplit所在节点列表(此属性关乎到任务的本地性)
            .
            .
            .
}


(二) host选择算法以及任务本地性分析

在InputSplit的切分算法确定,接下来确定每个InputSplit的元数据信息。元数据信息主要由4部分组成:

<file,start,length,hosts>,也就是上面的FileSplit对应的结构,其实hadoop在提交作业的时候会将FileSplit的元数据信息映射为2部分,分别是InputSplit元数据信息和原始InputSplit信息。其中元数据信息被JobTracker用于生成Task本地性相关数据结构,而原始InputSplit信息是在Map Task初始化的时候使用的,以被map获取到自己要进行处理的数据,他们分别对应的文件为jobMetainfo和job.split。在50070页面中我们可以看到(${mapreduce.jobtracker.staging.root.dir}/${user}/.staging/${jobId}):

hadoop输入分片 hadoop分片策略_大数据

继续InputSplit结构,前3项非常容易确定,但是hosts的选择策略就没有那么容易了,它关系到任务的本地性问题能够直接地影响作业的运行效率和网络的消耗。

   由于Hadoop在HDFS中上的文件时以block为单位的,那么一个大文件在HDFS上可能对应的多个block分散遍布各个节点上,而从hadoop的文件切片算法来看,一个InputSplit可能对应着多个block分布在不同的节点上(包含block副本),因此hadoop不可能实现完全的数据本地性。所以hadoop按照本地性的高低划分为3个等级:node locality、rack locality和dataCenter locality(hadoop暂时没有实现)。

   InputSplit对应的block可能分布在不同的节点上,那么是否应该将所有block所有的节点都放到hosts列表中,作为任务本地性的判断标准呢?举个例子,例如有一个切片P1,其对应着5个block分布在集群中的5个节点中。其中5个节点包含该切片的数据量分别是:500(slave1)、400(slave2)、100(salve3)、50(slave4)、50(slave5)(去掉重复的副本),如果将这5个节点都放到hosts列表作为任务本地性的判断标准的话,那么可能会出现这样的情况:当slave5有空闲的slot时,通过心跳包发送给jobTracker,请求任务分配。由于slave5的这个InputSplit的列表中,因此jobTracker将salve5视为该InputSplit的本地节点,创建Map task任务。很明显可以看出slave5只包含了改InputSplit 9%左右的数据量,其他91%的数据需要从其他节点中下载,本性性的效率十分低下。

   因此,hadoop考虑到这种任务调度的效率问题,不会将InputSplit包含的block所在的所有节点放到host列表中,而是选择包含改InputSplit数据总量(统计时去掉同一个节点重复的数据)最大的前几个节点(限制最多选择10个,多余的过滤掉)作为任务调度时的本地性判断标准FileInputFormate设计了一个有效的算法:先按照rack包含的数据量对rack进行排序,然后在rack内部按照每个node包含的数据量(统计时去掉同一个节点重复的数据)对node进行排序,最后取前N个node的host作为InputSplit的host列表,这里的N为block副本数。这样,当任务调度器调度Task时,只要将Task调度给位于host列表的节点就可以实现最大效率的本地性。

   【实例】hadoop集群的网络拓扑结构如下图所示,HDFS中block副本数为3,某个InputSplit包含3个block,大小依次为100、150和75,那么很容易统计出4个rack包含该InputSplit的数据量为:rack2[250]>rack1[175]>rack3[150]>rack4[75](统计时去掉同

一个节点重复的数据),其rack内部节点包含的数据量为:rack2[node4(150)>node3(100)]>rack1[node1(175)>node2(100)]>rack3[node5(150)=node6(150)]>rack4[node7(75)=node8(75),那么依次选择的3个(block副本数)节点为node4、node3和node1作为改InputSplit的host列表。


hadoop输入分片 hadoop分片策略_hadoop_02


    根据上面的分析,我们可以得出一个结论:当InputSplit尺寸大于block并且其对应的所有block(包含副本)不在同一个节点上时,Map Task不可能完全实现数据的本地化,也就是说,总有一部分数据需要从远程节点上读取,因此得出,当使用基于FileInputFormat实现InputFormat时,为了提高数据本地性,应该尽量使InputSplit大小与block大小一致。

(三) 任务本地性和多用户调度器联系

多任务调度器Capacity Scheduler和Fair Scheduler在任务调度时,会根据任务的本地性而选择相应去延迟调度任务。

   (a)Capacity Scheduler 采用的作业任务延迟调度策略:

    当选择一个作业后,如果在该作业中没有找到满足本地性要求的任务,那么Capacity Scheduler调度器会让该作业跳过一定数目的调度机会,直到找到满足本地性要求的任务或者达到跳过次数上限(requiredSlots*localityWaitFactor),其中localityWaitFactor可以通过参数

mapreduce.job.locality.wait.factor配置,默认情况下localityWaitFactor=min{jobNodes/clusterNodes,1},其中

                jobNodes表示:该作业输入数据所在节点总数。

                clusterNodes表示:集群节点总数。

requiredSlots=min{(numMapTasks-finishedMapTask),numTaskTrackers},其中

                numMapTasks表示:该作业的MapTask数目

                finishedMapTask表示:改作业已经完成的MapTask数目

                numTaskTrackers表示:taskTracker数目


(b)Fair Scheduler 采用的作业任务延迟调度策略:

    当出现一个空闲slot时,如果选中的作业中没有node-local或者rack-local任务,则暂时把资源让给其他作业,直到

找到一个满足数据本地性的任务或者达到一个时间阀值,此时不得不选在一个非本地性的任务执行。