前言:

        关于源码的文章,我自己其实也一直在有道云上有总结一些,但由于平日里上班的缘故,着实没有太多的精力来写体系的写这些东西,但是,确实觉得这些东西其实还是很重要的,特别是随着工作时间的渐长,越发觉得源码这个东西还是必须要看的,能带来很多的启发,我个人的体会是,每个工作阶段去解读都会有不一样的感受。

       我也不敢说去解读或者说让你彻底搞个明白,自己确实没有那个水平。我写博客一方面是为了自己日后回顾方便,另一方面也是希望能够以此会友,以后希望自己可以坚持做下去,这篇且作为我的源码第一篇,目前没有太明确的写作的规划,就想到哪里就写到哪里吧。

 updated on :2020-05-05

搞清楚这个原理其实对于很多地方都是有好处的,比如你的生产上hive调优里的如何调整map的数量,是很有帮助的。这里的原理和mr的基本上都是一致的。

直接写到最开端吧,这样时间比较紧张的朋友们可以直接看关键点,不必太拘泥于细节,不过笔者建议还是自己多去看几遍源码,多思考,多总结,最好是可以debug几次,这样会更加深刻的理解。

这里有几个关键点强调一下:

  1. 这里有三个关键的值:goalsize,minsize,blockSize
  •  goalsize 是理想的切分的尺寸,它是和我们指定的切分的数量,以及文件的总的长度有关,也就是文件的字节数,我们可以调节的是 numSplits 这个参数。默认值是 2
  • minsize 这个参数也是可以设置的 通过 mapreduce.input.fileinputformat.split.minsize 来设置,默认是1,和minSplitSize 取max,但是minSplitSize的默认值也是1,所以说如果你不设置mapreduce.input.fileinputformat.split.minsize的话,那么minsize 对应的值就是 1 
  • blockSize 这个参数现在都是128m默认的,也有比较豪横的256m的,根据自己的集群实际情况来看,笔者公司的是128m

他们三个擦出的火花在这里:  long splitSize = computeSplitSize(goalSize, minSize, blockSize);  这个方法其实就是求出了三个中的那个中间值。一般来说blocksize是已经定了,那就是去看 goalSize,minsize 你的设定了。【顺便多说一句,blocksize 在生产上一般不要动,(且hive里也不允许你去调整。)】,假如说你有个300m的文件,blocksize是128m,且minsize 你没有设定,让它默认的是1,那么splitsize就是128m了,也就是正好等于blockSize。

     2. 进行切分的时候 获取的是每个文件的大小,而不是所有的文件的大小,这个请注意了,否则有可能你做实验的时候会比较疑惑,这点在一种情形下会有影响,就是假如说你的目录下有10个文件,其中有一个文件的大小比其他的9个都要显著的大,这个时候你会发现切分之后得到的partitions的数量和你预期的有出入,会比你预期的要多一个分区。所以这点笔者在这里想强调一下

    3. 就是关于SPLIT_SLOP 这个很关键的点了,这个不仅和小文件有关,也会决定了你的文件切分的个数,因为从源码来看,如果说当你的文件的大小,小于splitsize * 1.1 的时候,这个时候就不会再被切分,而是直接被当作一个inputsplit

spark 读取textfile是如何决定分区数的???

       之所以写这个是因为当时去面试的时候,被面试官给问到了,当时只回答了一个大概,对很多的细节其实并不清楚,所以就决定分析一下。

我尝试了量两种方式,使用的是spark的2.0的版本 下面的分析是针对如下的情况来分析的。

spark.sparkContext.textFile("")

1:跟进源码我们发现注释是这样子的:

/**
   * Read a text file from HDFS, a local file system (available on all nodes), or any
   * Hadoop-supported file system URI, and return it as an RDD of Strings.
   */
   
  def textFile(
      path: String,
      minPartitions: Int = defaultMinPartitions): RDD[String] = withScope {
    assertNotStopped()
    hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
      minPartitions).map(pair => pair._2.toString).setName(path)
  }

我们可以看到这里的注释写的很清楚,读取的是hdfs上的或者是本地文件系统的,或者是任意的hadoop支持的文件系统,返回的是一个rdd

