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方法,并传入other
RDD和一个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
。这里self
和 other
就是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中的getDependencies
和getPartitions
两个函数。找到这两个函数,这里我们一个一个分析这两个函数。
首先是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
的返回结果。这里我们就需要知道原始代码中的rdd1
和rdd2
是否有分区器呢,我们可以打印测试一下:
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 oneToOneDependency
和case shuffleDependency
就是根据刚刚上面getDependencies
的过程为每个RDD生成的oneToOneDependency
和shuffleDependency
的标识,在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
以及shuffleManager
和 shuffle reader
的部分。是要去对RDD做shuffle的操作。
因此通过对partitionBy
源码的查阅,就验证了我们上述的思路,以及看源码的流程。找到了在CoGroupRDD
中判断是否对RDD做Shuffle的依据。
总结
因此通过上面的分析,我们可以得出结果回答题目:
对于题目中的两个join操作,第一个join操作是宽依赖,第二个join操作是窄依赖
join 操作何时是宽依赖,何时是窄依赖:
如果join操作的两个RDD有分区器,且两个分区器的分区数相同,则满足条件if (rdd.partitioner == Some(part))
,此时join操作是窄依赖。
如果join操作的两个RDD没有分区器或分区数量不同,那么则不满足上面的判断语句,会执行shuffle操作。