概述
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分区使用。他们有以下的区别:
- 宽依赖由于parent rdd的同一个分区需要一个或者多个子rdd处理,所以需要将同一个RDD分区的数据传入到不同的RDD分区中,中间可能涉及到多个节点之间数据的传输,对应着shuffle操作,运行时间会较长;而窄依赖的每个父RDD分区通常只会传入到另一个子RDD分区,通常在一个节点内完成。
- 在RDD分区丢失时,对于窄依赖来说,由于父RDD的一个分区只对应一个子RDD分区,这样只需要重新计算与子RDD分区对应的父RDD分区就行,所以对数据的利用是100%的;而对于宽依赖来说,重算的父RDD分区只有一部分数据是对应丢失的子RDD分区的,另一部分就造成了多余的计算,宽依赖中的子RDD分区通常来自多个父RDD分区,极端情况下,所有父RDD都有可能重新计算。
- 当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
。对于几个私有变量进行解释如下:
- Rdd:父RDD
- inStart:父RDDpartition的起始位置
- outStart:UnionRDD的起始位置
- length:父RDD partition的数量
重写了getParents方法:parentRDD在最终的rdd的位置[outStart, outStart + parentRDDLength]
窄依赖的算子
常见算子中
-
map, filter, join所对应的是OneToOneDependency
-
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的算子
-
coalesce、repartition、repartitionAndSortWithinPartitions
-
cogroup
-
intersection、subtractByKey、substract
-
sortByKey、sortBy
-
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]]
}
参考
- https://blog.csdn.net/weixin_30955341/article/details/97910007
- Spark技术内幕:深入解析Spark内核架构设计与实现原理
- https://blog.csdn.net/worldchinalee/article/details/79430912
- [https://huajianmao.github.io/code%20reading/spark-code-reading-Dependency/](https://huajianmao.github.io/code reading/spark-code-reading-Dependency/)
- [http://istoney.github.io/big%20data/2016/11/02/spark-rdd-dependency-analysis](