源碼走讀篇之:spark讀取textfile時是如何決定分區數的

前言:

        關於源碼的文章,我自己其實也一直在有道雲上有總結一些,猶豫平日裏上班的緣故,着實沒有太多的精力來寫體系的寫這些東西,但是,卻着實覺得這些東西其實還是很重要的,特別是隨着工作時間的漸長,越發覺得源碼這個東西還是必須要看的,能帶來很多的啓發,我個人的體會是,每個工作階段去解讀都會有不一樣的感受。

       我也不敢說去解讀或者說讓你徹底搞個明白,自己確實沒有那個水平。我寫博客一方面是爲了自己日後回顧方便,另一方面也是希望能夠以此會友,以後希望自己可以堅持做下去,這篇且作爲我的源碼第一篇,目前沒有太明確的寫作的規劃,就想到哪裏就寫到哪裏吧。

 

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>&lt;input-file-path, start, offset&gt;</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
這個具體的還是挺長的,我只挑我關心的重點來看:

 /** Splits files returned by {@link #listStatus(JobConf)} when
   * they're too big.*/ 
   
   //註釋裏說明了它的作用是來切分過大的file的
   
  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);
    
    ==//計算總的大小,這裏是一個初始值是0==
    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();
    }

// goalsize理想的分割尺寸,這裏默認的numSplits就是2 如果說我們設置了值的花那麼就是我們設置的值
    long goalSize = totalSize / (numSplits == 0 ? 1 : numSplits);
    
    //這裏有個SPLIT_MINSIZE 最小的切分尺寸。
   /* public static final String SPLIT_MINSIZE = 
    "mapreduce.input.fileinputformat.split.minsize";
    minSplitSize 的默認值是1,這個是我們自己可以設置的
    */
    //我們沒有去設置minSplitSize所以說那就是1,因爲他兩個的默認值都是1
    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)) {
        
        //這個在生產上看你是如何設置的了,windows本地測試話這個值是32m,我們生產上是128m
          long blockSize = file.getBlockSize();
          
          //計算切分的尺寸
          /*
          return Math.max(minSize, Math.min(goalSize, blockSize));
          從goalSize和blockSize中取出小的那個,然後和minSize取最大值
          */
          //這個操作最終其實就是拿到 這三個裏的 中間的那個值就對了,在生產上很多時候goalsize是大於blocksize 的,所以說一般來說都是按照blockSize來切分的,讓我們造成了一個錯覺,以爲文件的個數決定了我們的並行度,其實真的不是,當文件的數量尺寸小的時候,這個時候中間的那個值就變成了goalSize
          long splitSize = computeSplitSize(goalSize, minSize, blockSize);

          long bytesRemaining = length;
          //這裏又是一處重點:判斷文件的尺寸和splitSize的1.1倍的關係,但是切分的時候是按照splitSize的大小來切分的
          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;
          }

// 當剩下的文件的大小無法滿足上面的while循環的時候,就會進入這個判斷,剩下的那個小部分數據就直接被添加到了splits數組裏了。這個文件有可能就被切分成了多個split,也就意味着會有不止一個任務來處理這個文件,所以在都是小文件很多的時候你會發現,明明是7個文件,但是有一個文件的尺寸比別的都明顯大的時候,就會出現這種情況,並行度並不是文件的個數,而是會多出來一些分區數,就是這個原因。
          if (bytesRemaining != 0) {
            String[][] splitHosts = getSplitHostsAndCachedHosts(blkLocations, length
                - bytesRemaining, bytesRemaining, clusterMap);
            splits.add(makeSplit(path, length - bytesRemaining, bytesRemaining,
                splitHosts[0], splitHosts[1]));
          }
        } 
        
        //這裏如果說不可以被切分的話那麼就是直接成爲一個split
        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()]);
  }

我們繼續回到:

def getPartitions: Array[Partition]

最終返回的是一個Array[Partition]
我通常看rdd的分區數用到的一個api是:getNumPartitions
這個是rdd裏的一個方法,我們來看下他的實現:

def getNumPartitions: Int = partitions.length
partitions----》def partitions: Array[Partition]//其實就是這個數組的長度

2: 說了這麼多該做一個總結的時候了

其實來說我們的分區數量的決定如果說文件時可分割的話,那麼他就是取決於你的split的最終的個數,如果不可分割的話那就是一個走起。
分割的時候最關鍵的就是要確定切分的尺寸,這個是最終的一個決定的因素,有一個點我沒有搞太懂:

((double) bytesRemaining)/splitSize > SPLIT_SLOP  

這裏的 SPLIT_SLOP 是什麼意思,我看與源碼裏給的默認值是1.1 請指教

,先寫到這裏,都是純理論的東西。

我已經知道了 SPLIT_SLOP 是什麼意思了,這是一個很關鍵的操作,這個值是1.1,也就是說只有當剩餘的文件大小/splitsize的1.1倍的時候,才繼續進行切分,這樣一個最大的好處就是防止小文件的出現。我們來舉個例子:如果說剩餘的大小是31 切分的尺寸是30 那麼如果說不是1.1倍的話那麼就會滿足條件,做切分,那麼剩下的1咋辦,是不是就成了一個很小的文件了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章