Spark源码解读由浅入深 宽窄依赖篇

第一部分 宽窄依赖篇

1.依赖关系概述

1.1 依赖关系、血缘关系: 在 Spark 中,RDD 分区的数据不支持修改,是只读的。如果想更新 RDD 分区中的数据,那么只能对原有 RDD 进行转化操作,也就是在原来 RDD 基础上创建一个新的RDD。新的RDD依赖于旧的RDD,相领的两个RDD的关系成为依赖关系多个连续的RDD的依赖关系,称为血缘关系

1.2 血缘关系保存: 在计算的过程当中,RDD不会保存数据,为了提高容错性,RDD将自己与之前旧RDD血缘关系保存下来,当计算出现错误时,可以根据血缘关系将数据源重新读取进行计算。

1.3 查看方法: 我们可以调用RDD的相关方法查看对应的依赖血缘关系

    rdd.todebugstring:打印血缘关系
    rdd.dependencies:打印依赖关系

2.依赖分类

查看Spark源码中的Dependency.scala文件,其中包含了Dependency,NarrowDependency、OneToOneDependency、RangeDependency、ShuffleDependency.

总体而言,Spark的依赖关系可以分为NarrowDependency窄依赖和ShuffleDependenc宽依赖,因为其中OneToOneDependency和RangeDependency都继承了窄依赖。
我们可以这样认为:

(1)窄依赖:每个parent RDD 的 partition 最多被 child RDD 的一个partition 使用。

(2)宽依赖:每个parent RDD partition 被多个 child RDD 的partition 使用。

2.1Dependency继承 Serializable

源码如下,可以看出,Dependency 只是依赖的一个基类,继承了可序列化接口,并且是一个抽象类。

/**
* :: DeveloperApi ::
* Base class for dependencies.
*/
@DeveloperApi
abstract class Dependency[T] extends Serializable {
  def rdd: RDD[T]
}
2.2 NarrowDependency 窄依赖,继承 Dependency

相关算子:filter、map、flatMap、sample、union、intersection、mapPartitions、mapPartitionsWithIndex、coalesce、zip

NarrowDependency也是一个抽象类,在官方文档中 NarrowDependency 的描述为:

/**
* :: DeveloperApi ::
* Base class for dependencies where each partition of the child RDD depends on a small number
* of partitions of the parent RDD. Narrow dependencies allow for pipelined execution.
*/

可以理解为NarrowDependency 窄依赖是一个子RDD的每个分区依赖于父 RDD 的少量分区的依赖关系的基类。窄依赖关系允许任务如同一条流水线执行,不用等待其他分区任务执行完毕。

源码如下
定义了抽象方法getParents用来获取子分区的父分区, partitionId 表示子 RDD 的一个分区,方法返回子分区所依赖的父 RDD 的分区。

@DeveloperApi
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  /**
   * Get the parent partitions for a child partition.
   * @param partitionId a partition of the child RDD
   * @return the partitions of the parent RDD that the child partition depends upon
   */
  def getParents(partitionId: Int): Seq[Int]

  override def rdd: RDD[T] = _rdd
}

窄依赖的具体实现OneToOneDependencyRangeDependency

2.3 OneToOneDependency 一对一依赖,继承NarrowDependency

官方文档中 OneToOneDependency 描述为:

/**
 * :: DeveloperApi ::
 * Represents a one-to-one dependency between partitions of the parent and child RDDs.
 */

可以理解为:父RDD和子RDD的分区之间只有一对一的依赖关系,并且 child RDD 的分区数和 parent RDD 的分区数相同。即父RDD的partiton最多被下游子RDD的一个partition使用,可以想象为独生子女。这种依赖关系对应的转换算子有map()、flatMap()、filter()等。

对应源码如下
该部分重写了Dependency的getParents方法,partitionId表示子 RDD 的一个分区。

@DeveloperApi
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}
2.4 RangeDependency 范围依赖,继承NarrowDependency

官方文档中 RangeDependency 描述为:

/**
 * :: DeveloperApi ::
 * Represents a one-to-one dependency between ranges of partitions in the parent and child RDDs.
 */

可以理解为:父 RDD 和子 RDD 中有分区范围之间的一对一依赖关系。

