7.DAGScheduler的stage算法划分和TaskScheduler的task算法划分

先来一张图描述整个stage算法划分的由来:


7.DAGScheduler的stage算法划分和TaskScheduler的task算法划分_第1张图片

先从DAGScheduler的入口开始 , 源码如下 :
     
     
     
     
  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划分算法非常重要 , 对于spark高手来说必须对stage划分算法很清晰
  56. // 知道自己编写的spark application被划分为几个job
  57. // 每个job被划分为几个stage
  58. // 每个stage又包括了哪些代码
  59. // 只有知道了这些情况才能发现某个具体的stage执行特别慢或者报错 , 最后才能排查问题 , 性能调优
  60. // stage划分算法总结 :
  61. // 1.从finalStage倒推
  62. // 2.通过宽依赖来进行新的stage的划分
  63. // 3.使用递归优先提交父stage
  64. }
  65. }
  66. // 提交等待的stage
  67. submitWaitingStages()
  68. }

解释一下第一步和第二步 , 从我们编写程序的一个action算子开始往前倒推 , 最后一个RDD肯定会被划分在finalStage里面 , 并且最后一个RDD与父RDD之间的依赖关系肯定是窄依赖关系 , 因为stage划分的原则就是rdd与父rdd之间为宽依赖的时候就会被划分在不同的stage中,第二步就是将这个finalStage加入到该job缓存队列中 . 
关键在于第四步的submitStage方法 , 该方法会不断的从finalStage递归调用submitStage方法将stage加入 waitingStages缓存队列中 , 源码如下:
     
     
     
     
  1. /**
  2. * 提交stage的方法
  3. * 这个其实是stage的划分算法的入口
  4. * 但是stage划分算法其实就是submitStage()方法与getMissingParentStage()方法共同组成的
  5. */
  6. private def submitStage(stage: Stage) {
  7. val jobId = activeJobForStage(stage)
  8. if (jobId.isDefined) {
  9. logDebug("submitStage(" + stage + ")")
  10. if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
  11. // 调用getMissingParentStages方法获取当前stage的父stage
  12. val missing = getMissingParentStages(stage).sortBy(_.id)
  13. logDebug("missing: " + missing)
  14. // 这里会反复递归调用直到最初的stage没有父stage了 , 那么此时就会去首先提交第一个stage
  15. // 其余的stage此时全部都在waitingStages中
  16. if (missing == Nil) {
  17. logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
  18. submitMissingTasks(stage, jobId.get)
  19. } else {
  20. // 递归调用submit()方法去提交父stage , 这里的递归就是stage划分算法的推动者和精髓
  21. for (parent <- missing) {
  22. submitStage(parent)
  23. }
  24. // 并且将当前stage加入waitingStages等待执行的stage的队列中
  25. waitingStages += stage
  26. }
  27. }
  28. } else {
  29. abortStage(stage, "No active job for stage " + stage.id)
  30. }
  31. }
若是 getMissingParentStages方法获取到了当前stage的父stage(其实是遍历stage中RDD的依赖关系)不为Nil则将stage加入缓存队列并继续将stage作为参数调用subMitMissingTasks方法直到父stage为Nil .
getMissingParentStages方法获取stage的父stage , 源码如下:
      
      
      
      
  1. /**
  2. * stage的划分算法核心就在这里
  3. * 获取某个stage的父stage
  4. * 这个方法其实就是对最后的一个rdd的所有依赖都是窄依赖 , 那么就不会创建新的stage
  5. * 只要发现这个stage的rdd宽依赖了某个rdd,那么就用宽依赖的那个rdd创建一个新的stage
  6. * 然后立即将新的stage返回
  7. */
  8. private def getMissingParentStages(stage: Stage): List[Stage] = {
  9. val missing = new HashSet[Stage]
  10. val visited = new HashSet[RDD[_]]
  11. // We are manually maintaining a stack here to prevent StackOverflowError
  12. // caused by recursively visiting
  13. val waitingForVisit = new Stack[RDD[_]]
  14. // 自定义的visit方法
  15. def visit(rdd: RDD[_]) {
  16. if (!visited(rdd)) {
  17. visited += rdd
  18. if (getCacheLocs(rdd).contains(Nil)) {
  19. // 遍历rdd的依赖
  20. // 所以说 , 针对我们之前的那个图来看其实对于每一种有shuffle的操作,比如groupByKey , reduceByKey , countByKey ,
  21. // 底层对应了三个RDD : MapPartitionRDD , shuffleRDD , MapPartitionsRDD
  22. for (dep <- rdd.dependencies) {
  23. dep match {
  24. // 如果是宽依赖 , 那么使用宽依赖的那个rdd创建一个stage , 并且会将isShuffleMap设置为true
  25. // 默认最后一个stage不是shufflemap stage ,但是finaStage之前所有的stage都是shuffleMap stage
  26. case shufDep: ShuffleDependency[_, _, _] =>
  27. val mapStage = getShuffleMapStage(shufDep, stage.jobId)
  28. if (!mapStage.isAvailable) {
  29. missing += mapStage
  30. }
  31. // 如果是窄依赖 , 那么将依赖的rdd放入栈中
  32. case narrowDep: NarrowDependency[_] =>
  33. waitingForVisit.push(narrowDep.rdd)
  34. }
  35. }
  36. }
  37. }
  38. }
  39. // 首先往栈中推入了一个stage的最后一个RDD
  40. waitingForVisit.push(stage.rdd)
  41. // 然后进行while循环
  42. while (!waitingForVisit.isEmpty) {
  43. // 对stage的最后一个rdd调用自己定义的visit方法
  44. visit(waitingForVisit.pop())
  45. }
  46. missing.toList
  47. }

