SparkRDD并行度与分区算法源码研究

0 引 言

1 RDD并行度与分区

1.1 概念解释

默认情况下,Spark可以将一个作业切分多个任务后,发送给Executor节点并行计算,而分区数我们称之为并行度,并行度等于task总数,但task数并不等于某一时刻可以同时并行计算的任务数。这个数量可以在构建RDD时指定。

1.2 读取内存时数据并行度与分区算法

1.2.1 读取内存数据并行度算法

makeRDD的源码

  def makeRDD[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
    parallelize(seq, numSlices)
  }

从makeRDD的源码可以看出makeRDD底层调用的是parallelize(seq, numSlices),也就是说makeRDD是对parallelize的封装。从源码中可以知道makeRDD需要传入两个参数,一个是从内存中创建的数据源(序列),一个为numSlices,需要你传入的并行度(分片数),默认为numSlices: Int = defaultParallelism(默认并行度)。

默认RDD的分区数就是并行度,设置并行度就是设置分区数。通过makeRDD的第二个参数就可以修改并行度,那么默认并行度是多少呢?数据分区的核心规则是什么呢?

点进去查看源码:

(1)在makeRDD位置处点击进入查看源码

进入后代码如下:

(2)继续进入查看

发现为抽象方法,抽象方法必有实现类:idea查看实现类的方法 ,crtl + alt + b

(3) 查找结果如下

 

(5)最终找到源码如下

tips:idea中查找实现类的方法crtl + alt + b.找到后ctrl + f 查找该方法

最终核心源码如下:

  override def defaultParallelism(): Int =
    scheduler.conf.getInt("spark.default.parallelism", totalCores)

由源码可以看出如果获取不到指定的参数("spark.default.parallelism")会采用默认值totalCores,totalCores为当前环境下可用的机器总核数。 什么 意思呢?该核数是由下面代码设置的参数决定的

val conf = new SparkConf().setAppName("word count").setMaster("local") 

具体之间关系如下图所示:

其中 setMaster()中配置的参数为当前环境下可用的核数。当前环境包括windows环境及linux环境等等

  • 当为loacal的时候表示只有一个核
  • 当为local[*]的时候表示机器中所有的核数
  • 当为local[3]表示使用3个核数,可以由该参数指定。

具体示例代码如下

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}

object testMakeRDD {

    def main(args: Array[String]): Unit = {

        //TODO spark从内存中创建RDD
        //默认RDD分区的数量就是并行度,设定并行度就是设定分区数量,但分区数量不一定就是并行度
        //资源不够的情况下分区数与并行度是不等的
        val conf = new SparkConf().setAppName("word count").setMaster("local[*]") //单核数
        //创建spark上下文
        val sc = new SparkContext(conf)
        //1.makeRDD的第一个参数:数据源
        //2.makeRDD的第二个参数:并行度(分区数量)
        val rdd: RDD[Int] = sc.makeRDD(List(1,2,3,4))
        //不传参数的话默认分区数是多少?
//        println(rdd.collect().mkString(","))
//        将RDD的处理数据保存到文件中
        rdd.saveAsTextFile("output")
        sc.stop()
    }

}

代码执行结果如图所示,由于采用的是本地windows环境有八个核,因而生成8个文件

 

1.2.2 读取内存数据分区算法

分区算法指的是:数据最终被分配到哪个分区中计算。一个分区生成一个文件

1 测试

object testFenQu {

    def main(args: Array[String]): Unit = {

        //TODO spark从内存中创建RDD

        val conf = new SparkConf().setAppName("word count").setMaster("local[*]")
        //创建spark上下文
        val sc = new SparkContext(conf)
//        val rdd1: RDD[Int] = sc.makeRDD(List(1,2,3,4),2)
//        rdd1.saveAsTextFile("output")

//        val rdd2: RDD[Int] = sc.makeRDD(List(1,2,3,4),4)
//        rdd2.saveAsTextFile("output")

//        val rdd3: RDD[Int] = sc.makeRDD(List(1,2,3,4),3)
 //       rdd3.saveAsTextFile("output")


        val rdd4: RDD[Int] = sc.makeRDD(List(1,2,3,4,5),3)
        rdd4.saveAsTextFile("output")
        sc.stop()
    }



}

执行上述代码RDD4为例,其结果如下:

由代码可知有3个分区,则产生3个文件

打开文件part-00000,发现只有1。
打开文件part-00001,发现2,3。
打开文件part-00002,发现4,5。

为什么是这样的呢?

2 查看源码

 (1)点makeRDD查看源码

 (2)点击parallelize(seq, numSlices)进一步查看

