暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

spark内核源码深度剖析(十一):DAGScheduler原理剖析与源码分析

程序员雨衣 2019-10-21
415

流程图

源码

入口

  1. // 调用SparkContext,之前初始化时创建的dagScheduler的runJob()方法

  2. dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, allowLocal,

  3. resultHandler, localProperties.get)

看看runJob方法

  1. def runJob[T, U: ClassTag](

  2. rdd: RDD[T],

  3. func: (TaskContext, Iterator[T]) => U,

  4. partitions: Seq[Int],

  5. callSite: CallSite,

  6. allowLocal: Boolean,

  7. resultHandler: (Int, U) => Unit,

  8. properties: Properties = null)

  9. {

  10. val start = System.nanoTime

  11. val waiter = submitJob(rdd, func, partitions, callSite, allowLocal, resultHandler, properties)

  12. waiter.awaitResult() match {

  13. case JobSucceeded => {

  14. logInfo("Job %d finished: %s, took %f s".format

  15. (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))

  16. }

  17. case JobFailed(exception: Exception) =>

  18. logInfo("Job %d failed: %s, took %f s".format

  19. (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))

  20. throw exception

  21. }

  22. }

看看submitJob()方法

  1. def submitJob[T, U](

  2. rdd: RDD[T],

  3. func: (TaskContext, Iterator[T]) => U,

  4. partitions: Seq[Int],

  5. callSite: CallSite,

  6. allowLocal: Boolean,

  7. resultHandler: (Int, U) => Unit,

  8. properties: Properties = null): JobWaiter[U] =

  9. {

  10. // Check to make sure we are not launching a task on a partition that does not exist.

  11. val maxPartitions = rdd.partitions.length

  12. partitions.find(p => p >= maxPartitions || p < 0).foreach { p =>

  13. throw new IllegalArgumentException(

  14. "Attempting to access a non-existent partition: " + p + ". " +

  15. "Total number of partitions: " + maxPartitions)

  16. }


  17. val jobId = nextJobId.getAndIncrement()

  18. if (partitions.size == 0) {

  19. return new JobWaiter[U](this, jobId, 0, resultHandler)

  20. }


  21. assert(partitions.size > 0)

  22. val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]

  23. val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)

  24. eventProcessLoop.post(JobSubmitted(

  25. jobId, rdd, func2, partitions.toArray, allowLocal, callSite, waiter, properties))

  26. waiter

  27. }

看看DAGSchedulerEventProcessLoop(eventProcessLoop)的JobSubmitted

  1. private[scheduler] class DAGSchedulerEventProcessLoop(dagScheduler: DAGScheduler)

  2. extends EventLoop[DAGSchedulerEvent]("dag-scheduler-event-loop") with Logging {


  3. /**

  4. * The main event loop of the DAG scheduler.

  5. */

  6. override def onReceive(event: DAGSchedulerEvent): Unit = event match {

  7. case JobSubmitted(jobId, rdd, func, partitions, allowLocal, callSite, listener, properties) =>

  8. dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, allowLocal, callSite,

  9. listener, properties)

看看dagScheduler.handleJobSubmitted()方法

  1. /**

  2. * DAGScheduler的job调度的核心入口

  3. */

  4. private[scheduler] def handleJobSubmitted(jobId: Int,

  5. finalRDD: RDD[_],

  6. func: (TaskContext, Iterator[_]) => _,

  7. partitions: Array[Int],

  8. allowLocal: Boolean,

  9. callSite: CallSite,

  10. listener: JobListener,

  11. properties: Properties = null)

  12. {

  13. // 第一步,使用触发job的最后一个RDD,创建finalStage

  14. var finalStage: Stage = null

  15. try {

  16. // New stage creation may throw an exception if, for example, jobs are run on a

  17. // HadoopRDD whose underlying HDFS files have been deleted.

  18. // 创建一个stage对象,并且将stage加入DAGScheduler内部缓存中

  19. finalStage = newStage(finalRDD, partitions.size, None, jobId, callSite)

  20. } catch {

  21. case e: Exception =>

  22. logWarning("Creating new stage failed due to exception - job: " + jobId, e)

  23. listener.jobFailed(e)

  24. return

  25. }

  26. if (finalStage != null) {

  27. // 第二步,用finalStage创建一个job,这个job的最后一个stage,就是finalStage

  28. val job = new ActiveJob(jobId, finalStage, func, partitions, callSite, listener, properties)

  29. clearCacheLocs()

  30. logInfo("Got job %s (%s) with %d output partitions (allowLocal=%s)".format(

  31. job.jobId, callSite.shortForm, partitions.length, allowLocal))

  32. logInfo("Final stage: " + finalStage + "(" + finalStage.name + ")")

  33. logInfo("Parents of final stage: " + finalStage.parents)

  34. logInfo("Missing parents: " + getMissingParentStages(finalStage))

  35. val shouldRunLocally =

  36. localExecutionEnabled && allowLocal && finalStage.parents.isEmpty && partitions.length == 1

  37. val jobSubmissionTime = clock.getTimeMillis()

  38. if (shouldRunLocally) {

  39. // Compute very short actions like first() or take() with no parent stages locally.

  40. listenerBus.post(

  41. SparkListenerJobStart(job.jobId, jobSubmissionTime, Seq.empty, properties))

  42. runLocally(job)

  43. } else {

  44. // 第三步,将job加入内存缓存中

  45. jobIdToActiveJob(jobId) = job

  46. activeJobs += job

  47. finalStage.resultOfJob = Some(job)

  48. val stageIds = jobIdToStageIds(jobId).toArray

  49. val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))

  50. listenerBus.post(

  51. SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))

  52. // 第四步,使用submitStage()方法提交finalStage

  53. // 这个方法的调用,其实会导致第一个stage提交,并且导致其他所有的stage,都给放入waitingStages队列里了

  54. submitStage(finalStage)

  55. // stage划分算法,实在太重要了,必须对stage划分算法很清晰,知道自己编写的spark application被划分了几个job,每个job被划分成了几个stage

  56. // 每个stage,包括了你的那些代码,只有知道了那个stage包括了哪些自己的代码之后,在线上,如果发现某个stage执行特别慢

  57. // 或者某个stage一直报错,才能针对那个stage对应的代码,去排查问题,或者是性能调优


  58. // stage划分算法总结

  59. // 1. 从finalStage倒推

  60. // 2. 通过宽依赖,来进行新的stage划分

  61. // 3. 使用递归,优先提交父stage

  62. }

  63. }

  64. // 提交等待的stage

  65. submitWaitingStages()

  66. }

