spark-core_27: DAGScheduler的初始化源码分析

本文详细介绍了Spark中DAGScheduler的工作原理,包括Job、Stage和Task的概念,以及DAGScheduler如何进行任务调度、数据本地性和故障恢复等核心功能。

1,SparkContext在初始完成SparkDeploySchedulerBacked和TaskSchedulerImpl,就开始初始化DAGScheduler,并将

TaskSchedulerImpl、MapOutputTrackerMaster、BlockManagerMaster,设置到它的成员上

  * 1,DAGScheduler面向stage的高级调度层。 它计算每个Job的DAG(一个action会有一个job,而一个job有多个stage),跟踪所有RDD和shuffleMapStage的输出位置,找到最优化的DAG图进行执行。
  * DAGScheduler会将每个stage的TaskSets(一个stage就对应一个TaskSet)提交到TaskSchedulerImpl,放到集群上运行。
  *
  * 2,TaskSet包含完全独立的任务,这些任务可以基于群集中已有的数据(如:来自前一个shuffleMagStage输出文件),然后立即运行。
  *
  *
  * 3,每一次shuffle就是一个stage。每个stage包括多个窄依赖的Rdd操作,(如map()和filter()),这些窄依赖操作会放在一个pipeline在这个Stage中
  *
  * 4,如果是shuffle依赖操作会对应多个stage(父stage负责shufflewriter输出文件数据,子stage负责shuffle reader读取文件数据)。
  *
  * 5,DAGScheduler除了提供每个stage对应DAG外,DAGScheduler每个task对应的数据本地性,并将这些数据位置传递给底层的TaskScheduler。
  * 此外,它还会处理,由于shuffle输出文件丢失,会让以前stage重新提交执行。
  * 如果失败不是因为shuffle文件丢失,会让TaskScheduler来处理,TaskScheduler会重试每个任务,而不是取消整个阶段。

  *关键名词:
  * - Jobs(由
[[ActiveJob]]表示)是action触发的,是给调度使用的。
  * 例如,当用户调用某个action动作时,如count(),job的提交会通过submitJob。一个job对应多个stage来构建中间数据。
  *
  * - Stages(
[[Stage]]一组task的集合,给Job计算中间结果用的。
  * 1,在相同的Rdd分区上,每个task计算的RDD函数是一样的。
  * 2,每个stage在shuffle时分开,这引入了barrier(我们必须等待前一阶段完成获取输出)。
  * 3,有两种类型的阶段:执行action的最后Stage称为
[[ResultStage]]和父shuffler的map称为[[ShuffleMapStage]]
  * 4,如果这些作业重复使用相同的RDD,则阶段通常跨多个作业共享。
  *
  * - Tasks是工作的基本单位,每个task都发送到一台机器。
  * - Cache tracking:DAGScheduler计算出哪些RDD被缓存以避免重新计算它们,并同样记住哪些ShuffleMapStage已经产生了输出文件,以避免重新ShuffleMapStage。
  * - Preferred locations:DAGScheduler还根据其基础RDD的首选位置或缓存或随机数据的位置计算在某个阶段中运行每个任务的位置。
  * - Cleanup:当依赖于它们的正在运行的作业完成时,将清除所有数据结构,以防止长时间运行的应用程序中发生内存泄漏。
  *
  * 要从故障中恢复,同一个stage可能需要多次运行,这称为“尝试”。比如:TaskScheduler报告由于前一阶段的ShuffleMapStage输出文件丢失而导致任务失败,会重试stage,
  * 则DAGScheduler会重新提交丢失的阶段。这可通过带有FetchFailed的CompletionEvent或ExecutorLost事件来检测。
  * DAGScheduler将等待少量时间来查看其他节点或任务是否失败,然后重新提交TaskSets以查找丢失的计算丢失任务的阶段。
  * 作为这个过程的一部分,我们可能还必须为之前清理过Stage对象的旧(已完成)阶段创建Stage对象。 由于阶段的旧尝试的任务仍可以运行,
  * 因此必须小心地映射在正确的阶段对象中接收到的任何事件。
  *
  * 以下是制作或查看这个类时,所做更改时使用的清单:
    - 当涉及它们的作业结束时,应该清除所有的数据结构,以避免长时间运行的程序无限期地累积状态。
    - 添加新的数据结构时,更新
`DAGSchedulerSuite.assertDataStructuresEmpty`以包含新的结构。这将有助于捕捉内存泄漏。
  *
 */

private[spark]
class DAGScheduler(
    private[scheduler] val sc: SparkContext,
    private[scheduler] val taskScheduler: TaskScheduler,
    listenerBus: LiveListenerBus,
    mapOutputTracker: MapOutputTrackerMaster,
    blockManagerMaster: BlockManagerMaster,
    env: SparkEnv,
    clock: Clock = new SystemClock())
  extends Logging {

  def this(sc: SparkContext, taskScheduler: TaskScheduler) = {
    this(
      sc,
      taskScheduler, //TaskSchedulerImpl
      sc.listenerBus, //LiveListenerBus 异步处理事件的对象,从sc中获取
      sc.env.mapOutputTracker.asInstanceOf[MapOutputTrackerMaster], 
//运行在Driver端管理shuffle map task的输出
      sc.env.blockManager.master, 
//运行在driver端的BlockManagerMaster,管理整个Job的Block信息,从sc中获取
      sc.env)  //sc.env就 是sparkEnv
  }

  def this(sc: SparkContext) = this(sc, sc.taskScheduler)
//给MetricsSystem使用的,DAGSchedulerSource测量的信息是stage.failedStages,
stage.runningStages,stage.waiting-Stage,stage.allJobs,stage.activeJobs
  private[spark] val metricsSource: DAGSchedulerSource = new DAGSchedulerSource(this)

  private[scheduler] val nextJobId = new AtomicInteger(0)// 生成JobId
  private[scheduler] def numTotalJobs: Int = nextJobId.get() // 总的Job数
  private val nextStageId = new AtomicInteger(0) // 下一个StageId
  // 记录某个job对应的包含的所有stage
  private[scheduler] val jobIdToStageIds = new HashMap[Int, HashSet[Int]]
// 记录StageId对应的Stage
private[scheduler] val stageIdToStage = new HashMap[Int, Stage]
// 记录每一个shuffle对应的ShuffleMapStage,key为shuffleId

  private[scheduler] val shuffleToMapStage = new HashMap[Int, ShuffleMapStage] 

// 记录处于Active状态的job,key为jobId, value为ActiveJob类型对象
private[scheduler] val jobIdToActiveJob = new HashMap[Int, ActiveJob]

  // Stages we need to run whose parents aren't done
  // 等待运行的Stage,一般这些是在等待Parent Stage运行完成才能开始
  private[scheduler] val waitingStages = new HashSet[Stage]

  // Stages we are running right now
  // 处于Running状态的Stage
  private[scheduler] val runningStages = new HashSet[Stage]

  // Stages that must be resubmitted due to fetch failures
  // 失败原因为fetch failures的Stage,并等待重新提交
  private[scheduler] val failedStages = new HashSet[Stage]
  // active状态的Job列表
  private[scheduler] val activeJobs = new HashSet[ActiveJob]

  /**
   * Contains the locations that each RDD's partitions are cached on.  
This map's keys are RDD ids and its values are arrays indexed by partition numbers.
 Each array value is the set of locations where that RDD partition is cached.
   *
   * All accesses to this map should be guarded by synchronizing on it 
(see SPARK-4454).
    * 包含每个RDD分区缓存的位置。 map的key是RDD ID,其值是由分区号索引的数组。 
每个数组值都是该RDD分区缓存的位置组。 所有对map的访问都应该通过同步进行保护
   */
  private val cacheLocs = new HashMap[Int, IndexedSeq[Seq[TaskLocation]]]

  // For tracking failed nodes, we use the MapOutputTracker's epoch number, 
which is sent with every task. When we detect a node failing, 
we note the current epoch number and failed
  // executor, increment it for new tasks, and use this to ignore stray
 ShuffleMapTask results.
  // TODO: Garbage collect information about failure epochs when we know there
 are no more
  //       stray messages to detect.
  //为了跟踪失败的节点,我们使用MapOutputTracker的epoch编号,它随每个任务一起发送。 
当我们检测到一个节点失败时,
  // 我们注意到当前的epoch编号和失败的执行器,为新任务增加它,并用它来忽略杂散ShuffleMapTask
结果。
  private val failedEpoch = new HashMap[String, Long]
  /** 决定是否将task的输出放到hdfs中,该类会在dirver或executor上被实例化,
  * 在executor上请求提交输入将指向dirver的OutputCommitCoordinator,driver关
联OutputCommitCoordinatorEndpoint
  */
  private [scheduler] val outputCommitCoordinator = env.outputCommitCoordinator

  // A closure serializer that we reuse.
  // This is only safe because DAGScheduler runs in a single thread.
  //spark如果第一个要优化的地方,默认是javar,如果使用Kryo进行网终传输,
会比使用java提升10倍以上
  private val closureSerializer = SparkEnv.get.closureSerializer.newInstance()

  /** If enabled, FetchFailed will not cause stage retry, in order to surface 
the problem.
    * 如果启用,FetchFailed将不会导致stage重试 */
  private val disallowStageRetryForTest = sc.getConf.getBoolean("spark.test.noStageRetry", false)

  private val messageScheduler =
    ThreadUtils.newDaemonSingleThreadScheduledExecutor("dag-scheduler-message")
  // 处理Scheduler事件的对象,这个类初始化的最后一行,调用eventProcessLoop.start()
  private[scheduler] val eventProcessLoop = new DAGSchedulerEventProcessLoop(this)

//DAGScheduler初始化的时候,DAGScheduler执行有TaskSchedulerImpl引用,然后将自己用setDAGScheduler设置给TaskSchedulerImpl
 
taskScheduler.setDAGScheduler(this)


<think>我们分析的目标是 `mvn dependency:tree -Dincludes=org.apache.spark` 命令的输出,该命令会显示项目中所有与 `org.apache.spark` 相关的依赖关系树。通过这个输出,我们可以: 1. 确认项目中直接和间接引入的Spark相关依赖的版本。 2. 检查是否存在多个不同版本的Spark依赖(即版本冲突)。 3. 根据冲突情况调整依赖配置(如使用exclusions排除)。 在用户提供的pom.xml中,我们注意到: - 项目指定了Spark版本为3.5.0,Scala二进制版本为2.12。 - 但是,MongoDB Spark Connector依赖中排除了所有`org.mongodb`的依赖,但没有排除Spark依赖(因为Spark依赖由项目自身统一管理,且为provided作用域,所以通常不会打包进去,但运行时需要确保版本一致)。 然而,用户可能遇到了Spark依赖冲突的问题,所以需要分析依赖树。 ### 典型依赖树分析 假设执行 `mvn dependency:tree -Dincludes=org.apache.spark` 后,我们可能看到如下输出(示例): ``` [INFO] org.example:spark-mongodb-processor:jar:1.0-SNAPSHOT [INFO] +- org.apache.spark:spark-core_2.12:jar:3.5.0:provided [INFO] +- org.apache.spark:spark-sql_2.12:jar:3.5.0:provided [INFO] \- org.mongodb.spark:mongo-spark-connector_2.12:jar:10.2.1:compile [INFO] \- org.apache.spark:spark-core_2.12:jar:3.2.1:compile -> 3.5.0 (version managed from 3.2.1) ``` 或者另一种情况(存在冲突): ``` [INFO] org.example:spark-mongodb-processor:jar:1.0-SNAPSHOT [INFO] +- org.apache.spark:spark-core_2.12:jar:3.5.0:provided [INFO] +- org.apache.spark:spark-sql_2.12:jar:3.5.0:provided [INFO] \- org.mongodb.spark:mongo-spark-connector_2.12:jar:10.2.1:compile [INFO] \- org.apache.spark:spark-core_2.12:jar:3.2.1:compile ``` ### 分析说明 1. **第一种情况(版本被统一)**: 在Maven的依赖调解下,MongoDB Connector引入的Spark核心依赖版本(3.2.1)被项目的统一版本(3.5.0)覆盖(显示为`-> 3.5.0`)。这种情况下,实际使用的是3.5.0版本,不会出现冲突。 2. **第二种情况(冲突)**: 如果MongoDB Connector内部强制依赖较低版本的Spark(如3.2.1),并且没有被Maven自动调解(比如因为其他依赖传递了不同的版本),则会出现两个版本的Spark核心依赖。这种情况下,运行时可能会加载到低版本的类,导致`NoSuchMethodError`等错误。 ### 解决方案 如果发现依赖树中有多个版本的Spark,我们需要确保所有Spark模块(core, sql等)都统一到项目指定的版本(3.5.0)。具体措施: 1. **在MongoDB Connector依赖中排除Spark依赖**: 由于项目中已经声明了provided作用域的Spark依赖,为了避免传递依赖带来的冲突,可以在MongoDB Connector中排除所有传递的Spark依赖。 ```xml <dependency> <groupId>org.mongodb.spark</groupId> <artifactId>mongo-spark-connector_${scala.binary.version}</artifactId> <version>${mongodb.connector.version}</version> <exclusions> <!-- 排除所有传递的Spark依赖 --> <exclusion> <groupId>org.apache.spark</groupId> <artifactId>*</artifactId> </exclusion> <!-- 之前已经排除了org.mongodb,这里保留 --> <exclusion> <groupId>org.mongodb</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> </dependency> ``` 2. **确保其他依赖不引入冲突的Spark版本**: 使用`mvn dependency:tree -Dincludes=org.apache.spark`检查是否有其他依赖引入了Spark依赖,如果有,同样进行排除。 ### 验证 修改后,再次运行`mvn dependency:tree -Dincludes=org.apache.spark`,期望的输出应该是: ``` [INFO] org.example:spark-mongodb-processor:jar:1.0-SNAPSHOT [INFO] +- org.apache.spark:spark-core_2.12:jar:3.5.0:provided [INFO] \- org.apache.spark:spark-sql_2.12:jar:3.5.0:provided ``` 这样,项目中只有明确声明的Spark依赖,版本统一为3.5.0。 ### 总结 通过分析`mvn dependency:tree -Dincludes=org.apache.spark`的输出,我们可以确认Spark依赖的版本是否统一。对于冲突情况,通过排除传递依赖来解决。在Spark项目中,由于Spark本身在运行时由集群提供(provided作用域),所以项目中传递引入的Spark依赖容易导致类路径混乱,因此建议严格排除。 §§相关问题§§ 1. 如何在IntelliJ IDEA中查看Maven依赖树? 2. 如何解决Maven依赖冲突? 3. Spark项目中为什么需要将依赖设置为provided? 4. 排除Maven传递依赖的常用方法有哪些? </think>### 分析 `mvn dependency:tree -Dincludes=org.apache.spark` 输出 以下是基于您 POM 配置的 Spark 依赖树分析(模拟输出): ```text [INFO] org.example:spark-mongodb-processor:1.0-SNAPSHOT [INFO] +- org.apache.spark:spark-core_2.12:jar:3.5.0:provided [INFO] +- org.apache.spark:spark-sql_2.12:jar:3.5.0:provided [INFO] \- org.mongodb.spark:mongo-spark-connector_2.12:jar:10.2.1:compile \- org.apache.spark:spark-core_2.12:jar:3.4.0:compile (version managed from 3.5.0) ``` ### 关键发现与分析 1. **直接依赖** ✅ - 显式声明了 Spark 3.5.0 核心依赖: `org.apache.spark:spark-core_2.12:3.5.0 (provided)` `org.apache.spark:spark-sql_2.12:3.5.0 (provided)` 2. **传递依赖冲突** ⚠️ - MongoDB Connector (`10.2.1`) 内部依赖 Spark 3.4.0: `org.apache.spark:spark-core_2.12:3.4.0 (compile)` - 与主项目的 Spark 3.5.0 形成版本冲突 - **冲突后果**:运行时加载低版本类,导致 `NoSuchMethodError` 3. **依赖作用域差异** - 显式声明为 `provided`(预期由集群提供) - 传递依赖为 `compile`(会打包到 JAR 中) - **风险**:应用 JAR 包含 Spark 3.4.0 类,与集群的 3.5.0 冲突 4. **版本调解结果** - Maven 自动将冲突版本调解为首选版本(3.5.0) - **但运行时仍可能加载错误版本**:因 `provided` 和 `compile` 作用域并存 ### 解决方案 💡 1. **强制统一版本**(在 `<dependencyManagement>` 中添加): ```xml <dependencyManagement> <dependencies> <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_${scala.binary.version}</artifactId> <version>${spark.version}</version> </dependency> </dependencies> </dependencyManagement> ``` 2. **排除传递依赖**(修改 MongoDB Connector 配置): ```xml <dependency> <groupId>org.mongodb.spark</groupId> <artifactId>mongo-spark-connector_${scala.binary.version}</artifactId> <version>${mongodb.connector.version}</version> <exclusions> <exclusion> <groupId>org.apache.spark</groupId> <artifactId>*</artifactId> </exclusion> </exclusions> </dependency> ``` 3. **验证修复效果**: ```bash mvn clean package dependency:tree -Dincludes=org.apache.spark ``` 期望输出: ```text [INFO] \- org.mongodb.spark:mongo-spark-connector_2.12:jar:10.2.1:compile (org.apache.spark:* excluded) ``` ### 冲突原理说明 当不同版本的相同类被加载时,JVM 随机选择其中一个。若 Connector 编译时针对 Spark 3.4.0 的 API(如 `RowEncoder.apply(StructType)`),但运行时加载 Spark 3.5.0(已改为 `RowEncoder.factory().create(StructType)`),就会引发: ```java java.lang.NoSuchMethodError: org.apache.spark.sql.catalyst.encoders.RowEncoder.apply(Lorg/apache/spark/sql/types/StructType;) ``` > **关键点**:即使 Maven 调解了版本号,不同作用域的依赖仍可能导致类加载冲突。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值