Spark RDD之Dependency

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_34842671/article/details/83476225

概述

Partition是数据切分的逻辑,而Dependency是在Transformation过程中Partition的演化过程,根据Dependency的类型判断数据的处理方式,Dependency可以分为NarrowDependency(窄依赖)和ShuffleDependency(宽依赖)。Dependency是一个抽象类,只有一个属性RDD,该RDD为对应RDD的父RDD,因此Dependency是对父RDD的包装,Dependency的基类如下:

abstract class Dependency[T] extends Serializable {
  def rdd: RDD[T]
}

窄依赖

窄依赖是父RDD的partition只被子RDD的一个partition引用,允许流水线执行。窄依赖的每个父RDD的分区只会传入到一个子RDD分区中,通常可以在一个节点内完成转换,即子RDD的分区放在该子RDD分区的父RDD分区的同个节点。NarrowDependency依然是一个抽象类,其中partitionId为子RDD的分区Id,并增加了getParents方法,定义如下:

abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  def getParents(partitionId: Int): Seq[Int]

  override def rdd: RDD[T] = _rdd
}

窄依赖主要有如下三种具体实现:

  1. OneToOneDependency
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}
  1. RangeDependency
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
    }
  }
}
  1. PruneDependency
private[spark] class PruneDependency[T](rdd: RDD[T], partitionFilterFunc: Int => Boolean)
  extends NarrowDependency[T](rdd) {

  @transient
  val partitions: Array[Partition] = rdd.partitions
    .filter(s => partitionFilterFunc(s.index)).zipWithIndex
    .map { case(split, idx) => new PartitionPruningRDDPartition(idx, split) : Partition }

  override def getParents(partitionId: Int): List[Int] = {
    List(partitions(partitionId).asInstanceOf[PartitionPruningRDDPartition].parentSplit.index)
  }
}

Dependency中的核心方法为getParents。OneToOneDependency和RangeDependency都是一对一的,较容易理解。PruningDependency子RDD的partition中,包含不止一个父RDD的partition,子RDD获取依赖的方法getDependencies可以获取多个dependency,而dependency中包含rdd,getParents方法是Dependency中重写的方法,因此getParents是在已经确定RDD的情况下,根据子RDD的partitionId获取父RDD中partitionId的行为,表征partition的数据流向和处理方式,PartitionId是针对某个RDD中的分区序号。

  • OneToOneDependency:父RDD的partitionId和子RDD的partitionId是一致的,只是存在于不同的RDD中。
  • RangeDependency:inStart是父RDD开始的下标,outStart是子RDD开始的下标,length是partition的个数。这种依赖的典型是Union操作,将两个RDD合并成一个RDD。父RDD的partitionId根据以下公式进行计算:子RDD的partitionId-子RDD的开始下标+父RDD的开始下标(inStart存在意义的为:同一个RDD中的partition可能存在不同的节点上,对于一个节点来说,partition是有偏移量的,而不是从0开始的)。
  • PruningDependency:较为复杂,子RDD中的partition会从不止一个父RDD的partition中获取数据,这种依赖的典型操作是filterByRange操作。该Dependency定义了一个partitions数组,该数组是通过对父RDD进行了自定义的过滤操作后重新排序获得的,同时根据序号和partition重新new了Partition的子类PartitionPruningRDDPartition并返回,因此partitions中包含着过滤后的父RDD中的partitions和子RDD中重组的partitionId,而partition中有index属性,getParents方法便是返回了该属性以获得父RDD的partitionId,其中作为参数传入的partitionId(子RDD中partition的Id号)的个数,恰好是过滤后剩余partition的个数。

宽依赖

宽依赖是父RDD的partition被子RDD的多个partition引用,需要在运行过程中将同一个父RDD的分区传入到不同的子RDD分区中,中间可能涉及多个节点之间的数据传输。

Shuffle设计到网络传输,所以要有序列化serializer,为了减少网络传输,可以加map端聚合,通过mapSideCombine和aggregator控制,还有key排序相关的keyOrdering,以及重输出的数据如何分区的partitioner,其他信息包括k,v和combiner的class信息以及shuffleId。shuffle是个相对复杂且开销大的过程,Partition之间的关系在shuffle处停止,因此shuffle是划分stage的依据。

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)
  extends Dependency[Product2[K, V]] {

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

  private[spark] val keyClassName: String = reflect.classTag[K].runtimeClass.getName
  private[spark] val valueClassName: String = reflect.classTag[V].runtimeClass.getName
  
  private[spark] val combinerClassName: Option[String] =
    Option(reflect.classTag[C]).map(_.runtimeClass.getName)

  val shuffleId: Int = _rdd.context.newShuffleId()

  val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
    shuffleId, _rdd.partitions.length, this)

  _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}

当RDD分区丢失时(某个节点故障),spark会对数据进行重算:

  1. 对于窄依赖,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重算和子RDD分区对应的父RDD分区即可,所以这个重算对数据的利用率是100%的;
  2. 对于宽依赖,重算的父RDD分区对应多个子RDD分区,这样实际上父RDD 中只有一部分的数据是被用于恢复这个丢失的子RDD分区的,另一部分对应子RDD的其它未丢失分区,这就造成了多余的计算;更一般的,宽依赖中子RDD分区通常来自多个父RDD分区,极端情况下,所有的父RDD分区都要进行重新计算。

猜你喜欢

转载自blog.csdn.net/qq_34842671/article/details/83476225