看看submitStage()方法

  1. // 提交stage的方法

  2. // 这其实就是stage划分算法的入口,但是,stage划分算法,其实是由submitStage()和getMissingParentStages()方法共同组成的

  3. private def submitStage(stage: Stage) {

  4. val jobId = activeJobForStage(stage)

  5. if (jobId.isDefined) {

  6. logDebug("submitStage(" + stage + ")")

  7. if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {

  8. // 调用getMissingParentStages()去获取当前这个stage的父stage

  9. val missing = getMissingParentStages(stage).sortBy(_.id)

  10. logDebug("missing: " + missing)

  11. // 这里其实会反复递归调用,直到最初的stage,它没有父stage了,那么,此时,就会首先提交这个第一个stage,stage0

  12. // 其余的stage,此时,全部都在waitingStages里面

  13. if (missing == Nil) {

  14. logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")

  15. submitMissingTasks(stage, jobId.get)

  16. } else {

  17. // 递归调用submitStage()方法,去提交父stage

  18. // 这里的递归,就是stage划分算法的推动者和精髓

  19. for (parent <- missing) {

  20. submitStage(parent)

  21. }

  22. // 并且将当前stage放入waitingStages等待执行的stage队列中

  23. waitingStages += stage

  24. }

  25. }

  26. } else {

  27. abortStage(stage, "No active job for stage " + stage.id)

  28. }

  29. }

看看getMissingParentStages()

  1. // 获取某个stage的父stage

  2. // 这个方法的意思,就是说,对于一个stage,如果它的最后一个rdd的所有依赖,都是窄依赖,那么就不会创建任何新的stage

  3. // 但是,只要发现这个stage的rdd宽依赖了某个rdd,那么就用宽依赖的那个rdd,创建一个新的stage,然后立即将新的stage返回

  4. private def getMissingParentStages(stage: Stage): List[Stage] = {

  5. val missing = new HashSet[Stage]

  6. val visited = new HashSet[RDD[_]]

  7. // We are manually maintaining a stack here to prevent StackOverflowError

  8. // caused by recursively visiting

  9. val waitingForVisit = new Stack[RDD[_]]

  10. def visit(rdd: RDD[_]) {

  11. if (!visited(rdd)) {

  12. visited += rdd

  13. if (getCacheLocs(rdd).contains(Nil)) {

  14. // 遍历rdd的依赖

  15. // 所以说,针对之前那个流程图,其实对于每一种有shuffle的操作,比如groupByKey、reduceByKey、countByKey

  16. // 等操作,底层对应了三个RDD,MapPartitionsRDD、ShuffleRDD、MapPartitionsRDD,会划分为两个stage

  17. for (dep <- rdd.dependencies) {

  18. dep match {

  19. // 如果是宽依赖

  20. case shufDep: ShuffleDependency[_, _, _] =>

  21. // 那么使用宽依赖的那个rdd,创建一个stage,并且会将isShuffleMap设置为true

  22. // 默认最后一个stage,不是shuffleMap stage,但是finalStage之前所有的stage,都是shuffleMap stage

  23. val mapStage = getShuffleMapStage(shufDep, stage.jobId)

  24. if (!mapStage.isAvailable) {

  25. missing += mapStage

  26. }

  27. // 如果是窄依赖,那么将依赖的rdd放入栈中

  28. case narrowDep: NarrowDependency[_] =>

  29. waitingForVisit.push(narrowDep.rdd)

  30. }

  31. }

  32. }

  33. }

  34. }

  35. // 首先往栈中,推入了stage的最后一个rdd

  36. waitingForVisit.push(stage.rdd)

  37. // 进行while循环

  38. while (!waitingForVisit.isEmpty) {

  39. // 对stage的最后一个rdd,调用自己内部定义的visit()方法

  40. visit(waitingForVisit.pop())

  41. }

  42. missing.toList

  43. }