关键点就是那个for循环了  , 遍历Stage中RDD的依赖关系 , 若是宽依赖则创建新的stage并设置isShuffle变量为true,反之则加入rdd栈中 , 在返回的stage中判断是否为Nil , 若是Nil的话就调用 submitMissingTasks方法 , 告诉TaskScheduler提交task , 源码如下:
       
       
       
       
  1. /**
  2. * 提交stage , 为stage创建一批task , task数量与partition数量相同
  3. */
  4. private def submitMissingTasks(stage: Stage, jobId: Int) {
  5. logDebug("submitMissingTasks(" + stage + ")")
  6. // Get our pending tasks and remember them in our pendingTasks entry
  7. stage.pendingTasks.clear()
  8. // First figure out the indexes of partition ids to compute.
  9. // 获取你要创建的task的数量
  10. val partitionsToCompute: Seq[Int] = {
  11. if (stage.isShuffleMap) {
  12. (0 until stage.numPartitions).filter(id => stage.outputLocs(id) == Nil)
  13. } else {
  14. val job = stage.resultOfJob.get
  15. (0 until job.numPartitions).filter(id => !job.finished(id))
  16. }
  17. }
  18. val properties = if (jobIdToActiveJob.contains(jobId)) {
  19. jobIdToActiveJob(stage.jobId).properties
  20. } else {
  21. // this stage will be assigned to "default" pool
  22. null
  23. }
  24. // 将stage加入runningStages队列
  25. runningStages += stage
  26. // SparkListenerStageSubmitted should be posted before testing whether tasks are
  27. // serializable. If tasks are not serializable, a SparkListenerStageCompleted event
  28. // will be posted, which should always come after a corresponding SparkListenerStageSubmitted
  29. // event.
  30. stage.latestInfo = StageInfo.fromStage(stage, Some(partitionsToCompute.size))
  31. outputCommitCoordinator.stageStart(stage.id)
  32. listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))
  33. // TODO: Maybe we can keep the taskBinary in Stage to avoid serializing it multiple times.
  34. // Broadcasted binary for the task, used to dispatch tasks to executors. Note that we broadcast
  35. // the serialized copy of the RDD and for each task we will deserialize it, which means each
  36. // task gets a different copy of the RDD. This provides stronger isolation between tasks that
  37. // might modify state of objects referenced in their closures. This is necessary in Hadoop
  38. // where the JobConf/Configuration object is not thread-safe.
  39. var taskBinary: Broadcast[Array[Byte]] = null
  40. try {
  41. // For ShuffleMapTask, serialize and broadcast (rdd, shuffleDep).
  42. // For ResultTask, serialize and broadcast (rdd, func).
  43. val taskBinaryBytes: Array[Byte] =
  44. if (stage.isShuffleMap) {
  45. closureSerializer.serialize((stage.rdd, stage.shuffleDep.get) : AnyRef).array()
  46. } else {
  47. closureSerializer.serialize((stage.rdd, stage.resultOfJob.get.func) : AnyRef).array()
  48. }
  49. taskBinary = sc.broadcast(taskBinaryBytes)
  50. } catch {
  51. // In the case of a failure during serialization, abort the stage.
  52. case e: NotSerializableException =>
  53. abortStage(stage, "Task not serializable: " + e.toString)
  54. runningStages -= stage
  55. return
  56. case NonFatal(e) =>
  57. abortStage(stage, s"Task serialization failed: $e\n${e.getStackTraceString}")
  58. runningStages -= stage
  59. return
  60. }
  61. // 为stage创建指定数量的task
  62. // 这里有一点很关键就是task的最佳位置计算算法
  63. val tasks: Seq[Task[_]] = if (stage.isShuffleMap) {
  64. partitionsToCompute.map { id =>
  65. // 给每一个partition创建一个task
  66. // 给每个task计算最佳位置
  67. val locs = getPreferredLocs(stage.rdd, id)
  68. val part = stage.rdd.partitions(id)
  69. // 然后对于finalStage之外的stage , 它的shuffleMap都是true , 所以会创建shuffleMapTask
  70. new ShuffleMapTask(stage.id, taskBinary, part, locs)
  71. }
  72. } else {
  73. // 如果不是shuffleMap , 那么就是finalStage , final Stage是创建ResultTask的
  74. val job = stage.resultOfJob.get
  75. partitionsToCompute.map { id =>
  76. val p: Int = job.partitions(id)
  77. val part = stage.rdd.partitions(p)
  78. val locs = getPreferredLocs(stage.rdd, p)
  79. new ResultTask(stage.id, taskBinary, part, locs, id)
  80. }
  81. }
  82. if (tasks.size > 0) {
  83. logInfo("Submitting " + tasks.size + " missing tasks from " + stage + " (" + stage.rdd + ")")
  84. stage.pendingTasks ++= tasks
  85. logDebug("New pending tasks: " + stage.pendingTasks)
  86. // 最后 , 针对stage的task创建TaskSet对象 , 调用TaskScheduler的submitTasks方法提交TaskSet
  87. // 默认情况下Standalone模式使用的是TaskSchedulerImpl , TaskScheduler只是一个TaskSchusterImpl的接口
  88. taskScheduler.submitTasks(
  89. new TaskSet(tasks.toArray, stage.id, stage.newAttemptId(), stage.jobId, properties))
  90. stage.latestInfo.submissionTime = Some(clock.getTimeMillis())
  91. } else {
  92. // Because we posted SparkListenerStageSubmitted earlier, we should post
  93. // SparkListenerStageCompleted here in case there are no tasks to run.
  94. outputCommitCoordinator.stageEnd(stage.id)
  95. listenerBus.post(SparkListenerStageCompleted(stage.latestInfo))
  96. logDebug("Stage " + stage + " is actually done; %b %d %d".format(
  97. stage.isAvailable, stage.numAvailableOutputs, stage.numPartitions))
  98. runningStages -= stage
  99. }
  100. }

