RDD依赖关系及Stage划分源码

本文围绕Spark中RDD的依赖关系展开。介绍了查看RDD血缘关系和依赖关系的方法,阐述了窄依赖和宽依赖的概念及特点,宽依赖会引起Shuffle且对性能有重要影响。还讲解了基于DAG的Stage任务划分,包括任务运行流程、各层级关系,并进行了源码分析。

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

RDD依赖关系

1. 查看血缘关系

RDD只支持粗粒度转换,即在大量记录上执行的单个操作。将创建RDD的一系列Lineage(血统)记录下来,以便恢复丢失的分区。RDD的Lineage会记录RDD的元数据信息和转换行为,当该RDD的部分分区数据丢失时,它可以根据这些信息来重新运算和恢复丢失的数据分区。
在这里插入图片描述

1)代码实现

object Lineage01 {

def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        val fileRDD: RDD[String] = sc.textFile("input/1.txt")
        println(fileRDD.toDebugString)
        println("----------------------")

        val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
        println(wordRDD.toDebugString)
        println("----------------------")

        val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
        println(mapRDD.toDebugString)
        println("----------------------")

        val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
        println(resultRDD.toDebugString)

        resultRDD.collect()

        //4.关闭连接
        sc.stop()
    }
}

2)打印结果

(2) input/1.txt MapPartitionsRDD[1] at textFile at Lineage01.scala:15 []
 |  input/1.txt HadoopRDD[0] at textFile at Lineage01.scala:15 []
----------------------
(2) MapPartitionsRDD[2] at flatMap at Lineage01.scala:19 []
 |  input/1.txt MapPartitionsRDD[1] at textFile at Lineage01.scala:15 []
 |  input/1.txt HadoopRDD[0] at textFile at Lineage01.scala:15 []
----------------------
(2) MapPartitionsRDD[3] at map at Lineage01.scala:23 []
 |  MapPartitionsRDD[2] at flatMap at Lineage01.scala:19 []
 |  input/1.txt MapPartitionsRDD[1] at textFile at Lineage01.scala:15 []
 |  input/1.txt HadoopRDD[0] at textFile at Lineage01.scala:15 []
----------------------
(2) ShuffledRDD[4] at reduceByKey at Lineage01.scala:27 []
 +-(2) MapPartitionsRDD[3] at map at Lineage01.scala:23 []
    |  MapPartitionsRDD[2] at flatMap at Lineage01.scala:19 []
    |  input/1.txt MapPartitionsRDD[1] at textFile at Lineage01.scala:15 []
    |  input/1.txt HadoopRDD[0] at textFile at Lineage01.scala:15 []

注意:圆括号中的数字表示RDD的并行度,也就是有几个分区

2. 查看依赖关系

在这里插入图片描述

1)代码实现

object Lineage02 {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2.创建SparkContext,该对象是提交Spark App的入口
        val sc: SparkContext = new SparkContext(conf)

        val fileRDD: RDD[String] = sc.textFile("input/1.txt")
        println(fileRDD.dependencies)
        println("----------------------")

        val wordRDD: RDD[String] = fileRDD.flatMap(_.split(" "))
        println(wordRDD.dependencies)
        println("----------------------")

        val mapRDD: RDD[(String, Int)] = wordRDD.map((_,1))
        println(mapRDD.dependencies)
        println("----------------------")

        val resultRDD: RDD[(String, Int)] = mapRDD.reduceByKey(_+_)
        println(resultRDD.dependencies)

        resultRDD.collect()

        // 查看localhost:4040页面,观察DAG图
Thread.sleep(10000000)

        //4.关闭连接
        sc.stop()
    }
}

2)打印结果

List(org.apache.spark.OneToOneDependency@f2ce6b)
----------------------
List(org.apache.spark.OneToOneDependency@692fd26)
----------------------
List(org.apache.spark.OneToOneDependency@627d8516)
----------------------
List(org.apache.spark.ShuffleDependency@a518813)

3)全局搜索(ctrl+n)org.apache.spark.OneToOneDependency

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

注意:要想理解RDDS是如何工作的,最重要的就是理解Transformations。
RDD之间的关系可以从两个维度来理解:一个是RDD是从哪些RDD转换而来,也就是 RDD的parent RDD(s)是什么; 另一个就是RDD依赖于parent RDD(s)的哪些Partition(s),这种关系就是RDD之间的依赖。

RDD和它依赖的父RDD(s)的关系有两种不同的类型,即窄依赖(narrow dependency)和宽依赖(wide dependency)。

3. 窄依赖

窄依赖表示每一个父RDD的Partition最多被子RDD的一个Partition使用,窄依赖我们形象的比喻为独生子女。
在这里插入图片描述

4. 宽依赖

宽依赖表示同一个父RDD的Partition被多个子RDD的Partition依赖,会引起Shuffle,总结:宽依赖我们形象的比喻为超生。
在这里插入图片描述

具有宽依赖的transformations包括:sort、reduceByKey、groupByKey、join和调用rePartition函数的任何操作。
宽依赖对Spark去评估一个transformations有更加重要的影响,比如对性能的影响。

5. Stage任务划分

