Spark 核心之 RDD 窄依赖与宽依赖详解

摘要 :为什么 map().filter() 可以在一个 Stage 内完成,而 reduceByKey() 必须切分新的 Stage?为什么 Shuffle 是 Spark 性能的头号杀手?答案就在 RDD 的依赖类型之中------NarrowDependency 与 ShuffleDependency。本文从依赖类型体系、Stage 边界判定源码、Pipeline 优化原理、容错策略差异、性能影响与调优五个维度,配合 1 张原创深色架构图 + 完整源码分析,带你彻底理解 Spark 最核心的概念之一。

关键词:Spark RDD, NarrowDependency, ShuffleDependency, Stage, Pipeline, 血缘, 容错, Shuffle


一、开篇:为什么依赖类型决定一切?

先看两段等价代码:

scala 复制代码
// 方案 A: 全 Narrow --- 1 个 Stage, 无 Shuffle
rdd.map(_ + 1).filter(_ > 10).collect()

// 方案 B: 包含 Wide --- 2 个 Stage, 有 Shuffle
rdd.map(_ + 1).reduceByKey(_ + _).collect()

方案 A 只有 1 个 Stage ,因为 map 和 filter 都是窄依赖------子 Partition 只依赖父 Partition 的 1 对 1 映射,Spark 可以将它们 Pipeline 在一起 执行。

方案 B 有 2 个 Stage ,因为 reduceByKey 是宽依赖------子 Partition 需要从多个父 Partition 拉取数据,必须通过 Shuffle 重新分区。


二、NarrowDependency vs ShuffleDependency 全景图

三、NarrowDependency(窄依赖)

3.1 三种子类型

scala 复制代码
// 源码:Dependency.scala
abstract class NarrowDependency[T](_rdd: RDD[T]) extends Dependency[T] {
  // 子 Partition 依赖父 RDD 的哪些 Partition
  def getParents(partitionId: Int): Seq[Int]
}

// 1:1 映射
class OneToOneDependency[T](rdd: RDD[T]) extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int) = List(partitionId)
}

// Range 依赖 (union)
class RangeDependency[T](rdd: RDD[T], inStart: Int, outStart: Int, length: Int)
  extends NarrowDependency[T](rdd) {
  override def getParents(partitionId: Int) =
    if (partitionId >= outStart && partitionId < outStart + length)
      List(partitionId - outStart + inStart) else Nil
}

3.2 哪些算子产生 NarrowDependency?

算子 依赖类型 说明
map OneToOneDependency 1→1 映射
filter OneToOneDependency 1→1 映射(过滤后仍独立)
flatMap OneToOneDependency 1→1 映射
mapPartitions OneToOneDependency 1→1 映射
union RangeDependency 多 RDD 拼接
coalesce NarrowDependency 减少 Partition(不涉及 Shuffle 时)
sample OneToOneDependency 采样

四、ShuffleDependency(宽依赖)

4.1 源码结构

scala 复制代码
class ShuffleDependency[K, V, C](
    @transient private val _rdd: RDD[_ <: Product2[K, V]],
    val partitioner: Partitioner,        // HashPartitioner / RangePartitioner
    val serializer: Serializer = SparkEnv.get.serializer,
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None, // mapSideCombine
    val mapSideCombine: Boolean = false)
  extends Dependency[Product2[K, V]] {
  val shuffleId: Int = _rdd.context.newShuffleId()
  val shuffleHandle: ShuffleHandle = ... // SortShuffleManager 生成
}

4.2 ShuffleDependency 的五个关键属性

属性 说明 影响
partitioner HashPartitioner / RangePartitioner 决定数据如何分布到下游 Partition
serializer Kryo / Java 序列化 Shuffle 中间数据
keyOrdering 排序器 sortByKey 必须指定
aggregator 聚合器 reduceByKey 的 mapSideCombine
mapSideCombine 是否 Map 端预聚合 reduceByKey vs groupByKey 的关键区别

4.3 reduceByKey vs groupByKey

scala 复制代码
// ❌ groupByKey: mapSideCombine=false --- 全量数据传输
rdd.groupByKey()  // Shuffle 传输所有 KV 对

// ✅ reduceByKey: mapSideCombine=true --- Map 端预聚合
rdd.reduceByKey(_ + _)  // Shuffle 前先在 Map 端合并

性能差异 :reduceByKey 比 groupByKey 减少 90%+ 的 Shuffle 数据量(取决于 Key 的重复率)。