看看submitMissingTasks()

  1. // 提交stage,为stage创建一批task,task数量与partition数量相同

  2. private def submitMissingTasks(stage: Stage, jobId: Int) {

  3. logDebug("submitMissingTasks(" + stage + ")")

  4. // Get our pending tasks and remember them in our pendingTasks entry

  5. stage.pendingTasks.clear()


  6. // First figure out the indexes of partition ids to compute.

  7. // 获取你要创建的task的数量

  8. val partitionsToCompute: Seq[Int] = {

  9. if (stage.isShuffleMap) {

  10. (0 until stage.numPartitions).filter(id => stage.outputLocs(id) == Nil)

  11. } else {

  12. val job = stage.resultOfJob.get

  13. (0 until job.numPartitions).filter(id => !job.finished(id))

  14. }

  15. }


  16. val properties = if (jobIdToActiveJob.contains(jobId)) {

  17. jobIdToActiveJob(stage.jobId).properties

  18. } else {

  19. // this stage will be assigned to "default" pool

  20. null

  21. }


  22. // 将stage加入runningStages队列

  23. runningStages += stage

  24. // SparkListenerStageSubmitted should be posted before testing whether tasks are

  25. // serializable. If tasks are not serializable, a SparkListenerStageCompleted event

  26. // will be posted, which should always come after a corresponding SparkListenerStageSubmitted

  27. // event.

  28. stage.latestInfo = StageInfo.fromStage(stage, Some(partitionsToCompute.size))

  29. outputCommitCoordinator.stageStart(stage.id)

  30. listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))


  31. // TODO: Maybe we can keep the taskBinary in Stage to avoid serializing it multiple times.

  32. // Broadcasted binary for the task, used to dispatch tasks to executors. Note that we broadcast

  33. // the serialized copy of the RDD and for each task we will deserialize it, which means each

  34. // task gets a different copy of the RDD. This provides stronger isolation between tasks that

  35. // might modify state of objects referenced in their closures. This is necessary in Hadoop

  36. // where the JobConf/Configuration object is not thread-safe.

  37. var taskBinary: Broadcast[Array[Byte]] = null

  38. try {

  39. // For ShuffleMapTask, serialize and broadcast (rdd, shuffleDep).

  40. // For ResultTask, serialize and broadcast (rdd, func).

  41. val taskBinaryBytes: Array[Byte] =

  42. if (stage.isShuffleMap) {

  43. closureSerializer.serialize((stage.rdd, stage.shuffleDep.get) : AnyRef).array()

  44. } else {

  45. closureSerializer.serialize((stage.rdd, stage.resultOfJob.get.func) : AnyRef).array()

  46. }

  47. taskBinary = sc.broadcast(taskBinaryBytes)

  48. } catch {

  49. // In the case of a failure during serialization, abort the stage.

  50. case e: NotSerializableException =>

  51. abortStage(stage, "Task not serializable: " + e.toString)

  52. runningStages -= stage

  53. return

  54. case NonFatal(e) =>

  55. abortStage(stage, s"Task serialization failed: $e\n${e.getStackTraceString}")

  56. runningStages -= stage

  57. return

  58. }


  59. // 为stage创建指定数量的task

  60. // 这里很关键的一点是,task的最佳位置计算算法

  61. val tasks: Seq[Task[_]] = if (stage.isShuffleMap) {

  62. partitionsToCompute.map { id =>

  63. // 给每一个partition创建一个task,给每个task计算最佳位置

  64. val locs = getPreferredLocs(stage.rdd, id)

  65. val part = stage.rdd.partitions(id)

  66. // 对于finalStage之外的stage,它的isShuffleMap都是true,所以会创建ShuffleMapTask

  67. new ShuffleMapTask(stage.id, taskBinary, part, locs)

  68. }

  69. } else {

  70. // 如果不是shuffleMap,那么就是finalStage,finalStage是创建ResultTask

  71. val job = stage.resultOfJob.get

  72. partitionsToCompute.map { id =>

  73. val p: Int = job.partitions(id)

  74. val part = stage.rdd.partitions(p)

  75. val locs = getPreferredLocs(stage.rdd, p)

  76. new ResultTask(stage.id, taskBinary, part, locs, id)

  77. }

  78. }


  79. if (tasks.size > 0) {

  80. logInfo("Submitting " + tasks.size + " missing tasks from " + stage + " (" + stage.rdd + ")")

  81. stage.pendingTasks ++= tasks

  82. logDebug("New pending tasks: " + stage.pendingTasks)

  83. // 最后,针对stage的task,创建TaskSet对象,调用taskScheduler的submitTasks()方法,提交taskSet

  84. taskScheduler.submitTasks(

  85. new TaskSet(tasks.toArray, stage.id, stage.newAttemptId(), stage.jobId, properties))

  86. stage.latestInfo.submissionTime = Some(clock.getTimeMillis())

  87. } else {

  88. // Because we posted SparkListenerStageSubmitted earlier, we should post

  89. // SparkListenerStageCompleted here in case there are no tasks to run.

  90. outputCommitCoordinator.stageEnd(stage.id)

  91. listenerBus.post(SparkListenerStageCompleted(stage.latestInfo))

  92. logDebug("Stage " + stage + " is actually done; %b %d %d".format(

  93. stage.isAvailable, stage.numAvailableOutputs, stage.numPartitions))

  94. runningStages -= stage

  95. }

  96. }

