RDD和它依赖的父RDD的关系有两种不同的类型,即

宽依赖(shuffle dependency) 实际上在源码中,不存在(wide dependency),只有(shuffle dependency)

窄依赖(narrow dependency)

spark宽窄依赖算子 spark宽窄依赖怎么划分_spark

宽窄依赖的区别,如何划分?

现在网上有很多文章,包括很多机构的视频,都是简单的按父子RDD的数量关系来划分的,一对一、多对一是窄,一对多、多对多是宽,这么说其实不准确。
真正严谨的描述是:如果子RDD的一个分区完全依赖父RDD的一个或多个分区,则是窄依赖,否则就是宽依赖。这个完全依赖怎么理解呢?其实就是父RDD一个分区的数据是否需要切分,或者说子RDD分区要依赖父RDD分区的全部而不仅仅是一部分。上面这样说相对比较严谨,但也会有特殊情况,比如在只有一个分区的情况下,强行使用repartiton操作,即使父子RDD各自只有一个分区,也是宽依赖。这种情况生产中不会遇到,但要知晓。 ps:同学疑问?宽窄依赖划分的依据并不是是否产生了网络io,即使有网络IO的情况也有可能是窄依赖。

这是RDD论文中的图:

spark宽窄依赖算子 spark宽窄依赖怎么划分_spark_02

基于此图,分析下这里为什么左面的流程都是窄依赖,而右面的却是宽依赖:

左上角map和filter算子中,对于父RDD来说,一个分区内的数据,有且仅有一个子RDD的分区来消费该数据。UNION算子也是同样的。

有个极端情况,如果父类RDD有很多的分区,而子类RDD只有一个分区,我们可以使用repartition或者coalesce算子来实现该效果,请问,这种实现是宽依赖?还是窄依赖?

如果从网上流传的一种观点:子RDD一个partition内的数据依赖于父类RDD的所有分区,则为宽依赖,这种判断明显是不严谨的:

因为如果我们的reduceTask只有一个的时候,只有一个分区,这个分区内的数据,肯定依赖于所有的父类RDD,此时就要看是否使用了partitoner计算来获取分区id,如果使用了,相当于调用了coalsce(parNum,true),就是宽依赖,否则就是窄依赖。

spark宽窄依赖算子 spark宽窄依赖怎么划分_spark宽窄依赖算子_03

object SparkDepTest {
  def main(args: Array[String]): Unit = {
    //搭建环境
    val sparkConf = new SparkConf().setMaster("local[2]").setAppName(this.getClass.getSimpleName)
    val sparkContext = new SparkContext(sparkConf)
    val rdd = sparkContext.makeRDD(List(1, 2, 3, 4, 5), 1).map(_ + 1)

    println(rdd.getNumPartitions)
    val rdd2: RDD[Int] = rdd.coalesce(1)
    // val rdd2: RDD[Int] = rdd.repartition(1)
    println(rdd2.getNumPartitions)

    rdd2.foreach(println(_))
    Thread.sleep(50000)
    sparkContext.stop()
  }
}

上面代码中,即使rdd 只有一个分区,执行coalesce(1,true)或者repartition(1),也会出现宽依赖。

spark宽窄依赖算子 spark宽窄依赖怎么划分_spark_04

相对之下,宽依赖呢?
父RDD一个分区的数据,会被分割成不同的部分,发往不同的子RDD分区。

这里存在一个可能被挑刺的地方,比如说父类每个分区内都只有一条数据,当然,这些数据都会被唯一地指定到子类的某个分区内,这是窄依赖,还是宽依赖?
是宽依赖,因为根据partitioner提供的信息进行shuffle,窄依赖不需要根据partitioner提供的信息进行shuffle,只需根据partitioner提供的信息获取数据进行处理(一般是map()等操作)
结论:
父RDD中分区内的数据,是否需要通过partitioner提供的信息才能确定分发到哪个子RDD分区,如果是,宽依赖,如果不是,窄依赖。 跟父子RDD分区数量关系无关 原因:

  1. 父RDD分区的每条数据需要通过分区器才能确定输出到子RDD的哪个分区,也就是有了shuffle,而shuffle会落盘,此时如果强行pipeline,效率会大大降低
  2. 故障恢复时,依然要重新计算父RDD多个分区,如果没有cache,意味着整个任务要重跑
    这样的情况,只能是宽依赖。

