Spark中join操作何时是窄依赖何时是宽依赖

Spark中join操作何时是窄依赖何时是宽依赖

问题
  • 下面程序的两个打印语句的结果是什么,对应的依赖是宽依赖还是窄依赖,为什么会是这个结果;
  • join 操作何时是宽依赖,何时是窄依赖;

程序代码如下:

import org.apache.spark.rdd.RDD
import org.apache.spark.{SparkConf, SparkContext}


object JoinDemo {
def main(args: Array[String]): Unit = {
val conf = new SparkConf().setAppName(this.getClass.getCanonicalName.init).setMaster("local[*]")
val sc = new SparkContext(conf)
sc.setLogLevel("WARN")

val random = scala.util.Random


val col1 = Range(1, 50).map(idx => (random.nextInt(10), s"user$idx"))
val col2 = Array((0, "BJ"), (1, "SH"), (2, "GZ"), (3, "SZ"), (4, "TJ"), (5, "CQ"), (6, "HZ"), (7, "NJ"), (8, "WH"), (0,"CD"))


val rdd1: RDD[(Int, String)] = sc.makeRDD(col1)
val rdd2: RDD[(Int, String)] = sc.makeRDD(col2)

val rdd3: RDD[(Int, (String, String))] = rdd1.join(rdd2)println(rdd3.dependencies)
val rdd4: RDD[(Int, (String, String))] = rdd1.partitionBy(new HashPartitioner(3)).join(rdd2.partitionBy(newHashPartitioner(3)))

println(rdd4.dependencies)
sc.stop()
}
}

首先将代码块复制到Idea中进行运行,然后Arlt+鼠标左键点进去join的源码,来进行分析:

  def join[W](other: RDD[(K, W)]): RDD[(K, (V, W))] = self.withScope {
    join(other, defaultPartitioner(self, other))
  }

