Spark 宽依赖和窄依赖有什么区别?
面试怎么回答
窄依赖中,子分区只依赖少量确定的父分区,多个算子可以在同一个 Stage 流水执行,失败时通常只重算相关分区。宽依赖中,子分区需要多个父分区的数据重新分组,会产生 Shuffle,并形成 Stage 边界。它会增加网络、磁盘、序列化、内存和倾斜成本。优化不是消灭宽依赖,而是通过提前过滤、Map 端预聚合、Broadcast 和合理分区减少不必要的数据搬运。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
很多人会回答:
“窄依赖不发生 Shuffle,宽依赖会发生 Shuffle。”
这句话没错,但面试官通常会继续问:为什么宽依赖会切 Stage?为什么它更慢?对容错有什么影响?
真正需要理解的是:
依赖类型描述的是子分区需要哪些父分区的数据,它决定能否流水线执行、是否需要 Shuffle,以及任务失败后的重算边界。
第一步:窄依赖可以在同一个 Stage 流水执行
窄依赖中,一个父分区的数据只会被少量确定的子分区使用,一个子分区也不需要等待所有父分区的数据重新分组。
常见操作包括:
mapfilterflatMapmapPartitions- 不触发 Shuffle 的
coalesce
例如:
RDD A 分区 0 → RDD B 分区 0 → RDD C 分区 0
Spark 可以把这些 Transformation 合并在同一个 Stage 的 Task 中流水执行,不需要把中间结果跨节点重新分发。
某个分区丢失时,也通常只需要沿着 Lineage 重算与该分区相关的父分区。
第二步:宽依赖需要跨分区重新分组
宽依赖中,一个子分区的数据可能来自多个父分区,同一个父分区的数据也可能被发送到多个子分区。
常见操作包括:
groupByKeyreduceByKeydistinctrepartition- 普通大表
join
例如按 Key 聚合时,相同 Key 可能分散在所有上游分区。Spark 必须先完成上游 Map Task,把记录按目标 Partition 写入 Shuffle,再由下游 Reduce Task 拉取对应数据。
因此 Shuffle 会形成 Stage 边界:下游 Stage 需要等待上游 Shuffle Map 输出可用,不能像窄依赖一样直接串在同一条流水线上。

第三步:宽依赖的成本不只是网络传输
Shuffle 通常会引入:
- 跨 Executor 网络传输
- Shuffle 文件写盘和读取
- 序列化与反序列化
- 内存排序、聚合和 Spill
- 更多 GC 与磁盘 I/O
- 数据倾斜导致的长尾 Task
所以看到宽依赖时,不能只说“有网络开销”。真正的性能问题还取决于数据量、Key 分布、分区数、聚合方式和磁盘能力。
宽依赖也影响故障恢复。如果下游 Task 失败,通常可以重新读取已有 Shuffle 输出;如果保存 Shuffle 文件的 Executor 丢失,对应上游 Shuffle Map Task 可能需要重算。
第四步:优化目标不是消灭所有宽依赖
按 Key 聚合、去重和大表 Join 本来就需要重新分区,宽依赖并不是错误。
优化重点是减少不必要的数据搬运:
- 能在 Map 端预聚合时,优先
reduceByKey或aggregateByKey - 小表 Join 评估 Broadcast,避免大规模 Shuffle
- 过滤和字段裁剪尽量放在 Shuffle 前
- 避免连续无意义的
repartition - 根据数据量设置合理 Shuffle Partition
- 检查热点 Key,必要时拆分或两阶段聚合
还要结合 Spark UI 或执行计划确认实际 Stage 和 Shuffle 指标,而不是只凭代码方法名判断。
一个典型场景
比如统计每个用户的订单金额,代码先使用:
groupByKey().mapValues(sum)
它会把同一个用户的所有明细跨网络拉到同一个分区,再一次性聚合。大用户可能占用大量内存并触发 Spill。
改成 reduceByKey 后,可以先在 Map 端对每个分区内的同 Key 数据局部聚合,再传输中间结果。
两者都存在宽依赖和 Shuffle,但后者通常减少了网络传输量和 Reduce 端内存压力。
理解宽窄依赖,不是为了背算子名单,而是为了看懂 Stage 为什么切开、数据为什么要移动。