源码如下:rdd 表示父 RDD,inStart 表示父 RDD 中范围的开始,outStart 表示子 RDD 中范围的开始,length 表示范围的长度

@DeveloperApi
class RangeDependency[T](rdd: RDD[T], inStart: Int, outStart: Int, length: Int)
  extends NarrowDependency[T](rdd) {

  override def getParents(partitionId: Int): List[Int] = {
    if (partitionId >= outStart && partitionId < outStart + length) {
      List(partitionId - outStart + inStart)
    } else {
      Nil
    }
  }
}
2.5 ShuffleDependency 宽依赖,继承Dependency

sortBy、sortByKey、reduceByKey、join、leftOuterJoin、rightOuterJoin、fullOuterJoin、distinct、cogroup、repartition、groupByKey

官方文档中 ShuffleDependency 描述为:

/**
 * :: DeveloperApi ::
 * Represents a dependency on the output of a shuffle stage. Note that in the case of shuffle,
 * the RDD is transient since we don't need it on the executor side.
 */

可以理解为:ShuffleDependency表示对 shuffle 阶段的输出的依赖。在一般的shuffle 阶段都存在打乱重组这些过程,所以每个分区之间的任务执行是相互影响的,可以把每一个RDD的任务都看作一个阶段,同一个父RDD阶段的所有分区的任务都完成之后,才可以继续执行子RDD的任务。请注意,在 shuffle 的情况下,RDD 是瞬态的,因此我们在 executor 端不需要它。

源码如下
由于该部分源码过长,并且涉及到shuffle的知识,我分为几部分来解读。

@DeveloperApi
class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
    @transient private val _rdd: RDD[_ <: Product2[K, V]],
    val partitioner: Partitioner,
    val serializer: Serializer = SparkEnv.get.serializer,
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None,
    val mapSideCombine: Boolean = false,

_rdd 表示父RDD,partitioner 用于对 shuffle 输出进行分区。serializer如果未明确设置,则将使用由spark.serializer 配置选项指定的默认序列化程序。 keyOrdering 用于 RDD 的 shuffle 的键排序,aggregator 用于 RDD 的 shuffle 的 mapreduce-side 聚合器,mapSideCombine 表示是否执行部分聚合(也称为 map-side combine)。

