一、RDD分区的含义

  • RDD 内部的数据集合在逻辑上和物理上被划分成多个子集合分布到集群的节点中,这样的每一个子集合我们将其称为分区(Partitions)
  • 分区个数的多少涉及对该RDD进行并行计算的粒度
  • spark会为每个分区起一个单独的任务进行计算,因此并行任务的个数,也是由分区的个数决定的
  • 分区是一个逻辑概念,变换前后的新旧分区在物理上可能是同一块内存或存储,这种优化防止函数式不变性导致的内存需求无限扩张

二、RDD分区的实现

我们先来看一下spark-core包下的Partition类,如下图所示:

spark stage 划分 spark的分区概念_scala


RDD 只是数据集的抽象,分区内部并不会存储具体的数据。Partition 类内包含一个 index 成员,表示该分区在 RDD 内的编号,通过 RDD 编号 + 分区编号可以唯一确定该分区对应的块编号,利用底层数据存储层提供的接口,就能从存储介质(如:HDFS、Memory)中提取出分区对应的数据。再来,我们看一下RDD的抽象类,如下图所示:

spark stage 划分 spark的分区概念_大数据_02


RDD抽象类中定义了_partitions 数组成员和 partitions 方法,partitions 方法提供给外部开发者调用,用于获取 RDD 的所有分区。partitions 方法会调用内部 getPartitions 接口,RDD 的子类需要自行实现 getPartitions 接口和Partition子类,这里我们拿ParallelCollectionRDD这个RDD子类作为例子:

spark stage 划分 spark的分区概念_scala_03


首先定义了ParallelCollectionPartition类继承Partition类,然后重写getPartitions方法:

spark stage 划分 spark的分区概念_spark_04

三、RDD分区的个数以及对spark性能的影响

  • 分区个数的获取
    通过partitions方法获取RDD划分的分区数
scala> val data = sc.textFile("/data/spark_rdd.txt")
  data: org.apache.spark.rdd.RDD[String] = /data/spark_rdd.txt MapPartitionsRDD[1] at textFile at <console>:24

  scala> data.partitions.size
  res0: Int = 2
  • 分区个数的指定
  • 用户在创建操作时手动指定分区的个数
scala> val data = sc.textFile("/data/spark_rdd.txt", 4)
  data: org.apache.spark.rdd.RDD[String] = /data/spark_rdd.txt MapPartitionsRDD[1] at textFile at <console>:24

  scala> data.partitions.size
  res0: Int = 4
  • 在没有指定分区个数的情况下,spark会根据集群部署模式,来确认分区个数默认值,分配的原则是尽可能使得分区的个数,等于集群核心数目
def parallelize[T: ClassTag](
      seq: Seq[T],
      numSlices: Int = defaultParallelism): RDD[T] = withScope {
      assertNotStopped()
      new ParallelCollectionRDD[T](this, seq, numSlices, Map[Int, Seq[String]]())
  }

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

  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)
  }
  def defaultMinPartitions: Int = math.min(defaultParallelism, 2)

通过上面SparkContext中一些RDD创建操作可以看出,默认情况下,分区的个数会受 Apache Spark 配置参数 spark.default.parallelism 的影响,官方对该参数的解释是用于控制 Shuffle 过程中默认使用的任务数量,这也符合我们之间对分区个数与任务个数之间关系的理解

对于本地模式,默认分区个数等于本地机器的 CPU 核心总数(或者是用户通过 local[N] 参数指定分配给 Apache Spark 的核心数目,见 LocalBackend 类),显然这样设置是合理的,因为把每个分区的计算任务交付给单个核心执行,能够保证最大的计算效率

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

其他集群模式(Standalone 或者 Yarn),默认分区个数等于集群中所有核心数目的总和,或者 2,取两者中的较大值(见 CoarseGrainedSchedulerBackend 类)

override def defaultParallelism(): Int = {
  conf.getInt("spark.default.parallelism", math.max(totalCoreCount.get(), 2))
}

如果RDD是用HDFS文件创建,默认分区个数为文件的数据块数

  • 分区个数对spark性能的影响

分区块越小,分区数量就会越多。分区数据就会分布在更多的worker节点上。但分区越多意味着处理分区的计算任务越多,太大的分区数量(任务数量)可能是导致Spark任务运行效率低下的原因之一。

所以,太大或太小的分区都有可能导致Spark任务执行效率低下。那么,应该如何设置RDD的分区?

Spark只能为RDD的每个分区运行1个并发任务,直到达到Spark集群的CPU数量。

所以,如果你有一个拥有50个CPU的Spark集群,那么你可以让RDD至少有50个分区(或者是CPU数量的2到3倍)。

一个比较好的分区数的值至少是executors的数量。可以通过参数设置RDD的默认分区数,也就是我们所说的并行度:sc.defaultParallelism

同样,RDD的action函数产生的输出文件数量,也是由分区的数量来决定的。

分区数量的上限,取决于executor的可用内存大小。

