文章目录
第一部分 宽窄依赖篇
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
}
窄依赖的具体实现有OneToOneDependency
和RangeDependency
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
结果完成后再被执行。