从这里开始后面都是shuffleWriterProcessor,它是在 ShuffleMapTask 中控制写入行为的处理器。

    val shuffleWriterProcessor: ShuffleWriteProcessor = new ShuffleWriteProcessor)
  extends Dependency[Product2[K, V]] with Logging {

以下源码表示mapSideCombine默认为false,如果开启,需要指定aggregator聚合器,即用于 RDD 的 shuffle 的 mapreduce-side 聚合器。

  if (mapSideCombine) {
    require(aggregator.isDefined, "Map-side combine without Aggregator specified!")
  }

以下源码表示,shuffle都是基于PairRDD进行的,所以传入的RDD要是key-value类型的,于是ShuffleDependency重写了父类Dependency的rdd方法,把父类的RDD即_rdd转化为[RDD[Product2[K, V]]的对象实例后返回。

  override def rdd: RDD[Product2[K, V]] = _rdd.asInstanceOf[RDD[Product2[K, V]]]

以下源码表示,获取shuffleId,向shuffleManager注册shuffle信息。

  private[spark] val keyClassName: String = reflect.classTag[K].runtimeClass.getName
  private[spark] val valueClassName: String = reflect.classTag[V].runtimeClass.getName
  // Note: It's possible that the combiner class tag is null, if the combineByKey
  // methods in PairRDDFunctions are used instead of combineByKeyWithClassTag.
  private[spark] val combinerClassName: Option[String] =
    Option(reflect.classTag[C]).map(_.runtimeClass.getName)

  val shuffleId: Int = _rdd.context.newShuffleId()//获取shuffleId

  val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
    shuffleId, this)//向shuffleManager注册shuffle信息

以下源码表示,通过后面的canShuffleMergeBeEnabled()方法,判断是否允许 shuffle合并,默认情况下,如果启用了基于推送的 shuffle,则 ShuffleDependency 允许 shuffle 合并。

  private[this] val numPartitions = rdd.partitions.length

  private[this] var _shuffleMergeAllowed = canShuffleMergeBeEnabled()

  private[spark] def setShuffleMergeAllowed(shuffleMergeAllowed: Boolean): Unit = {	
    _shuffleMergeAllowed = shuffleMergeAllowed
  }

  def shuffleMergeEnabled : Boolean = shuffleMergeAllowed && mergerLocs.nonEmpty

  def shuffleMergeAllowed : Boolean = _shuffleMergeAllowed

以下源码表示存储所选外部 shuffle 服务列表的位置,以在此 shuffle map 阶段处理来自映射器的 shuffle 合并请求

  private[spark] var mergerLocs: Seq[BlockManagerId] = Nil

以下源码表示,存储有关与此 shuffle 依赖项关联的 shuffle map 阶段的 shuffle 合并是否已完成的信息

  private[this] var _shuffleMergeFinalized: Boolean = false

shuffleMergeId 用于唯一标识一次不确定的stage尝试shuffle的合并过程

  private[this] var _shuffleMergeId: Int = 0

  def shuffleMergeId: Int = _shuffleMergeId

  def setMergerLocs(mergerLocs: Seq[BlockManagerId]): Unit = {
    assert(shuffleMergeAllowed)
    this.mergerLocs = mergerLocs
  }

  def getMergerLocs: Seq[BlockManagerId] = mergerLocs

  private[spark] def markShuffleMergeFinalized(): Unit = {
    _shuffleMergeFinalized = true
  }

  private[spark] def isShuffleMergeFinalizedMarked: Boolean = {
    _shuffleMergeFinalized
  }

以下源码表示,如果基于推送的 shuffle 被禁用或者此 shuffle 的 shuffle 合并已完成,则返回 true

  def shuffleMergeFinalized: Boolean = {
    if (shuffleMergeEnabled) {
      isShuffleMergeFinalizedMarked
    } else {
      true
    }
  }

新的shuffle合并阶段

  def newShuffleMergeState(): Unit = {
    _shuffleMergeFinalized = false
    mergerLocs = Nil
    _shuffleMergeId += 1
    finalizeTask = None
    shufflePushCompleted.clear()
  }

方法canShuffleMergeBeEnabled():用于前面判断是否允许 shuffle合并

  private def canShuffleMergeBeEnabled(): Boolean = {
    val isPushShuffleEnabled = Utils.isPushBasedShuffleEnabled(rdd.sparkContext.getConf,
      // invoked at driver
      isDriver = true)
    if (isPushShuffleEnabled && rdd.isBarrier()) {
      logWarning("Push-based shuffle is currently not supported for barrier stages")
    }
    isPushShuffleEnabled && numPartitions > 0 &&
      // TODO: SPARK-35547: Push based shuffle is currently unsupported for Barrier stages
      !rdd.isBarrier()
  }

  @transient private[this] val shufflePushCompleted = new RoaringBitmap()

以下源码表示,在跟踪位图中将给定的map任务标记推送完成。使用位图可确保由于推测或阶段重试而多次启动的同一map任务仅计算一次mapIndex 表示映射任务索引,返回完成块推送的映射任务数

  /**
   * Mark a given map task as push completed in the tracking bitmap.
   * Using the bitmap ensures that the same map task launched multiple times due to
   * either speculation or stage retry is only counted once.
   * @param mapIndex Map task index
   * @return number of map tasks with block push completed
   */
  private[spark] def incPushCompleted(mapIndex: Int): Int = {
    shufflePushCompleted.add(mapIndex)
    shufflePushCompleted.getCardinality
  }

以下源码表示,表示仅由 DAGScheduler 用于协调 shuffle 合并完成

  @transient private[this] var finalizeTask: Option[ScheduledFuture[_]] = None

  private[spark] def getFinalizeTask: Option[ScheduledFuture[_]] = finalizeTask

  private[spark] def setFinalizeTask(task: ScheduledFuture[_]): Unit = {
    finalizeTask = Option(task)
  }

  _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
  _rdd.sparkContext.shuffleDriverComponents.registerShuffle(shuffleId)
}

从上面的分析,不难看出,在窄依赖中子 RDD 的每个分区数据的生成操作都是可以并行执行的,而在宽依赖中需要所有父 RDD 的Shuffle结果完成后再被执行。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值