1)DAG有向无环图
DAG(Directed Acyclic Graph)有向无环图是由点和线组成的拓扑图形,该图形具有方向,不会闭环。例如,DAG记录了RDD的转换过程和任务的阶段。
在这里插入图片描述

2)任务运行的整体流程
在这里插入图片描述
在这里插入图片描述

3)RDD任务切分中间分为:Application、Job、Stage和Task
(1)Application:初始化一个SparkContext即生成一个Application;
(2)Job:一个Action算子就会生成一个Job;
(3)Stage:Stage等于宽依赖的个数加1;
(4)Task:一个Stage阶段中,最后一个RDD的分区个数就是Task的个数。
注意:Application->Job->Stage->Task每一层都是1对n的关系。
在这里插入图片描述

4)代码实现

object Stage01 {

    def main(args: Array[String]): Unit = {

        //1.创建SparkConf并设置App名称
        val conf: SparkConf = new SparkConf().setAppName("SparkCoreTest").setMaster("local[*]")

        //2. Application:初始化一个SparkContext即生成一个Application;
        val sc: SparkContext = new SparkContext(conf)

        //3. 创建RDD
        val dataRDD: RDD[Int] = sc.makeRDD(List(1,2,3,4,1,2),2)

        //3.1 聚合
        val resultRDD: RDD[(Int, Int)] = dataRDD.map((_,1)).reduceByKey(_+_)

        // Job:一个Action算子就会生成一个Job;
        //3.2 job1打印到控制台
        resultRDD.collect().foreach(println)

        //3.3 job2输出到磁盘
        resultRDD.saveAsTextFile("output")

        Thread.sleep(1000000)

        //4.关闭连接
        sc.stop()
    }
}

5)查看Job个数
查看http://localhost:4040/jobs/,发现Job有两个。
在这里插入图片描述

6)查看Stage个数
查看Job0的Stage。由于只有1个Shuffle阶段,所以Stage个数为2。
在这里插入图片描述

查看Job1的Stage。由于只有1个Shuffle阶段,所以Stage个数为2。
在这里插入图片描述

7)Task个数
查看Job0的Stage0的Task个数
在这里插入图片描述

查看Job0的Stage1的Task个数
在这里插入图片描述

查看Job1的Stage2的Task个数
在这里插入图片描述

查看Job1的Stage3的Task个数
在这里插入图片描述

注意:如果存在shuffle过程,系统会自动进行缓存,UI界面显示skipped的部分

6. Stage任务划分源码分析

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

### Apache Spark Stage 划分源码分析 #### 程序入口与初始化 当执行 `spark-submit` 命令时,实际调用的是位于 `${SPARK_HOME}/bin/` 下的 `spark-class` 脚本。此脚本最终会启动 `org.apache.spark.deploy.SparkSubmit` 类来处理命令行参数并准备应用程序环境[^1]。 ```bash exec "${SPARK_HOME}"/bin/spark-class org.apache.spark.deploy.SparkSubmit "$@" ``` #### DAGScheduler 和 Stage 划分逻辑 DAGScheduler 是负责管理作业调度的核心组件之一,在接收到由驱动器端发起的动作操作(action)请求之后,DAGScheduler 将构建有向无环图(Directed Acyclic Graph, DAG),并将该图形转换成一系列的任务集(TaskSet)[^2]。具体来说: - **Job 提交**: 当用户提交一个 Action 操作给 Spark 应用程序时,这将触发一个新的 Job 创建。 - **Stage 构建**: 对于每一个新创建的 Job,DAGScheduler 需要解析构成这个 Job 的所有宽依赖关系(Wide Dependency)和窄依赖关系(Narrow Dependency), 并据此定义多个 Stage。每个 Stage 内部只包含具有窄依赖的数据流变换;而不同 Stages 之间则存在宽依赖边界[^3]。 - **Parent RDDs 获取**: 在确定如何分割这些 Stage 后,DAGScheduler 使用 `RDD.getDependencies()` 方法获取当前 RDD 所有的父级 RDD 实例列表作为依据来进行进一步划分[^4]。 #### 结果计算阶段( Result Stage ) 对于那些直接对应于某些特定动作的操作而言,比如 `count()`, `saveAsTextFile()` 或者其他任何返回值类型的 API ,它们会被视为特殊的 Stage 称之为 “Result Stage”。这类 Stage 特指那部分能够立即产出最终结果数据集合的工作单元。 ```scala // 示例代码片段展示了一个简单的Action操作是如何被转化为ResultStage的. val rdd = sc.parallelize(Seq(("a", 1), ("b", 2))) rdd.count() ``` 上述例子中,`count()` 动作将会促使系统生成相应的 ResultStage 来完成计数统计工作。 #### 完整流程总结 综上所述,整个 Stage 划分过程大致如下: - 用户通过 `spark-submit` 发起应用; - SparkSubmit 解析配置项并向集群申请资源; - Driver 接收到来自 Client 的指令后开始构建 DAG 图形结构; - 根据不同的 ShuffleDependency 关系切割出独立运作的小型任务组即 Stage; - 如果遇到 Action 类型的操作,则额外形成专门用于求解终态数据的结果 Stage
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值