我们在返回到 submitStage方法中,将返回的stage在加入waitingStages中  , 最后在 入口方法handleJobSubmitted()将所有的缓存队列中的stage全部提交 ; 

总结一下:
job:一个job代表的是action之前的所有transformatio操作
stage划分算法原理:该算法是在DAGScheduler中操作的 , 在执行action操作的那个RDD为最后一个RDD ,为它创建一个stage ,  将该RDD往前倒推 , 若是窄依赖则将该RDD加入stage中 , 若是宽依赖则创建一个新的stage , 发现的宽依赖的RDD会加入到这个新创建的stage中 , 然后再次往前倒推 , 直到所有的RDD被遍历完 ,stage以栈的数据结构存储RDD , 也就是说从最后一个RDD开始往前推 , 直到遇到一个宽依赖的RDD , 这之间的RDD都会存储到一个stage中 , 而且最后入栈的RDD是下面task划分算法中最先操作的RDD
也就是说一个job需要划分出多少个stage与action操作之前的RDD宽依赖有多少密切相关 , stage数等于宽依赖数 , 最后action操作前DAGScheduler为最后一个RDD所创建的stage
stage数 = 宽依赖数


接下来就是task的算法划分了!
DAGScheduler在提交stage的时候就会为每一个stage创建指定数量的task,task数量与partition数量相同 , 每个task对应一个partition , 并计算每一个task对应的paritition的最佳位置
task的最佳位置计算是从stage的最后一个入栈的RDD(stage中的栈底)开始 , 先判断该RDD是否有cache和checkpoint ,若没有的话则会去找父RDD是否有cache和checkpoint ,若找到该RDD是有缓存的 , 那么就不用往前找了 , 就直接从该RDD开始计算直到计算到该stage中的栈顶的那个RDD ,  若都没有的话就会走到TaskScheduler去 , 最后对stage的task创建taskset对象 , 调用TaskScheduler的submitTasks()方法提交taskSet;