看看getPreferredLocs()方法

  1. private[spark]

  2. def getPreferredLocs(rdd: RDD[_], partition: Int): Seq[TaskLocation] = {

  3. getPreferredLocsInternal(rdd, partition, new HashSet)

  4. }

继续看getPreferredLocsInternal()方法

  1. /**

  2. * 计算每个task对应的partition的最佳位置,说白了,就是从stage的最后一个rdd开始,去找哪个rdd的partition,是被cache了,或者checkpoint了

  3. * 那么,task的最佳位置,就是缓存的/checkpoint的partition的位置

  4. * 因为这样的话,task就在哪个节点上执行,不需要计算之前的rdd了

  5. */

  6. private def getPreferredLocsInternal(

  7. rdd: RDD[_],

  8. partition: Int,

  9. visited: HashSet[(RDD[_],Int)])

  10. : Seq[TaskLocation] =

  11. {

  12. // If the partition has already been visited, no need to re-visit.

  13. // This avoids exponential path exploration. SPARK-695

  14. if (!visited.add((rdd,partition))) {

  15. // Nil has already been returned for previously visited partitions.

  16. return Nil

  17. }

  18. // If the partition is cached, return the cache locations

  19. // 寻找当前pdd的partiton是否缓存了

  20. val cached = getCacheLocs(rdd)(partition)

  21. if (!cached.isEmpty) {

  22. return cached

  23. }

  24. // If the RDD has some placement preferences (as is the case for input RDDs), get those

  25. // 寻找当前rdd的partition是否checkpoint了

  26. val rddPrefs = rdd.preferredLocations(rdd.partitions(partition)).toList

  27. if (!rddPrefs.isEmpty) {

  28. return rddPrefs.map(TaskLocation(_))

  29. }

  30. // If the RDD has narrow dependencies, pick the first partition of the first narrow dep

  31. // that has any placement preferences. Ideally we would choose based on transfer sizes,

  32. // but this will do for now.

  33. // 最后,递归调用自己,去寻找rdd的父rdd,看看对应的partition是否缓存或者checkpoint了

  34. rdd.dependencies.foreach {

  35. case n: NarrowDependency[_] =>

  36. for (inPart <- n.getParents(partition)) {

  37. val locs = getPreferredLocsInternal(n.rdd, inPart, visited)

  38. if (locs != Nil) {

  39. return locs

  40. }

  41. }

  42. case _ =>

  43. }

  44. // 如果这个stage,从最后一个rdd,到最开始的rdd,partition都没有被缓存或者checkpoint,那么task的最佳位置(PreferredLocs),就是Nil


  45. Nil

  46. }


文章转载自程序员雨衣,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论