Spark的宽依赖与窄依赖

RDD的计算图-血统图(Lineage Graph)

RDD上的计算表示成血统图(Lineage Graph):即表示RDD之上所做操作的有向无环图。Spark通过这些血统图/有向无环图来表示RDD,并据此进行优化。

RDD的具体表示(四部分)

组成部分 说明
分区 数据集的原子划分,每个计算节点有一或多个
依赖 用来对RDD和其上的分区与派生它的RDD(Parent RDD)之间的关系进行建模
函数 基于父RDD对数据集进行计算的操作函数
元数据 关于RDD的分区策略及数据存放位置的元数据

RDD依赖与Shuffle

RDD依赖关系表明了数据必须经由网络移动(即shuffle)的时机。Transformations操作导致shuffle,且Transformations产生依赖。

两种依赖关系的定义
  • 窄依赖:父RDD的每个分区只被子RDD的一个分区所使用。

    • 1个子RDD的分区对应于1个父RDD的分区(如map、filter、union等算子);
    • 1个子RDD的分区对应于N个父RDD的分区(如co-partioned join,协同划分的join)。
  • 宽依赖:父RDD的每个分区都可能被多个子RDD分区所使用。

    • 1个父RDD分区对应非全部多个子RDD分区(如groupByKey、reduceByKey、sortByKey);
    • 1个父RDD分区对应所有子RDD分区(如未经协同划分的join)。
为什么要有宽窄依赖?
  • 窄依赖的价值:支持在同一个集群Executor上以pipeline(管道)形式顺序执行多条命令,例如执行map后紧接着执行filter。分区内计算收敛,无需依赖所有分区数据,可在不同节点并行计算;失败恢复更高效,只需重新计算丢失的父分区(Parent Partition)。
  • 宽依赖的必要性:虽需等待父RDD全部分区数据就绪才能计算,且可能涉及跨节点数据传输(类似MapReduce操作),但能支持分组、排序、非协同划分join等复杂场景------这些场景必须聚合多分区数据才能完成计算,是宽依赖存在的核心原因。
依赖关系的特性对比
特性 窄依赖 宽依赖
数据流向 父分区→单个子分区(一对一或多对一) 父分区→多个子分区(一对多)
网络传输 无需shuffle,可在单个节点内完成 需要shuffle,依赖跨节点数据传输
计算效率 支持pipeline执行,效率高 需等待父RDD全部分区就绪,效率较低
容错恢复 仅需重算对应父分区,恢复快,数据利用率100% 需重算多个父分区,恢复慢,存在冗余计算

可视化依赖关系示例

假设存在以下RDD转换流程:

复制代码
原始RDD(2个分区) → filter(窄依赖) → map(窄依赖) → groupByKey(宽依赖) → 结果RDD(2个分区)

对应的依赖关系示意图:

复制代码
父RDD分区0 → 子RDD分区0(filter) → 子RDD分区0(map) → 子RDD分区0(groupByKey)
                                                                 ↘
父RDD分区1 → 子RDD分区1(filter) → 子RDD分区1(map) → 子RDD分区1(groupByKey)
  • 其中filtermap属于窄依赖(每个父分区仅对应一个子分区);
  • groupByKey属于宽依赖(父分区数据会分发到多个子分区)。

Transformations操作与依赖类型对应表

依赖类型 对应操作
窄依赖 map、mapValues、flatMap、filter、mapPartitions、mapPartitionsWithIndex、union、join(父RDD是hash-partitioned)
宽依赖 groupByKey、join(父RDD不是hash-partitioned)、partitionBy、cogroup、groupWith、leftOuterJoin、rightOuterJoin、reduceByKey、combineByKey、distinct、intersection、repartition、coalesce

注意:join既有可能是宽依赖,也有可能是窄依赖(窄依赖的join可能因groupBy之后做了cache操作)。

确定依赖类型的方法

  1. 通过dependencies方法

    返回Dependency对象序列,供Spark调度器内部使用:

    scala 复制代码
    // Scala示例:查看RDD依赖类型
    val rdd = sc.parallelize(1 to 10).map(_ * 2).groupBy(_ % 3)
    rdd.dependencies.foreach { dep =>
      println(dep.getClass.getSimpleName)  // 输出:ShuffleDependency(宽依赖)
    }
  2. 通过toDebugString方法

    可视化打印RDD的血统图及调度信息(缩进分组表示stage):

    scala 复制代码
    // Scala示例:打印血统图
    val rdd = sc.parallelize(1 to 10).filter(_ > 5).map(_ * 2)
    println(rdd.toDebugString)
    // 输出(窄依赖操作在同一stage):
    // (8) MapPartitionsRDD[2] at map at <console>:25 []
    //  |  FilteredRDD[1] at filter at <console>:25 []
    //  |  ParallelCollectionRDD[0] at parallelize at <console>:25 []

