Spark源码分析系列之RDD基础:Dependency

概述

​ RDD实现了基于Lineage的容错机制,不同RDD的依赖关系构成了计算链,当某个RDD出现错误时候,可以通过依赖关系进行重算。那么spark的依赖关系是如何划分的,以及是如何进行依赖关系记录的,本文通过分析源码一探究竟。

Dependency及其划分

​ spark中定义在org.apache.spark.Dependency中,是个抽象类,只包含一个rdd,是它依赖的parentRDD。

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

​ spark的依赖关系分为两种:宽依赖(narrow dependency)和窄依赖(wide dependency),不同依赖关系继承于Dependency基类。不同依赖如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8WV2hBZN-1571624174952)(/Users/lidongmeng/Library/Application Support/typora-user-images/image-20191018145840026.png)]

窄依赖是指每个parent的每个partition最多被一个子RDD的一个partition使用;宽依赖是指parent RDD的每个分区都有可能被多个子RDD分区使用。他们有以下的区别:

  1. 宽依赖由于parent rdd的同一个分区需要一个或者多个子rdd处理,所以需要将同一个RDD分区的数据传入到不同的RDD分区中,中间可能涉及到多个节点之间数据的传输,对应着shuffle操作,运行时间会较长;而窄依赖的每个父RDD分区通常只会传入到另一个子RDD分区,通常在一个节点内完成。
  2. 在RDD分区丢失时,对于窄依赖来说,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重新计算与子RDD分区对应的父RDD分区就行,所以对数据的利用是100%的;而对于宽依赖来说,重算的父RDD分区只有一部分数据是对应丢失的子RDD分区的,另一部分就造成了多余的计算,宽依赖中的子RDD分区通常来自多个父RDD分区,极端情况下,所有父RDD都有可能重新计算。
  3. 当RDD分区丢失时,对于宽依赖来说,重算的父RDD分区只有一部分数据是对应丢失的子RDD分区的,另一部分就造成了多余的计算。宽依赖中的子RDD分区通常来自多个父RDD分区,极端情况下,所有父RDD都有可能重新计算。

Dependency一个很重要的要求是,子RDD可以为其每个partition根据dependency找到它所对应的父RDD的partition,或者是找到计算的数据来源,所以每个实现的Dependency都要提供根据partitionID获取parentRDD的partition的方法。

窄依赖

窄依赖定义&继承关系

​ NarrowDependency中子RDD的每个分区依赖少量(一个或多个)parent RDD分区,即parent RDD的partition至多被子RDD的某个partition使用一次

​ 窄依赖继承于Dependency并定义了一个获取parent rdd的方法,窄依赖有两个子类: OneToOneDependency和RangeDependency。

abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  // 得到子RDD的某个Partition所依赖的parent rdd的partitions集合
  def getParents(partitionId: Int): Seq[Int]

  override def rdd: RDD[T] = _rdd
}
OneToOneDependency

​ OneToOneDependency是一对一依赖关系,子RDD的每个partition依赖单个parentRdd的一个partition。实现了getParents方法:子RDD以及父RDD之间,每个partition是对应(如果子RDD中存在的话)的,所以两个RDD中的对应的partition应该具有相同的partitionId。

​ 此类的Dependency中parent中的partitionId与childRDD中的partitionId是一对一的关系,也就是partition本身范围不会改变,一个parition经过transform还是一个partition,虽然内容发生了变化,所以可以在local完成,此类场景通常像mapreduce中只有map的场景。

class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int): List[Int] = List(partitionId)
}
RangeDependency

​ RangeDependency是子rdd的每个partition依赖多个父parentRdd的一个partition,源码如下:

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
    }
  }
}

​ 该依赖关系仍然是一一对应,但是是parentRDD中的某个区间的partitions对应到childRDD中的某个区间的partitions。典型的操作是union,多个parentRDD合并到一个childRDD,所以将每个parentRDD都对应到childRDD中的一个区间。需要注意的是:union不会把多个partition合并成一个partition,而是的简单的把多个RDD中的partitions放到一个RDD里面,partition不会发生变化

​ 父RDD中的partition通常是子RDD中,连续的某块partition区间的父partition,所以对应关系应该是parentPartitionId = childPartitionId - childStart + parentStart。对于几个私有变量进行解释如下:

  1. Rdd:父RDD
  2. inStart:父RDDpartition的起始位置
  3. outStart:UnionRDD的起始位置
  4. length:父RDD partition的数量

​ 重写了getParents方法:parentRDD在最终的rdd的位置[outStart, outStart + parentRDDLength]

窄依赖的算子

常见算子中

  1. map, filter, join所对应的是OneToOneDependency

  2. union所对应的则是RangeDependency。

宽依赖

​ 宽依赖,是在shuffle stage的时候的依赖关系,依赖首先要求是PariRdd即k,v的形式,这样才能做shuffle,同时shuffle操作也是划分Stage的重要标志。宽依赖只有ShuffleDependency一个实现。每个Shuffle过程会有一个Id,ShuffleDependency可以根据这个ShuffleId去获得所依赖的partition的数据,所以ShuffleDependency所需要记录的就是要能够通过ShuffleId去获得需要的数据。

