论宽依赖、窄依赖与shuffle

本文深入解析Spark中Shuffle的含义及触发条件,通过join操作示例区分宽依赖与窄依赖,阐述在网络传输与Shuffle之间的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概念上来说

    Shuffle的含义就是洗牌,将数据打散,父RDD一个分区中的数据如果给了子RDD的多个分区(只要存在这种可能),就是shuffle。Shuffle会有网络传输数据,但是有网络传输,并不意味着就是shuffle。

窄依赖:没有发生shuffle

宽依赖:存在shuffle

    也许大家看了上面的说法只是有个初步的印象,下面我将以join为例进行讲解,相信大家看了这个例子就会对上面的概念有一个更为深刻的理解。

val rdd1=sc.parallelize(List((1,"wzh"),(2,"lly")),2)
val rdd2=sc.parallelize(List((1,"wzh"),(2,"lly")),2)
rdd1.join(rdd2).collect

    普通的join是一个宽依赖,比如rdd1.join(rdd2).collect中,(2个rdd都通过parallelize并行化创建),在这个过程中会产生3个stage。第一个Stage是rdd1(包括形成rdd1过程所进行的转换操作)将中间数据写到磁盘上等待下游来拉取,第二个Stage是rdd2(包括形成rdd2过程所进行的转换操作)将中间数据写到磁盘上等待下游来拉取,第三个Stage就是下游读取磁盘数据。也产生了2个shuffle。

    这里大家也许会困惑,明明2个rdd各有2个分区,4个分区均各有一个数据,在join时并没有出现父rdd的某个分区中的数据跑到子rdd不同分区的情况,也不可能发生(因为单个数据不会拆分),为什么还会出现shuffle?实际上,虽然一个父rdd中的一个分区中的数据并没有被拆开,但是它存在着会发生一个父rdd的一个分区数据分给多个子rdd分区的可能(即不管实际上有多少数据,只要这种模式下存在着这种可能),就会产生shuffle,就是宽依赖,就会划分stage。

    相信通过上面的例子,大家应该对何时发生宽依赖有了比较深的理解了。这里大家可能会疑惑,如果是上面这种说法,那么join不就一定是宽依赖吗?其实还存在着一种特殊的join,见下图:

   

    在先进行grouByKey之后,再进行join,且在这个过程中既没有修改RDD分区的数量,也没有设置新的分区器(默认都是按照hashpartitioner分区),那么这个join就是窄依赖。这里稍微详细的解释一下,因为在上面2个RDD中,在groupByKey之后,会产生shuffle,且按照hashpartitioner进行分区,也就是说2个RDD中的数据会按照同样的分区方式进行分区,从而导致相对应的key所对应的数据都到了同一个分区,因此再进行join时,只要子RDD分区数和分区器(默认还是hashpartitioner)不变,这些相对应的数据还是会跑到同一分区中,因此就不会出现父RDD一个分区中的数据跑到子RDD的多个分区中,因此就是一个窄依赖。

注:在这个过程中也可能发生网络传输,但是并没有发生shuffle,因此有网络传输并不一定就是shuffle。

    这里对上面所述进行一个通俗的总结,如果在进行join之前调用的是宽依赖(存在shuffle)的RDD,两个RDD的分区数量一致,且join之后产生的子RDD的分区数量也一样,那么这个时候的join就是窄依赖。除此之外的,都是宽依赖。

 

 

 

### 三、Spark 依赖窄依赖的区别特点 #### 窄依赖(Narrow Dependency) 在 Spark 中,**窄依赖**是指子 RDD 的每个分区只依赖于父 RDD 的一个分区。这种依赖关系通常出现在如 `map`、`filter`、`union` 等操作中。由于每个子分区的数据来源唯一,因此可以实现高效的流水线式计算,不需要进行数据的重新洗牌(shuffle)[^2]。 常见的窄依赖包括: - **OneToOneDependency**:子 RDD 分区父 RDD 分区一一对应。 - **RangeDependency**:用于 `union` 操作后生成的 RDD,其分区是父 RDD 分区的连续子集。 窄依赖的优势在于: - 数据流简单,易于并行处理。 - 不涉及 shuffle,执行效率高。 - 在故障恢复时更高效,因为只需重新计算失败的分区及其依赖的父分区。 ```java JavaRDD<String> lines = sc.textFile("hdfs://..."); JavaRDD<String> filtered = lines.filter(line -> line.contains("spark")); ``` 上述代码中的 `filter` 是典型的窄依赖操作,子 RDD 的每个分区只依赖于父 RDD 的对应分区[^4]。 #### 依赖(Wide Dependency) **依赖**是指子 RDD 的一个分区可能依赖于父 RDD 的多个分区。这种情况通常发生在需要进行数据重分布的操作中,例如 `groupByKey`、`reduceByKey`、`sortByKey` 和部分形式的 `join`。依赖会导致 shuffle 操作,即跨节点的数据传输和重新分组,从而带来较高的网络 I/O 开销[^1]。 常见的依赖包括: - **ShuffleDependency**:当子 RDD 的分区器(Partitioner)父 RDD 不一致或需要聚合操作时,会触发 shuffle依赖的特点如下: - 子 RDD 的每个分区依赖于多个父 RDD 分区。 - 触发 shuffle 操作,增加执行时间。 - 在调度上需要划分不同的 stage,每个 stage 内部由窄依赖组成,stage 之间通过 shuffle 边界隔开[^3]。 ```java JavaPairRDD<String, Integer> pairs = lines.mapToPair(s -> new Tuple2<>(s, 1)); JavaPairRDD<String, Integer> counts = pairs.reduceByKey((a, b) -> a + b); ``` 上述代码中,`reduceByKey` 操作导致依赖,因为每个键值对的归并操作需要将相同 key 的数据集中到同一个分区中[^5]。 #### 总结对比 | 特性 | 窄依赖 | 依赖 | |----------------|----------------------------|----------------------------------| | 是否触发 shuffle | 否 | 是 | | 分区依赖关系 | 子分区仅依赖一个父分区 | 子分区依赖多个父分区 | | 执行效率 | 高 | 相对较低 | | 故障恢复 | 只需恢复失败分区及父分区 | 需要恢复多个父分区 | | 常见算子 | map、filter、union | groupByKey、reduceByKey、join | 在实际开发中,应尽量使用窄依赖操作来优化性能,减少 shuffle 的发生[^2]。同时,合理设计分区策略(如使用合适的 Partitioner)也能有效降低依赖带来的性能瓶颈。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值