RDD的宽窄依赖

在设计RDD的接口时,一个有意思的问题是如何表现RDD之间的依赖。在RDD中将依赖划分成了两种类型:窄依赖(narrow dependencies)和宽依赖(wide dependencies)。窄依赖是指父RDD的每个分区都只被子RDD的一个分区所使用。相应的,那么宽依赖就是指父RDD的分区被多个子RDD的分区所依赖。例如,map就是一种窄依赖,而join则会导致宽依赖(除非父RDD是hash-partitioned,见下图)。

在这里插入图片描述
这种划分有两个用处。首先,窄依赖支持在一个结点上管道化执行。例如基于一对一的关系,可以在filter之后执行map。其次,窄依赖支持更高效的故障还原。因为对于窄依赖,只有丢失的父RDD的分区需要重新计算。而对于宽依赖,一个结点的故障可能导致来自所有父RDD的分区丢失,因此就需要完全重新执行。因此对于宽依赖,Spark会在持有各个父分区的结点上,将中间数据持久化来简化故障还原,就像MapReduce会持久化map的输出一样。

注意:join操作有两种情况:如果两个RDD在进行join操作时,一个RDD的partition仅仅和另一个RDD中已知个数的Partition进行join,那么这种类型的join操作就是窄依赖,例如图1中左半部分的join操作(join with inputsco-partitioned);其它情况的join操作就是宽依赖,例如图1中右半部分的join操作(join with inputsnot co-partitioned),由于是需要父RDD的所有partition进行join的转换,这就涉及到了shuffle,因此这种类型的join操作也是宽依赖。

Stage:
一个Job会被拆分为多组Task,每组任务被称为一个Stage就像Map Stage, Reduce Stage。Stage的划分在RDD的论文中有详细的介绍,简单的说是以shuffle和result这两种类型来划分。在Spark中有两类task,一类是shuffleMapTask,一类是resultTask,第一类task的输出是shuffle所需数据,第二类task的输出是result,stage的划分也以此为依据,shuffle之前的所有变换是一个stage,shuffle之后的操作是另一个stage。比如 rdd.parallize(1 to 10).foreach(println) 这个操作没有shuffle,直接就输出了,那么只有它的task是resultTask,stage也只有一个;如果是rdd.map(x => (x, 1)).reduceByKey(_ + _).foreach(println), 这个job因为有reduce,所以有一个shuffle过程,那么reduceByKey之前的是一个stage,执行shuffleMapTask,输出shuffle所需的数据,reduceByKey到最后是一个stage,直接就输出结果了。如果job中有多次shuffle,那么每个shuffle之前都是一个stage.
在这里插入图片描述
会根据RDD之间的依赖关系将DAG图划分为不同的阶段,对于窄依赖,由于partition依赖关系的确定性,partition的转换处理就可以在同一个线程里完成,窄依赖就被spark划分到同一个stage中,而对于宽依赖,只能等父RDD shuffle处理完成后,下一个stage才能开始接下来的计算。之所以称之为ShuffleMapTask是因为它需要将自己的计算结果通过shuffle到下一个stage中

Stage划分思路
因此spark划分stage的整体思路是:从后往前推,遇到宽依赖就断开,划分为一个stage;遇到窄依赖就将这个RDD加入该stage中。因此在图2中RDD C,RDD D,RDD E,RDDF被构建在一个stage中,RDD A被构建在一个单独的Stage中,而RDD B和RDD G又被构建在同一个stage中。
  在spark中,Task的类型分为2种:ShuffleMapTask和ResultTask;简单来说,DAG的最后一个阶段会为每个结果的partition生成一个ResultTask,即每个Stage里面的Task的数量是由该Stage中最后一个RDD的Partition的数量所决定的!而其余所有阶段都会生成ShuffleMapTask;之所以称之为ShuffleMapTask是因为它需要将自己的计算结果通过shuffle到下一个stage中;也就是说图2中的stage1和stage2相当于mapreduce中的Mapper,而ResultTask所代表的stage3就相当于mapreduce中的reducer。

### Spark 中宽依赖与窄依赖的概念及区别 #### 窄依赖 (Narrow Dependency) 窄依赖是指子 RDD 的每个分区最多依赖于父 RDD 的一个分区。这意味着数据可以在不需要 shuffle 的情况下直接从父 RDD 转移到子 RDD。由于不存在跨分区的数据交换,因此可以高效地进行流水线操作[^1]。 例如,在 `map` 或 `filter` 操作中,输入数据的每一个分区只会映射到输出的一个分区,不会引发大规模的数据重分布。这种特性使得窄依赖下的任务能够快速完成,因为它们只需要顺序读取本地磁盘上的数据即可[^2]。 ```python rdd = sc.parallelize([1, 2, 3]) mapped_rdd = rdd.map(lambda x: x * 2) # 这是一个典型的窄依赖例子 ``` --- #### 宽依赖 (Wide Dependency) 宽依赖则是指子 RDD 的某个分区可能会依赖于父 RDD 的多个分区甚至全部分区。这种情况通常发生在需要对数据进行重新分区或者聚合的操作时,比如 `groupByKey` 和 `reduceByKey`。为了实现这样的转换,必须通过 shuffle 将来自不同节点的数据集中起来再分配给新的分区[^3]。 在宽依赖的情况下,Spark 需要将整个 DAG 划分为不同的 Stage 来管理复杂的计算流程。每当遇到一次 shuffle 操作时,就会创建一个新的 Stage 边界[^4]。 ```python rdd = sc.parallelize([(1, 'a'), (2, 'b'), (1, 'c')]) reduced_rdd = rdd.reduceByKey(lambda a, b: a + b) # 属于宽依赖的例子 ``` --- #### 主要区别总结如下表所示: | 特性 | 窄依赖 | 宽依赖 | |-----------------|---------------------------------|--------------------------------| | 数据流动模式 | 不涉及 Shuffle | 必须经过 Shuffle | | 分区依赖 | 子分区仅依赖单个父分区 | 子分区可依赖多个父分区 | | Fault Tolerance | 可以只重算丢失的那个分区链条 | 需要回溯至共同祖先并重做更多工作 | | 性能影响 | 更加轻量级 | 开销较大 | --- ### 实际应用中的意义 了解这两种依赖类型有助于优化 Spark 应用程序性能。尽量减少不必要的 shuffles 是提高效率的关键之一。可以通过调整逻辑设计来避免过多使用那些容易引起宽依赖的操作符(如 join),转而寻找替代方案降低整体复杂度[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值