想了解Shuffle 的处理流程,首先要了解Spark是如何划分Stage的。下面,让我们看看 Spark 是如何根据RDD 的依赖关系来划分Stage。
首先 我们必须要理解 Spark 中RDD的依赖关系.
1.Rdd的依赖关系:
Rdd的依赖有两种:
1.宽依赖(
Wide
Dependency)
2.窄依赖(
Narrow Dependency)
以下图说明RDD的窄依赖和宽依赖
窄依赖
窄依赖指父RDD的每一个分区最多被一个子RDD的分区所用,表现为
- 一个父RDD的分区对应于一个子RDD的分区
- 两个父RDD的分区对应于一个子RDD 的分区。
如上面的map,filter,union属于第一类窄依赖,而join with inputs co-partitioned(对输入进行协同划分的join操作)则为第二类窄依赖
如果有多个父RDD的分区对应于同一个子RDD的分区不能称之为窄依赖?
宽窄依赖与容错性
Spark基于lineage的容错性是值,如果一个RDD出错,那么可以从它的所有父RDD重新计算所得,如果一个RDD仅有一个父RDD(即窄依赖),那么这种重新计算的代价会非常小。
Spark基于Checkpoint(物化)的容错机制何解?在上图中,宽依赖得到的结果(经历过Shuffle过程)是很昂贵的,因此,Spark将此结果物化到磁盘上了,以备后面使用
宽依赖
宽依赖指子RDD的每个分区都要依赖于父RDD的所有分区,这是shuffle类操作,上图中的groupByKey和对输入未协同划分的join操作就是宽依赖。
窄依赖细说
窄依赖对优化很有利。逻辑上,每个RDD的算子都是一个fork/join(此join非上文的join算子,而是指同步多个并行任务的barrier): 把计算fork到每个分区,算完后join,然后fork/join下一个RDD的算子。如果直接翻译到物理实现,是很不经济的:一是每一个RDD(即使 是中间结果)都需要物化到内存或存储中,费时费空间;二是join作为全局的barrier,是很昂贵的,会被最慢的那个节点拖死。如果子RDD的分区到 父RDD的分区是窄依赖,就可以实施经典的fusion优化,把两个fork/join合为一个;如果连续的变换算子序列都是窄依赖,就可以把很多个 fork/join并为一个,不但减少了大量的全局barrier,而且无需物化很多中间结果RDD,这将极大地提升性能。Spark把这个叫做
流水线(pipeline)优化。
Spark流水线优化:
宽依赖细说
变换算子序列一碰上shuffle类操作,宽依赖就发生了,流水线优化终止。在具体实现 中,DAGScheduler从当前算子往前回溯依赖图,一碰到宽依赖,就生成一个stage来容纳已遍历的算子序列。在这个stage里,可以安全地实施流水线优化。然后,又从那个宽依赖开始继续回溯,生成下一个stage。
Spark中关于Dependency的源代码
/**
* :: DeveloperApi ::
* Base class for dependencies.
*/
@DeveloperApi
abstract class Dependency[ T] extends Serializable {
def rdd: RDD[ T]
}
/**
* :: 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.
* 这里是说,窄依赖是指子RDD的每个Partition只依赖于父RDD很少部分的的分区,文档明显说的不对!窄依赖起码需要,父RDD的每个Partition只被一个子RDD的Partition依赖
*/
@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
}
/**
* ShuffleDependency指的是,子RDD的partition部分依赖于父RDD的每个Partition 部分依赖被称为 ShuffleDependency。
* 其实 ShuffleDependency 跟 MapReduce 中 shuffle 的数据依赖相同
* (mapper 将其 output 进行 partition,然后每个 reducer 会将所有 mapper 输出中属于自己的 partition 通过 HTTP fetch 得到)。
*/
/**
* :: 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.
*
* @param _rdd the parent RDD
* @param partitioner partitioner used to partition the shuffle output
* @param serializer [[ org.apache.spark.serializer.Serializer Serializer ]] to use. If set to None,
* the default serializer, as specified by ` spark.serializer ` config option, will
* be used.
* @param keyOrdering key ordering for RDD's shuffles
* @param aggregator map/reduce-side aggregator for RDD's shuffle
* @param mapSideCombine whether to perform partial aggregation (also known as map-side combine)
*/
@DeveloperApi
class ShuffleDependency[ K: ClassTag , V: ClassTag , C: ClassTag](
@transient private val _rdd: RDD[_ <: Product2[ K , V]] ,
val <