(3) 步骤3

(4) 步骤4

最终得到数据分区代码如下:

private object ParallelCollectionRDD {
  /**
   * Slice a collection into numSlices sub-collections. One extra thing we do here is to treat Range
   * collections specially, encoding the slices as other Ranges to minimize memory cost. This makes
   * it efficient to run Spark over RDDs representing large sets of numbers. And if the collection
   * is an inclusive Range, we use inclusive range for the last slice.
   */
  def slice[T: ClassTag](seq: Seq[T], numSlices: Int): Seq[Seq[T]] = {
    if (numSlices < 1) {
      throw new IllegalArgumentException("Positive number of partitions required")
    }
    // Sequences need to be sliced at the same set of index positions for operations
    // like RDD.zip() to behave as expected
    def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {
      (0 until numSlices).iterator.map { i =>
        val start = ((i * length) / numSlices).toInt
        val end = (((i + 1) * length) / numSlices).toInt
        (start, end)
      }
    }
    seq match {
      case r: Range =>
        positions(r.length, numSlices).zipWithIndex.map { case ((start, end), index) =>
          // If the range is inclusive, use inclusive range for the last slice
          if (r.isInclusive && index == numSlices - 1) {
            new Range.Inclusive(r.start + start * r.step, r.end, r.step)
          }
          else {
            new Range(r.start + start * r.step, r.start + end * r.step, r.step)
          }
        }.toSeq.asInstanceOf[Seq[Seq[T]]]
      case nr: NumericRange[_] =>
        // For ranges of Long, Double, BigInteger, etc
        val slices = new ArrayBuffer[Seq[T]](numSlices)
        var r = nr
        for ((start, end) <- positions(nr.length, numSlices)) {
          val sliceSize = end - start
          slices += r.take(sliceSize).asInstanceOf[Seq[T]]
          r = r.drop(sliceSize)
        }
        slices
      case _ =>
        val array = seq.toArray // To prevent O(n^2) operations for List etc
        positions(array.length, numSlices).map { case (start, end) =>
            array.slice(start, end).toSeq
        }.toSeq
    }
  }
}

通过分析上述代码可知,对于内存中创建的数据最终走的是case _这段代码,这段代码调用的方法为:

传入的参数为数组的长度及分片数

def positions(length: Long, numSlices: Int): Iterator[(Int, Int)] = {

  (0 until numSlices).iterator.map { i =>

    val start = ((i * length) / numSlices).toInt

    val end = (((i + 1) * length) / numSlices).toInt

    (start, end) //返回数据的偏移量左闭右开

  }

}


-------------------------------------

case _ =>
        val array = seq.toArray // To prevent O(n^2) operations for List etc
        positions(array.length, numSlices).map { case (start, end) =>
            array.slice(start, end).toSeq
        }.toSeq

代码解释如下:

举例:

 注意:until是左闭合右开的

总结:内存中数据能够被整除的时候基本上是平均分配,如果不能被整除则按照一定算法进行分配

1.3 读取文件时数据分区算法

读取文件数据时,数据是按照Hadoop文件读取的规则进行切片分区,而切片规则和数据读取的规则有些差异,具体Spark核心源码如下

public InputSplit[] getSplits(JobConf job, int numSplits)

    throws IOException {


    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);

     

    ...

   

    for (FileStatus file: files) {

   

        ...

   

    if (isSplitable(fs, path)) {

          long blockSize = file.getBlockSize();

          long splitSize = computeSplitSize(goalSize, minSize, blockSize);


          ...


  }

  protected long computeSplitSize(long goalSize, long minSize,

                                       long blockSize) {

    return Math.max(minSize, Math.min(goalSize, blockSize));

  }

 

分区测试

object partition03_file {

    def main(args: Array[String]): Unit = {
        val conf: SparkConf = new SparkConf().setMaster("local[*]").setAppName("SparkCoreTest1")
        val sc: SparkContext = new SparkContext(conf)

        //1)默认分区的数量:默认取值为当前核数和2的最小值
        //val rdd: RDD[String] = sc.textFile("input")

        //2)输入数据1-4,每行一个数字;输出:0=>{1、2} 1=>{3} 2=>{4} 3=>{空}
        //val rdd: RDD[String] = sc.textFile("input/3.txt",3)

        //3)输入数据1-4,一共一行;输出:0=>{1234} 1=>{空} 2=>{空} 3=>{空} 
        val rdd: RDD[String] = sc.textFile("input/4.txt",3)

        rdd.saveAsTextFile("output")

        sc.stop()
    }
}

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值