在DAGScheduler的submitMissingTasks方法中有如下代码就会触发task的提交 ,可见提交task是一批次一批次的提交, 代码如下:
      
      
      
      
  1. taskScheduler.submitTasks(
  2. new TaskSet(tasks.toArray, stage.id, stage.newAttemptId(), stage.jobId, properties))

TaskScheduler只是提交任务的一个接口 , 具体的操作由其实现类TaskSchedulerImpl实现 , submitTasks方法就是TaskScheduler提交任务的入口 , 源码如下:
      
      
      
      
  1. /**
  2. * TaskScheduler提交任务的入口
  3. */
  4. override def submitTasks(taskSet: TaskSet) {
  5. val tasks = taskSet.tasks
  6. logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
  7. this.synchronized {
  8. // 给每一TaskSet都会创建一个TaskSetManager
  9. // TaskSetManager实际上在后面会负责它的那个TaskSet的任务执行状况的监视和管理
  10. val manager = createTaskSetManager(taskSet, maxTaskFailures)
  11. // 然后加入内存缓存中
  12. activeTaskSets(taskSet.id) = manager
  13. schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
  14. if (!isLocal && !hasReceivedTask) {
  15. starvationTimer.scheduleAtFixedRate(new TimerTask() {
  16. override def run() {
  17. if (!hasLaunchedTask) {
  18. logWarning("Initial job has not accepted any resources; " +
  19. "check your cluster UI to ensure that workers are registered " +
  20. "and have sufficient resources")
  21. } else {
  22. this.cancel()
  23. }
  24. }
  25. }, STARVATION_TIMEOUT, STARVATION_TIMEOUT)
  26. }
  27. hasReceivedTask = true
  28. }
  29. // 在SparkContext原理分析的时候说过创建TaskScheduler的时候就是为TaskSchedulerImpl创建一个SparkDeploySchedulerBackend,这里的backend指的就是之前创建好的SparkDeploySchedulerBackend
  30. // 而且这个backend是负责创建AppClient向master注册Application的
  31. backend.reviveOffers()
  32. }
首先创建TaskSetManager , 它的作用是
在TaskSchedulerImpl中对一个单独的taskSet的任务调度 , 负责追踪每一个task , 如果失败则重试task , 直到超过重试的次数限制 , 并且会通过延迟调度, 并 为这个TaskSet处理本地化调度机制 , 它的主要接口是resourceOffer , 在这个接口中 , TaskSet会希望在一个节点上运行一个任务 , 并且接受任务的状态变化消息来知道它负责的task的状态改变了,一个TaskSetManager

最后执行backend(SparkDeploySchedulerBackend)的reciverOffers()方法 , 源码如下:
       
       
       
       
  1. override def reviveOffers() {
  2. driverActor ! ReviveOffers
  3. }

该方法通过driverActor发送一个ReciverOffers消息 , 然后在receiveWithLogging方法中接收到这个消息 , 代码如下:
        
        
        
        
  1. case ReviveOffers =>
  2. makeOffers()

继续往makeOffers方法深入:
        
        
        
        
  1. // Make fake resource offers on all executors
  2. def makeOffers() {
  3. // 第一步 : 调用TaskSchedulerImpl的resourceOffer方法 , 执行任务分配算法将各个task分配到executor上去
  4. // 第二步 : 分配好task到executor之后执行自己的launchTasks方法 , 将分配的task发送LaunchTask消息到对应的executor上去 , 由executor启动并执行task
  5. // 给resourceOffer方法传入的是这个Application所有可用的executor , 并且将其封装成了WorkerOffer , 每个workerOffer代表了每个executor可用的cpu资源数量
  6. launchTasks(scheduler.resourceOffers(executorDataMap.map { case (id, executorData) =>
  7. new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
  8. }.toSeq))
  9. }