为什么要划分宽窄依赖,有什么作用?

引申联想

在数据处理中,出于效率的考虑,有2种方法论。

  • 一种是并行,即将数据切分为几个部分,一般叫分区,并行处理
  • 一种是批量,即将数据先汇总到一个地方,凑够一定数量,一起处理
    这2种思路并无优劣之分,只是适用于不同的场景。无论在计算机宇宙还是现实生活中,类似的处理思想比比皆是:
  • 比如过年包水饺,几个大妈同时包,相当于pipeline并行处理,但包完了并不是立即放到锅里煮,而是先放到盖帘上,等凑满一起下
  • 再比如大货车装货,几个工人同时搬,等装满了之后大货车才会开
  • 向数据库中插入大量数据,凑够一定数量放到缓存中,批量插入
  • 消息队列的读取数据和缓存功能

在spark中也是一样的道理,有些场景,比如map、filter、co-partitioned join,适用于并行。另一些场景,比如reduceByKey、join、repartition,适用于批量。

而对于第二种批量场景,临时数据放在哪就成了问题,放在内存?如果内存无限大那当然可以。大数据场景下数据量往往远大于内存,所以需要落盘,自然跟mapreduce一样有了溢写的问题,而spark的溢写机制要比mapreduce效率更高,更先进。
同时,出于以下几点考虑,有些场景的shuffle需要进行排序:

  1. 有些算子在reducer端需要全局排序,那么可以在mapper端进行预排序,相当于预聚合
  2. 有利于Mapper压缩合并数据,减少网络层传输
  3. 基于排序的数据,进行Shuffle时效率更高
  4. Reduce基于排序数据合并更高效
    排序本身就是耗费性能的,所以排序是把双刃剑,需要根据场景进行选择,spark在shuffle reduce task的数量小于spark.shuffle.sort.bypassMergeThreshold(默认200)时,使用bypass机制,也就是shuffle时不排序。

答案

1. 避免重复计算。试想,有一个普通的join场景,子RDD一个分区依赖父RDD每个分区的一部分,假如子RDD有3个分区,那么子RDD的分区每去拉取一次数据,所有分区就要重新计算一次,总共要计算3次。除非是co-partitioned join

类似于招聘面试,有3家公司招人,10个人面试,但每家公司面试的时候这10个人都要去参加一次招聘。而预聚合的join就相当于上班,这10个人已经明确知道自己的公司了,直接去即可。

2. 如果父子RDD是完全依赖,意味着父子RDD之间可以以pipeline(管道)的方式实现合并 + 并行计算,方便优化,不需要shuffle,子RDD不需要等父RDD分区计算完。父RDD分区的每一条数据流向,在一开始生成DAG图的时候,就已经确定了。

3. 提高容错效率,如果有一个分区数据丢失,只需要从父RDD的对应1个分区重新计算即可,不需要重新计算所有分区,提高容错。

如果是不完全依赖,那么父RDD分区中的数据,需要通过partitioner(分区器)的计算,才能确定下一步的走向,如果在不完全依赖中强行以pipeline的方式计算,不仅不会提高效率,反而会使效率和容错变得更加困难。
统计班里人数。这是什么思想的体现?批量嘛,一把梭嘛,为什么要批量?还是为了效率,为了容错方便嘛,你一个个数,万一中间有个数错了咋办,再来一遍?你批量数,数错了重试也比一个一个要方便的多,就算不重试,你也仅仅是统计下每行有多少人、一共几行,总比你一个一个来要方便吧。去迪士尼乐园、坐火车,这种批量处理例子在生活中比比皆是,什么是学习,就是用已知解释未知,就像一棵树一样,不断的开枝散叶。

所以说,宽窄依赖的划分,归根到底还是为了提高效率。

shuffle一定要落盘吗???

shuffle本身就是为了应对内存不足的情况,因为宽依赖是要等父RDD所有分区计算完才能发送到子RDD的,大数据场景下,一般内存都吃不消。
即使数据量很小,经过测试,也是需要落盘的,并没有不落盘的方法。

join的情况

spark宽窄依赖算子 spark宽窄依赖怎么划分_spark_02


预聚合意味着父RDD分区中的数据不需要partitioner提供的信息,在生成DAG的时候便能确定分发到哪个子RDD分区,可以pipeline,也可以高效容错。

spark宽窄依赖算子 spark宽窄依赖怎么划分_父类_06