Spark宽依赖窄依赖的区别

定义:

一般观点:

  • 窄依赖是子RDD的一个分区只依赖于父RDD的一个分区,即每个父RDD的分区最多被子RDD的一个分区使用;
  • 宽依赖是子RDD的一个分区依赖了父RDD的多个分区,即多个子RDD的分区数据依赖父RDD的同一个分区的数据。

而实际上:

  • 窄依赖是父RDD的一个或多个分区的数据全部流入到子RDD 的一个或多个分区;
  • 宽依赖是父RDD的每个分区的不同部分,分别流入到子RDD的不同分区。

算子:

窄依赖算子:map、filter、union、join(hash-partitioned)、mapPartitions;

宽依赖算子:join(非hash-partitioned)、groupByKey、partitionBy;

详解

类图关系

代码位于:spark/core/scala/Dependency.scala

Spark宽依赖窄依赖的区别_第1张图片

源码中反查RangeDependency的引用情况,只有UnionRDD引用到了

class UnionRDD[T: ClassTag](
    sc: SparkContext,
    var rdds: Seq[RDD[T]])
  extends RDD[T](sc, Nil) {  // Nil since we implement getDependencies  
    
  override def getDependencies: Seq[Dependency[_]] = {
    val deps = new ArrayBuffer[Dependency[_]]
    var pos = 0
    for (rdd <- rdds) {
      deps += new RangeDependency(rdd, 0, pos, rdd.partitions.length)
      pos += rdd.partitions.length
    }
    deps
  }
}

源码中反查OneToOneDependency的引用情况:

Spark宽依赖窄依赖的区别_第2张图片

窄依赖的各种情况

在官方文档中 Spark 2.4.7 ScalaDoc,窄依赖的描述为:

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.

即 child RDD 中的每个分区都依赖 parent RDD 中的一小部分分区。那么如何理解这句话呢,我们首先来看下面这张图。

Spark宽依赖窄依赖的区别_第3张图片

本图囊括了有关窄依赖的各种依赖情况,我们一一来看。

OneToOneDependency

一对一依赖。从图中我们可以看出,child RDD 中的每个分区都只依赖 parent RDD 中的一个分区,并且 child RDD 的分区数和 parent RDD 的分区数相同。这种我们称之为 OneToOneDependency。属于这种依赖关系的转换算子有 map()、flatMap()、filter() 等。通过阅读 Spark 源码,我们可以发现,这些算子生成的 RDD 的依赖关系使用的就是 OneToOneDependency 这个类。

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


RangeDependency

范围依赖。child RDD 和 parent RDD 的分区经过划分,每个范围内的父子 RDD 的分区都为一一对应的关系。属于这种依赖关系的转换算子有 union() 等。通过阅读源码,我们可以看到,在 UnionRDD 的 getDependencies() 方法中,创建了一个 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
    }
  }
}
class UnionRDD[T: ClassTag](
    sc: SparkContext,
    var rdds: Seq[RDD[T]])
  extends RDD[T](sc, Nil) {

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


NarrowDependency

窄依赖类。通过代码我们可以发现,上面的 OneToOneDependency 和 RangeDependency 都继承了 NarrowDependency 这个类。现在我们来看下上图的下半部分。

  • 左边我们可以看作是多对一的依赖,属于这种依赖关系的转换算子有特殊情形的 join()、cogroup() 等。为什么说特殊情形呢,用 cogroup() 举例。cogroup() 可以聚合多个 RDD,其中如果某些 parent RDD 和 child RDD 的 partitioner 和分区数相同(比如,都为 HashPartitioner),那么这些 parent RDD 的分区就可以直接流入到 child RDD 的对应分区中,为 OneToOneDependency 情形。而其它不符合这种条件的分区,则为 ShuffleDependency 。

join和cogroup举例:Spark join和cogroup算子-CSDN博客

class CoGroupedRDD[K: ClassTag](
    @transient var rdds: Seq[RDD[_ <: Product2[K, _]]],
    part: Partitioner)
  extends RDD[(K, Array[Iterable[_]])](rdds.head.context, Nil) {

  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)
      }
    }
  }
}
  • 右边我们可以看做是多对多的依赖,属于这种依赖关系的转换算子有 cartesian()。可能大家一不注意就把这种情形当成是 ShuffleDependency 了,但通过源码我们可以发现,CartesianRDD 中创建了两个 NarrowDependency 完成了笛卡尔乘积操作,属于窄依赖。

cartesian:笛卡尔积

class CartesianRDD[T: ClassTag, U: ClassTag](
    sc: SparkContext,
    var rdd1 : RDD[T],
    var rdd2 : RDD[U])
  extends RDD[(T, U)](sc, Nil)
  with Serializable {

  override def getDependencies: Seq[Dependency[_]] = List(
     new NarrowDependency(rdd1) {
         def getParents(id: Int): Seq[Int] = List(id / numPartitionsInRdd2)
     },
     new NarrowDependency(rdd2) {
         def getParents(id: Int): Seq[Int] = List(id % numPartitionsInRdd2)
     }
  )
}

宽依赖 ShuffleDependency

接下来我们再来看宽依赖。在官方文档中 Spark 2.4.7 ScalaDoc,宽依赖的描述为:

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.

官方这里并没有从 RDD 分区角度来解释什么是 ShuffleDependency ,只是说需要 shuffle 的两个 Stage 的依赖。那到底什么是 ShuffleDependency 呢?我们来看下图。

Spark宽依赖窄依赖的区别_第4张图片

看到这可能有的同学会说,这不和 NarrowDependency 一样么?仔细看,NarrowDependency 虽然也有 child RDD 的一个分区依赖 parent RDD 的多个分区的情况,但都是依赖分区的全部。而 ShuffleDependency 中,child RDD 的一个分区依赖的是 parent RDD 中各个分区的某一部分。如上图左半部分,child RDD 的两个分区分别只依赖 parent RDD 中的 1 和 2 部分。而计算出 1 或者 2 部分的过程,以及 child RDD 分别读取 1 和 2 的过程,即为 shuffle write/shuffle read,这个过程正是 shuffle 开销所在。

总结

  • NarrowDependency :parent RDD 的一个或多个分区的数据全部流入到 child RDD 的一个或多个分区;
  •  ShuffleDependency: parent RDD 的每个分区的不同部分,分别流入到 child RDD 的不同分区。

Spark 之所以要将依赖关系分为 NarrowDependency 和 ShuffleDependency ,是可以更好的将各种依赖类型进行分类,明确数据怎么流出流入,从而更容易生成对应的物理执行计划。

  • NarrowDependency 不需要 shuffle 操作,并且可以用于流式操作(pipeline);
  • ShuffleDependency 则需要进行 shuffle 操作,有 shuffle 的地方需要划分不同的 stage。

参考:

深入解读 Spark 宽依赖和窄依赖(ShuffleDependency & NarrowDependency)_因特马的博客-CSDN博客
Spark宽依赖和窄依赖深度剖析 - 简书

你可能感兴趣的:(Spark,大数据,面试,spark,大数据,分布式)