launchTasks方法调用之前会首先调用 resourceOffers, 这个方法就是task算法划分的关键 , 我们看看这个方法:
        
        
        
        
  1. /**
  2. * Called by cluster manager to offer resources on slaves. We respond by asking our active task
  3. * sets for tasks in order of priority. We fill each node with tasks in a round-robin manner so
  4. * that tasks are balanced across the cluster.
  5. */
  6. def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
  7. // Mark each slave as alive and remember its hostname
  8. // Also track if new executor is added
  9. var newExecAvail = false
  10. for (o <- offers) {
  11. executorIdToHost(o.executorId) = o.host
  12. activeExecutorIds += o.executorId
  13. if (!executorsByHost.contains(o.host)) {
  14. executorsByHost(o.host) = new HashSet[String]()
  15. executorAdded(o.executorId, o.host)
  16. newExecAvail = true
  17. }
  18. for (rack <- getRackForHost(o.host)) {
  19. hostsByRack.getOrElseUpdate(rack, new HashSet[String]()) += o.host
  20. }
  21. }
  22. // Randomly shuffle offers to avoid always placing tasks on the same set of workers.
  23. // 首先将可用executor进行shuffle , 也就是打散尽量做到负载均衡
  24. val shuffledOffers = Random.shuffle(offers)
  25. // Build a list of tasks to assign to each worker.
  26. // 然后针对WorkerOffer创建出一堆需要用的东西
  27. // 比如tasks , 很重要 , 它可以理解为一个二位数组ArrayBuffer , 元素又是一个ArrayBuffer
  28. // 并且每个子ArrayBuffer的数量是固定的 , 也就是这个executor可用的cpu数量
  29. val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
  30. val availableCpus = shuffledOffers.map(o => o.cores).toArray
  31. // 从rootPool中取出了排序的TaskSet , 之前说了TaskScheduler初始化的时候创建完TaskSchedulerImpl,SparkDeploySchedulerBackend之后执行一个initialize方法
  32. // 在这个方法中会创建一个调度池
  33. // 这里相当于是所有提交的TaskSet会先放入调度池 , 然后在执行task分配算法的时候会从这个调度池中取出排好队的TaskSet
  34. val sortedTaskSets = rootPool.getSortedTaskSetQueue
  35. for (taskSet <- sortedTaskSets) {
  36. logDebug("parentName: %s, name: %s, runningTasks: %s".format(
  37. taskSet.parent.name, taskSet.name, taskSet.runningTasks))
  38. if (newExecAvail) {
  39. taskSet.executorAdded()
  40. }
  41. }
  42. // Take each TaskSet in our scheduling order, and then offer it each node in increasing order
  43. // of locality levels so that it gets a chance to launch local tasks on all of them.
  44. // NOTE: the preferredLocality order: PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY
  45. // 这里就是核心的任务分配算法的核心了
  46. // 双重for循环遍历所有的taskset , 以及每一种本地化级别
  47. // 本地化级别(从上到下性能越来越差): Process_Local->进程本地化,rdd的partition和task进入一个executor内 , 那么速度当然快
  48. // NODE_LOCAL , rdd的partition和task不在一个executor中,不在一个进程中,但是在一个worker节点上
  49. // NO_PREF , 没有本地化级别
  50. // RACK_LOCAL , 机架本地化 , 至少rdd的partition和task在一个机架上
  51. // ANY , 任意的本地化级别
  52. // 对每个TaskSet从最好的一种本地化级别开始遍历
  53. var launchedTask = false
  54. for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
  55. do {
  56. // 对当前TaskSet尝试优先使用每一种本地化几倍 , 将TaskSet的task在executor上进行启动
  57. // 如果启动不了那么跳出这个do while循环 , 进入下一种本地化级别 , 也就是放大本地化级别 , 依次类推直到尝试将TaskSet在某些本地化级别下
  58. // 让task在executor上全部启动
  59. launchedTask = resourceOfferSingleTaskSet(
  60. taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
  61. } while (launchedTask)
  62. }
  63. if (tasks.size > 0) {
  64. hasLaunchedTask = true
  65. }
  66. return tasks
  67. }