以上描述只是对非压缩文件适用,对于压缩文件不能在textFile中指定分区数,而是要进行repartition:

val rdd = sc.textFile('demo.gz') 
rdd.repartition(100)

四、自定义RDD分区

分区划分对于Shuffle类操作很关键,他决定了该操作的父RDD和子RDD之间的依赖类型。例如Join操作,如果协同划分的话,两个父RDD之间、父RDD与子RDD之间能形成一致的分区安排,即同一个key保证被映射到同一个分区,这样就形成窄依赖。反之,如果没有协同划分,就会形成宽依赖。这里所说的协同划分是指定分区划分以产生前后一致的分区安排。

在spark默认提供两种划分器:哈希分区划分器(HashPartitioner)和范围分区划分器(RangePartitioner),且Partitioner只存在(K, V)类型的RDD中,对于非(K, V)类型的Partitioner值为None,如下所示:

scala> val rdd = sc.textFile("/wordcount/word.txt")
rdd: org.apache.spark.rdd.RDD[String] = /wordcount/word.txt MapPartitionsRDD[29] at textFile at <console>:24

scala> rdd.partitioner
res25: Option[org.apache.spark.Partitioner] = None

scala> rdd.partitions
res26: Array[org.apache.spark.Partition] = Array(org.apache.spark.rdd.HadoopPartition@725)

scala> rdd.partitions.size
res27: Int = 1

scala> val rdd = sc.textFile("/wordcount/word.txt", 2)
rdd: org.apache.spark.rdd.RDD[String] = /wordcount/word.txt MapPartitionsRDD[31] at textFile at <console>:24

scala> rdd.partitioner
res28: Option[org.apache.spark.Partitioner] = None

scala> rdd.partitions.size
res29: Int = 2

scala> val newRDD = rdd.flatMap((_.split(" "))).map((_, 1)).reduceByKey((_ + _))
newRDD: org.apache.spark.rdd.RDD[(String, Int)] = ShuffledRDD[34] at reduceByKey at <console>:26

scala> newRDD.partitions
res30: Array[org.apache.spark.Partition] = Array(org.apache.spark.rdd.ShuffledRDDPartition@0, org.apache.spark.rdd.ShuffledRDDPartition@1)
  • HashPartitioner
  • 默认分区
class HashPartitioner(partitions: Int) extends Partitioner {
 require(partitions >= 0, s"Number of partitions ($partitions) cannot be negative.")

 def numPartitions: Int = partitions

 def getPartition(key: Any): Int = key match {
   case null => 0
   case _ => Utils.nonNegativeMod(key.hashCode, numPartitions)
 }

 override def equals(other: Any): Boolean = other match {
     case h: HashPartitioner =>
     h.numPartitions == numPartitions
     case _ =>
     false
 }

 override def hashCode: Int = numPartitions
 }

 def nonNegativeMod(x: Int, mod: Int): Int = {
   val rawMod = x % mod
   rawMod + (if (rawMod < 0) mod else 0)
 }
  • 原理
    对于给定的key,计算其hashCode,并除于分区的个数取余,如果余数小于0,则用余数+分区的个数,最后返回的值就是这个key所属的分区ID
  • 弊端
    可能导致每个分区中数据量的不均匀,极端情况下会导致某些分区拥有RDD的全部数据(HashCode为负数时,为了避免小于0,spark在nonNegativeMod方法里面做了处理)

2.RangePartitioner

  • 作用
    将一定范围内的数映射到某一个分区内,在实现中,分界的算法尤为重要。算法对应的函数是rangeBounds,所用到的算法是水塘抽样水塘抽样算法参照这里
  • 优点
    尽量保证每个分区中数据量的均匀,而且分区与分区之间是有序的,一个分区中的元素肯定都是比另一个分区内的元素小或者大,但是分区内的元素是不能保证顺序的。简单的说就是将一定范围内的数映射到某一个分区内。

由于源代码较长,这里就不贴代码了,有兴趣的可以查看Partitioner.scala

3.自定义分区
自定义实现分区只需要继承Partitioner类即可,如下所示:

import org.apache.spark.Partitioner

class  MySparkPartition(numParts: Int) extends Partitioner {

override def numPartitions: Int = numParts

override def getPartition(key: Any): Int = {
  val domain = new java.net.URL(key.toString).getHost()
  val code = (domain.hashCode % numPartitions)
  if (code < 0) {
    code + numPartitions
  } else {
    code
  }
}
override def equals(other: Any): Boolean = other match {
  case mypartition: MySparkPartition =>
    mypartition.numPartitions == numPartitions
  case _ =>
    false
}
override def hashCode: Int = numPartitions

}

numPartitions:这个方法需要返回你想要创建分区的个数
getPartition:这个函数需要对输入的key做计算,然后返回该key的分区ID,范围一定是0numPartitions - 1equals:用于Spark内部比较两个RDD的分区是否一样

通过partitionBy算子就可以将我们自定义的Partitioner用于RDD分区