五、依赖类型决定 Stage 边界

scala 复制代码
// 源码:DAGScheduler.scala
private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
  rdd.dependencies.flatMap {
    case shufDep: ShuffleDependency[_, _, _] =>
      // 🔥 遇到 Shuffle → 创建 ShuffleMapStage → Stage 边界
      getOrCreateShuffleMapStage(shufDep, firstJobId) :: Nil
    case _: NarrowDependency[_] =>
      Nil  // Narrow → 同 Stage, 不切分
  }.toList
}

规则 :遇到 ShuffleDependency 即切分 Stage,NarrowDependency 全部在同一 Stage 内 Pipeline 执行。


六、容错策略差异

维度 Narrow Wide
容错范围 只重算丢失 Partition 重算整个 Stage(因为上游数据需要重新 Shuffle)
血缘链 短(Pipeline 内) 长(跨 Stage)
恢复成本 低 高(Shuffle 重复 I/O)
Checkpoint 通常不需要 强烈建议在 Shuffle 后 Checkpoint

七、性能优化建议

bash 复制代码
# 1. 用 reduceByKey 代替 groupByKey(开启 mapSideCombine)
# 2. 用 Kryo 序列化减少 Shuffle 数据量
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer

# 3. 调大 Shuffle 分区数避免数据倾斜
--conf spark.sql.shuffle.partitions=400

# 4. 启用 Shuffle 文件合并
--conf spark.shuffle.consolidateFiles=true

八、总结

要点 总结
NarrowDependency 1→1 / Range / Prune, 同 Stage Pipeline, 容错轻
ShuffleDependency 多→多, Stage 边界, Shuffle I/O 重, 容错贵
Stage 切分 遇 ShuffleDependency 即切分
优化核心 减少 Shuffle = 减少 Stage = 提升性能

金句:Narrow 是团队内部的接力赛(一棒接一棒,全在一个 Stage),Wide 是部门之间的邮件沟通(必须写好 Shuffle 文件,下一个 Stage 再来读)。


作者:starzy | AI Data Engineer / 大数据技术实践者

博客:blog.starzy.cn | GitHub:starzy1990.github.io

专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践

相关推荐
计算机毕业编程指导师1 小时前
计算机毕设选题推荐:基于Hadoop与Spark的Steam游戏数据分析系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习 深度学习
大数据·hadoop·python·计算机·spark·毕业设计·steam游戏
计算机毕业编程指导师1 小时前
【计算机毕设选题推荐】基于Hadoop+Spark的白鹿抖音评论大数据分析与可视化系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习 深度学习
大数据·hadoop·python·计算机·spark·毕业设计·抖音评论
计算机毕业编程指导师7 小时前
【大数据毕设选题推荐】基于Spark的WTA职业网球赛事演变与竞技格局分析系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习 深度学习
大数据·python·计算机·spark·毕业设计·课程设计·wta网球
计算机毕业编程指导师10 小时前
【计算机毕设选题推荐】基于Spark与Hadoop的AI就业收入区域差异分析系统 源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习 深度学习
大数据·python·数据分析·spark·毕业设计·课程设计·ai就业收入
计算机毕业编程指导师13 小时前
【大数据毕设选题】基于Spark的电信网络诈骗话术语义特征挖掘分析系统源码 毕业设计 选题推荐 数据分析 机器学习 深度学习
大数据·hadoop·python·spark·毕业设计·课程设计·网络诈骗
计算机毕业编程指导师13 小时前
计算机大数据毕设怎么选?基于Spark的个体肥胖健康风险评估的数据分析与可视化系统带你通关 源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·计算机·spark·毕业设计·肥胖风险
IT研究室14 小时前
最新大数据毕业设计选题推荐-基于大数据的单位招聘岗位信息分析与可视化-大数据-Spark-Hadoop-Bigdata
大数据·spark·课程设计
计算机毕业编程指导师15 小时前
计算机毕设答辩技巧:基于Hadoop+Django的公共交通运营数据分析与可视化系统怎么做 源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·spark·毕业设计·课程设计·交通运行
计算机毕业编程指导师15 小时前
【计算机毕设选题推荐】基于Hadoop+Django高频电力消耗大数据分析系统从0到1 源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
hadoop·python·数据分析·spark·毕业设计·课程设计·电力
FYKJ_201015 小时前
springboot手工蜀绣在线销售系统60947-计算机课程设计、毕业设计
java·spring boot·后端·python·mysql·spark·课程设计