该方法里面就是task算法划分的精髓 , 其实就是一个双重for循环比较task与executor的本地化级别是否匹配,匹配的具体方法就是resourceOfferSingleTaskSet方法 , 代码如下:
         
         
         
         
  1. private def resourceOfferSingleTaskSet(
  2. taskSet: TaskSetManager,
  3. maxLocality: TaskLocality,
  4. shuffledOffers: Seq[WorkerOffer],
  5. availableCpus: Array[Int],
  6. tasks: Seq[ArrayBuffer[TaskDescription]]) : Boolean = {
  7. var launchedTask = false
  8. // 遍历所有executor
  9. for (i <- 0 until shuffledOffers.size) {
  10. val execId = shuffledOffers(i).executorId
  11. val host = shuffledOffers(i).host
  12. // 如果当前executor的cpu数量至少大于每个task要使用的cpu数量 , 默认是1
  13. if (availableCpus(i) >= CPUS_PER_TASK) {
  14. try {
  15. // 调用TaskSetManager的resourceOffer方法去找到在这个executor上就用这种本地化级别的情况下
  16. // 哪些TaskSet的哪些task可以启动
  17. // 遍历使用当前本地化级别 , 可以在该executor上启动的task
  18. for (task <- taskSet.resourceOffer(execId, host, maxLocality)) {
  19. // 放入tasks这个二位数组 , 给指定的executor加上要启动的task
  20. tasks(i) += task
  21. // 到这里为止其实就是task分配算法的实现了
  22. // 尝试用本地化级别这种模型去优化task的分配和启动 , 优先希望在最佳本地化的地方启动task , 然后将task分配给executor
  23. // 将相应的分配信息加入内存缓存
  24. val tid = task.taskId
  25. taskIdToTaskSetId(tid) = taskSet.taskSet.id
  26. taskIdToExecutorId(tid) = execId
  27. executorsByHost(host) += execId
  28. availableCpus(i) -= CPUS_PER_TASK
  29. assert(availableCpus(i) >= 0)
  30. //
  31. launchedTask = true
  32. }
  33. } catch {
  34. case e: TaskNotSerializableException =>
  35. logError(s"Resource offer failed, task set ${taskSet.name} was not serializable")
  36. // Do not offer resources for this task, but don't throw an error to allow other
  37. // task sets to be submitted.
  38. return launchedTask
  39. }
  40. }
  41. }
  42. return launchedTask
  43. }



 最后调用CoarseGraineSchedulerBackend的launchTasks方法启动task : 
        
        
        
        
  1. // 根据分配好的情况去在executor上启动相应的task
  2. def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
  3. for (task <- tasks.flatten) {
  4. // 首先将每个executor要执行的task信息统一进行序列化操作
  5. val ser = SparkEnv.get.closureSerializer.newInstance()
  6. val serializedTask = ser.serialize(task)
  7. if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {
  8. val taskSetId = scheduler.taskIdToTaskSetId(task.taskId)
  9. scheduler.activeTaskSets.get(taskSetId).foreach { taskSet =>
  10. try {
  11. var msg = "Serialized task %s:%d was %d bytes, which exceeds max allowed: " +
  12. "spark.akka.frameSize (%d bytes) - reserved (%d bytes). Consider increasing " +
  13. "spark.akka.frameSize or using broadcast variables for large values."
  14. msg = msg.format(task.taskId, task.index, serializedTask.limit, akkaFrameSize,
  15. AkkaUtils.reservedSizeBytes)
  16. taskSet.abort(msg)
  17. } catch {
  18. case e: Exception => logError("Exception in error callback", e)
  19. }
  20. }
  21. }
  22. else {
  23. // 找到对应的executor
  24. val executorData = executorDataMap(task.executorId)
  25. // 给executor上的资源减去要使用的cpu资源
  26. executorData.freeCores -= scheduler.CPUS_PER_TASK
  27. // 向executor发送LaunchTask消息 , 来在executor上启动task
  28. executorData.executorActor ! LaunchTask(new SerializableBuffer(serializedTask))
  29. }
  30. }
  31. }

总结 一下 task,partition ,stage:
一个stage对应多个task , 一个task对应一个partiition的数据 , 而一个RDD对应多个partition的数据 , 也就说stage是老大 , 有了stage才有task , 一个RDD的数据对应几个partition就会有几个task
因此在一个stage中决定task的数量是栈底的那个宽依赖RDD , 这个宽依赖RDD对应了他的几个父RDD , 并且这几个父RDD是窄依赖 , 宽依赖RDD对应了几个Partition的数据就会有几个task创建
stage中的task数量最终由栈底的那个宽依赖RDD依赖了几个父RDD决定
task从哪个RDD开始计算由哪个RDD是否做了cache和checkpoint决定 , 没有的话则从第一个RDD开始


你可能感兴趣的:(spark)