class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
    // parentRdd是transient
    @transient private val _rdd: RDD[_ <: Product2[K, V]],
    // 给shuffle输出数据进行分区的partitioner
    val partitioner: Partitioner,
    // 由于要网络传输,所以指定序列化方法
    val serializer: Serializer = SparkEnv.get.serializer,
    // shuffles的key顺序
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None,
    // 是否进行map端的部分聚合
    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
  // 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()

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

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

需要shuffle的算子

  1. coalesce、repartition、repartitionAndSortWithinPartitions

  2. cogroup

  3. intersection、subtractByKey、substract

  4. sortByKey、sortBy

  5. combineByKeyWithClassTag 、combineByKey、aggregateByKey、foldByKey、reduceByKey、countApproxDistinctByKey、groupByKey、partitionBy

具体RDD实现依赖关系

​ rdd基类中定义了获取依赖关系的方法,不同子类实现具体的获取依赖关系的逻辑。

protected def getDependencies: Seq[Dependency[_]] = deps

CoGroupedRDD

​ CoGroupedRDD在没有partitioner时候不需要shuffle操作,是一对一的依赖,建立依赖关系链,只需要新建OneToOneDependency即可。

override def getDependencies: Seq[Dependency[_]] = {
  rdds.map { rdd: RDD[_] =>
    if (rdd.partitioner == Some(part)) {
      logDebug("Adding one-to-one dependency with " + rdd)
      new OneToOneDependency(rdd)
    } else {
      logDebug("Adding shuffle dependency with " + rdd)
      new ShuffleDependency[K, Any, CoGroupCombiner](
        rdd.asInstanceOf[RDD[_ <: Product2[K, _]]], part, serializer)
    }
  }
}

UnionRDD

​ UnionRDD的dependency是RangeDependency,重写了getDependencies方法,构建方法是遍历每个rdd,将其放到一个区间中,区间开始位置是前面所有rdd的长度和到加上这个rdd的长度这个区间。

override def getDependencies: Seq[Dependency[_]] = {
  val deps = new ArrayBuffer[Dependency[_]]
  var pos = 0
  // rdds是内部变量
  for (rdd <- rdds) {
    deps += new RangeDependency(rdd, 0, pos, rdd.partitions.length)
    pos += rdd.partitions.length
  }
  deps
}

ShuffledRDD

​ shuffleRDD的getDependencies得到该RDD依赖的parentRDD的数据、序列化方式、聚合器等一系列信息,这些信息可以让这个RDD计算时正确地获取数据和处理数据。因为shuffle阶段涉及的数据移动、数据聚合太复杂,因此用到了好几种参数取控制。

override def getDependencies: Seq[Dependency[_]] = {
  val serializer = userSpecifiedSerializer.getOrElse {
    val serializerManager = SparkEnv.get.serializerManager
    if (mapSideCombine) {
      serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[ClassTag[C]])
    } else {
      serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[ClassTag[V]])
    }
  }
  // prev为父RDD,part为partitioner,后面三个参数都是ShuffledRDD初始化时设置
  List(new ShuffleDependency(prev, part, serializer, 
                             keyOrdering, aggregator,
                             mapSideCombine))
}

依赖关系获取

​ 依赖关系中最重要的是: 子RDD可以为其每个partition根据dependency找到它所对应的父RDD的partition,或者是找到计算的数据来源,那么RDD之间是怎么来找依赖关系的?

​ RDD对象的内部有一个Depedency对象的列表dependencies_,而每个Depedency对象内部会存储一个RDD对象,对应一个父RDD;通过遍历RDD内部的Dependency列表即可获取该RDD所有依赖的父RDD。内部方法dependencies初始化这个Depedency列表,主要思路是先看是否是checkpoint了,是的话就不需要parentRDD的数据了,直接依赖改checkpoint数据,否则看是否已经初始化了dependencies,未初始化就要进行getDependencies的初始化,该方法上面已经分析过了,就不在啰嗦了;另外还提供了获取第一个parent以及第j个parent的方法。

// rdd中变量定义
private var dependencies_ : Seq[Dependency[_]] = _

final def dependencies: Seq[Dependency[_]] = {
  // 如果checkpoint了就不需要寻找parentRDD了
  checkpointRDD.map(r => List(new OneToOneDependency(r))).getOrElse {
    if (dependencies_ == null) {
      // 没有计算过依赖,调用getDependencies的方法(因为这个方法只能调用一次)
      dependencies_ = getDependencies
    }
    // 若已经计算过依赖,那么直接返回(缓存RDD的依赖已经存在)
    dependencies_
  }
}

/** Returns the first parent RDD */
protected[spark] def firstParent[U: ClassTag]: RDD[U] = {
  dependencies.head.rdd.asInstanceOf[RDD[U]]
}

/** Returns the jth parent RDD: e.g. rdd.parent[T](0) is equivalent to rdd.firstParent[T] */
protected[spark] def parent[U: ClassTag](j: Int): RDD[U] = {
  dependencies(j).rdd.asInstanceOf[RDD[U]]
}

参考

  1. https://blog.csdn.net/weixin_30955341/article/details/97910007
  2. Spark技术内幕:深入解析Spark内核架构设计与实现原理
  3. https://blog.csdn.net/worldchinalee/article/details/79430912
  4. [https://huajianmao.github.io/code%20reading/spark-code-reading-Dependency/](https://huajianmao.github.io/code reading/spark-code-reading-Dependency/)
  5. [http://istoney.github.io/big%20data/2016/11/02/spark-rdd-dependency-analysis](
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值