DAG与Stage划分

  1. DAG的形成

    RDD之间的依赖关系形成DAG(有向无环图)。在Spark作业调度系统中,调度需先判断任务间的因果依赖关系(部分任务需先执行,后续依赖任务才能执行),且任务无循环依赖,因此适合用DAG表示。

  2. Stage划分依据

    以宽依赖(shuffle依赖)为边界,每个宽依赖会将DAG拆分为新的Stage。因shuffle依赖需等父RDD全部分区数据可读才能计算,Spark设计为让父RDD将结果写在本地,写完后通知子RDD,这种特性决定了宽依赖必须作为Stage划分的边界。

  3. Stage执行逻辑

    • 第一个Stage:需将结果shuffle到本地(如groupByKey先聚合某个key的所有记录,此汇聚过程即shuffle);
    • 第二个Stage:读入shuffle后的本地数据,执行后续计算。
  4. Stage 划分与 Shuffle 的对应关系

    Shuffle 过程存在"先写后读"的强依赖------必须等前一个 Stage 所有任务的 Shuffle Write 全部完成(所有临时文件就绪),后一个 Stage 的 Shuffle Read 才能开始拉取数据 ,两者无法并行执行,因此 Spark 必须以 Shuffle 为界,将前后操作拆分为两个独立的 Stage,按"先上游 Stage 再下游 Stage"的顺序调度执行。

    而窄依赖操作(如 map → filter)因无 Shuffle,可在同一个 Stage 内以"流水线(Pipeline)"方式执行(一个数据元素从 map 到 filter 无需落地磁盘,直接在内存中连续处理),无需拆分 Stage。

    阶段(Stage) 核心操作 类比 MapReduce 作用
    前一个 Stage(Shuffle 上游) Shuffle Write(写 shuffle 数据) Map 端 执行到 Shuffle 依赖前的所有窄依赖操作(如 map、filter),最后将计算结果按 Key 分区,写入本地磁盘的 shuffle 临时文件(供下游读取)。 例:groupByKey 中"聚合本地 Key 记录"的操作,就属于这个阶段的 Shuffle Write。
    后一个 Stage(Shuffle 下游) Shuffle Read(读 shuffle 数据) Reduce 端 先从上游所有节点的 Shuffle Write 临时文件中,拉取属于当前分区的 Key 数据(跨节点网络传输),再执行后续窄依赖操作(如 reduce 聚合、filter)。 例:groupByKey 中"汇总所有节点同一 Key 数据并最终聚合"的操作,就属于这个阶段的 Shuffle Read 之后的计算。

血统图与容错性(对优化的帮助)

血统图是Spark容错性的关键,其借助函数式编程思想实现容错:

  • RDD是不可变的;
  • 使用map、flatMap、filter等高阶函数对不可变数据进行函数式变换;
  • 基于父RDD对数据集计算所做的变换函数,也是血统图中RDD构成的一部分。

当RDD分区丢失(如节点故障)时,Spark通过血统图重新计算恢复,两种依赖的恢复差异直接影响效率:

  • 窄依赖:因父RDD一个分区只对应一个子RDD分区,只需重算对应父分区,数据利用率100%,且不同节点可并行恢复;
  • 宽依赖:父RDD分区对应多个子RDD分区,重算时仅部分数据用于恢复丢失子分区,存在冗余计算;极端情况下,子RDD分区来自所有父分区,需重算全部父分区,恢复成本极高。

区分宽窄依赖的核心优化价值:

  1. 窄依赖支持节点内pipeline计算,减少数据传输;宽依赖需针对性优化shuffle(如调整分区数),降低跨节点开销;
  2. 指导Stage划分与资源分配,让窄依赖操作聚合在同一Stage,提升并行效率;
  3. 容错时差异化处理:窄依赖优先并行重算,宽依赖需尽量减少冗余计算(如优先复用缓存数据)。
相关推荐
共享家95274 小时前
QT-常用控件(多元素控件)
开发语言·前端·qt
葱头的故事4 小时前
将传给后端的数据转换为以formData的类型传递
开发语言·前端·javascript
_23334 小时前
vue3二次封装element-plus表格,slot透传,动态slot。
前端·vue.js
jump6804 小时前
js中数组详解
前端·面试
智象科技4 小时前
CMDB报表体系如何驱动智能运维
大数据·运维·报表·一体化运维·cmdb
崽崽长肉肉4 小时前
iOS 基于Vision.framework从图片中提取文字
前端
温宇飞4 小时前
Web Abort API - AbortSignal 与 AbortController
前端
Tomoon4 小时前
前端开发者的全栈逆袭
前端