hadoopFile(path, classOf[TextInputFormat], classOf[LongWritable], classOf[Text],
      minPartitions).map(pair => pair._2.toString).setName(path)

可以看到返回的是一个hadoopfile经过map操作之后的结果。
我们继续往下走:

def hadoopFile[K, V](
    
     minPartitions: Int = defaultMinPartitions): RDD[(K, V)] = withScope {
   assertNotStopped()
   
   val setInputPathsFunc = (jobConf: JobConf) => FileInputFormat.setInputPaths(jobConf, path)
 =new HadoopRDD(
minPartitions).setName(path)

这里我只展示出来重点的代码。我们可以看到这里是new HadoopRDD
我们跟踪进入到 HadoopRDD 去看下这个分区数到到底是如何确定的。 锁定了获取分区的方法:

override def getPartitions: Array[Partition] = {
    val jobConf = getJobConf()
    // add the credentials here as this can be called before SparkContext initialized
    SparkHadoopUtil.get.addCredentials(jobConf)
    val inputFormat = getInputFormat(jobConf)
    
    //这个是关键,是如何获取inputSplit的,这个方法里传入了minPartitions,这个值的默认值是 2
   == val inputSplits = inputFormat.getSplits(jobConf, minPartitions)==
    val array = new Array[Partition](inputSplits.size)
    for (i <- 0 until inputSplits.size) {
      array(i) = new HadoopPartition(id, i, inputSplits(i))
    }
    array
  }

我们来重点的看下这个getSplits的源码:

/** 
   * Logically split the set of input files for the job.  
   * 
   *
   * <p><i>Note</i>: The split is a <i>logical</i> split of the inputs and the
   * input files are not physically split into chunks. For e.g. a split could
   * be <i><input-file-path, start, offset></i> tuple.
   * 
   * @param job job configuration.
   * @param numSplits the desired number of splits, a hint.
   * @return an array of {@link InputSplit}s for the job.
   */
  InputSplit[] getSplits(JobConf job, int numSplits) throws IOException;

==通过注释我们注意到:这个是一个逻辑上的划分,而并不是物理物理上的切分。 ==
split of the inputs and the input files are not physically split into chunks

这个就是这个inputsplit的本质。是一个逻辑上的概念

我们来看一个他的具体的实现:
他的是实现有很多我们拿出来一个我们很熟悉的来说:

FileInputFormat 这个类的
这个具体的还是挺长的,我只挑我关心的重点来看:

我们继续回到:

def getPartitions: Array[Partition]

最终返回的是一个Array[Partition]
我通常看rdd的分区数用到的一个api是:getNumPartitions
这个是rdd里的一个方法,我们来看下他的实现:

def getNumPartitions: Int = partitions.length
partitions----》def partitions: Array[Partition]//其实就是这个数组的长度
/** Splits files returned by {@link #listStatus(JobConf)} when
   * they're too big.*/ 
  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.now(TimeUnit.MILLISECONDS));
    }
    return splits.toArray(new FileSplit[splits.size()]);
  }
protected long computeSplitSize(long goalSize, long minSize,
                                     long blockSize) {
  return Math.max(minSize, Math.min(goalSize, blockSize));
}

2: 说了这么多该做一个总结的时候了

其实来说我们的分区数量的决定如果说文件时可分割的话,那么他就是取决于你的split的最终的个数,如果不可分割的话那就是一个走起。
分割的时候最关键的就是要确定切分的尺寸,这个是最终的一个决定的因素,有一个点我没有搞太懂:

((double) bytesRemaining)/splitSize > SPLIT_SLOP

这里的 SPLIT_SLOP 是什么意思,我看与源码里给的默认值是1.1 请指教

,先写到这里,都是纯理论的东西。

我已经知道了 SPLIT_SLOP 是什么意思了,这是一个很关键的操作,这个值是1.1,也就是说只有当剩余的文件大小/splitsize的1.1倍的时候,才继续进行切分,这样一个最大的好处就是防止小文件的出现。我们来举个例子:如果说剩余的大小是31 切分的尺寸是30 那么如果说不是1.1倍的话那么就会满足条件,做切分,那么剩下的1咋办,是不是就成了一个很小的文件了。