可以看到,该函数又调用了join方法,并传入otherRDD和一个defaultPartitioner的结果。先点进defaultPartitioner,观察该函数的返回值是什么

  def defaultPartitioner(rdd: RDD[_], others: RDD[_]*): Partitioner = {
    val rdds = (Seq(rdd) ++ others)
    val hasPartitioner = rdds.filter(_.partitioner.exists(_.numPartitions > 0))

    val hasMaxPartitioner: Option[RDD[_]] = if (hasPartitioner.nonEmpty) {
      Some(hasPartitioner.maxBy(_.partitions.length))
    } else {
      None
    }

可以看到,首先该函数将join操作需要连接的两个RDD进行了拼接,

返回的结果如下所示:

List(ParallelCollectionRDD[0] at makeRDD at joinDemo.scala:69, ParallelCollectionRDD[1] at makeRDD at joinDemo.scala:70)

如果两个 都进行了分区,则返回:

List(ShuffledRDD[2] at partitionBy at joinDemo.scala:77, ShuffledRDD[3] at partitionBy at joinDemo.scala:78)

然后去利用filter去过滤,只保留两个参数中RDD有分区器的结果。然后从有分区器的RDD中,利用maxBy挑选出分区数最大的RDD。如果两个RDD都没有分区器,则返回None。

现在知道了defaultPartitioner函数的结果后,就可以带着该结果放到join函数里,然后再看join函数里做了什么。

  def join[W](other: RDD[(K, W)], partitioner: Partitioner): RDD[(K, (V, W))] = self.withScope {
    this.cogroup(other, partitioner).flatMapValues( pair =>
      for (v <- pair._1.iterator; w <- pair._2.iterator) yield (v, w)
    )
  }

这里join函数中flatMapVlues肯定是个窄依赖的算子,所以要想知道flatMapValues中的pair表示的是什么,就需要去看这里的cogroup里做了什么。

  def cogroup[W](other: RDD[(K, W)], partitioner: Partitioner)
      : RDD[(K, (Iterable[V], Iterable[W]))] = self.withScope {
    if (partitioner.isInstanceOf[HashPartitioner] && keyClass.isArray) {
      throw new SparkException("HashPartitioner cannot partition array keys.")
    }
    val cg = new CoGroupedRDD[K](Seq(self, other), partitioner)
    cg.mapValues { case Array(vs, w1s) =>
      (vs.asInstanceOf[Iterable[V]], w1s.asInstanceOf[Iterable[W]])
    }
  }

这里cogroup函数中,定义了一个CoGroupRDD的对象,传入的参数是Seq(self, other), partitioner。这里selfother就是join操作需要连接的两个RDD。那么我们就还需要点进CoGroupRDD里去一探究竟

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

首先可以看到,CoGroupedRDD继承了RDD,因此也就需要重新RDD中的getDependenciesgetPartitions两个函数。找到这两个函数,这里我们一个一个分析这两个函数。

首先是getDependencies

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

首先该函数中对要做join操作的两个rdd进行遍历,判断每个rdd的分区器是否是传入参数中给定的分区器类型。那么回到最初的那个join函数,里面传入了defaultPartitioner的返回结果。这里我们就需要知道原始代码中的rdd1rdd2是否有分区器呢,我们可以打印测试一下:

    println(rdd1.partitioner)
    println(rdd2.partitioner)

    println(rdd1.partitioner.exists(_.numPartitions > 0))
    println(rdd2.partitioner.exists(_.numPartitions > 0))

输出结果:

None
None
false
false

也就是说我们的rdd1和rdd2是没有分区器的,那么此时,defaultPartitioner的返回结果也会是None, 因为hasPartitioner.nonEmpty返回的是False。那么此时class CoGroupedRDD 中传入的参数part 也会是None。 所以到了

getDependencies函数中,rdd.partitioner == Some(part)返回的就是False,所以进行进入else部分的代码,要执行new ShuffleDependency[K, Any, CoGroupCombiner](rdd.asInstanceOf[RDD[_ <: Product2[K, _]]], part, serializer).

此时再点进ShuffleDependency中查看:

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)
  extends Dependency[Product2[K, V]] {

该类继承了Dependency.

回到getDependencies函数中,这里就是遍历每个rdds,然后为每个rdd创建一个标识,标识该rdd是否需要进行Shuffle。

看到下面的compute函数:

override def compute(s: Partition, context: TaskContext): Iterator[(K, Array[Iterable[_]])] = {
    val split = s.asInstanceOf[CoGroupPartition]
    val numRdds = dependencies.length

    // A list of (rdd iterator, dependency number) pairs
    val rddIterators = new ArrayBuffer[(Iterator[Product2[K, Any]], Int)]
    for ((dep, depNum) <- dependencies.zipWithIndex) dep match {
      case oneToOneDependency: OneToOneDependency[Product2[K, Any]] @unchecked =>
        val dependencyPartition = split.narrowDeps(depNum).get.split
        // Read them from the parent
        val it = oneToOneDependency.rdd.iterator(dependencyPartition, context)
        rddIterators += ((it, depNum))

      case shuffleDependency: ShuffleDependency[_, _, _] =>
        // Read map outputs of shuffle
        val it = SparkEnv.get.shuffleManager
          .getReader(shuffleDependency.shuffleHandle, split.index, split.index + 1, context)
          .read()
        rddIterators += ((it, depNum))
    }

这里case oneToOneDependencycase shuffleDependency就是根据刚刚上面getDependencies的过程为每个RDD生成的oneToOneDependencyshuffleDependency的标识,在RDD计算的时候判断其依赖类型。可以看到如果是shuffleDependency,就进入到了SparkEnv.get.shuffleManager.getReader的代码中,也就是Spark Shuffle过程的Shuffle Reader,以及Shuffle Writer的知识。说明该部分应该是RDD进行shuffle的过程代码。也就找到了RDD是否进行shuffle的依据.

我们再重新回顾一下整个流程:

首先我们的目标算子是rdd1.join(rdd2),那么我们点击join函数里,发现参数里面有defaultPartitioner,阅读源码知道,该函数返回的就是两个RDD中分区数最大的那个RDD。如果两个RDD都没有分区器,那么就返回None。由于我们的两个RDD: rdd1和rdd2都没有设置分区器,所以这里函数的返回值就是None。然后带着返回值继续深挖代码,进入到了CoGroupRDD这个类,这个类是继承了RDD的类。然后在getDependencies函数中,我们找到了为RDD是否做Shuffle的依据:if (rdd.partitioner == Some(part))

这个判断语句判断了RDD的分区器是否与defaultPartitioner函数得到的RDD的默认分区器相同,如果不同则会为该rdd创建一个new ShuffleDependency的标识,该标识在后续RDD的实际计算中,在compute函数里,会以该标识为依据对RDD进行判断。如果RDD满足case shuffleDependency,则会进行后续到Shuffle操作。如果相同,可知在compute函数里,就只会对RDD进行窄依赖的操作。

当然还有更多的关于RDD中计算函数compute,以及getDependencies的调用过程等就需要关于Spark的更多学习进行了解了。

最后我们再去看一下partitionBy的源码,看看它是怎么控制RDD分区的。

我们可以看到partitionBy的源码一样是进行了判断:

if (self.partitioner == Some(partitioner)) {
      self
    } else {
      new ShuffledRDD[K, V, V](self, partitioner)
    }
  }

如果RDD没有分区器,就将该RDD转变为一个ShuffleRDD。进入ShuffleRDD可以看到,它也是继承了RDD,所以这里ShuffleRDD和上面的CoGroupRDD其实是同类的东西。一样是找到getDependencies函数,因为这里ShuffleRDD类表明RDD一定会去做Shuffle,所以返回的直接就是包含new ShuffleDependency的列表。

然后进到compute函数里,就可以看到,这部分的代码就跟我们上面CoGroupRDD类的compute函数里,要对RDD做Shuffle的部分是一样的了。都涉及到了SparkEnv以及shuffleManagershuffle reader的部分。是要去对RDD做shuffle的操作。

因此通过对partitionBy源码的查阅,就验证了我们上述的思路,以及看源码的流程。找到了在CoGroupRDD中判断是否对RDD做Shuffle的依据。

总结

因此通过上面的分析,我们可以得出结果回答题目:

对于题目中的两个join操作,第一个join操作是宽依赖,第二个join操作是窄依赖

join 操作何时是宽依赖,何时是窄依赖:

如果join操作的两个RDD有分区器,且两个分区器的分区数相同,则满足条件if (rdd.partitioner == Some(part)),此时join操作是窄依赖。

如果join操作的两个RDD没有分区器或分区数量不同,那么则不满足上面的判断语句,会执行shuffle操作。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JermeryBesian

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值