SparkCore — Task分配算法

Task分配算法

  接着上一篇的Task最佳位置,我们分析了submitMissingTasks()方法,其中里面比较重要的:一个是task的最佳位置计算,另一个就是提交TaskSet给TaskScheduler。下面分析提交到TaskScheduler后的TaskSet中的task是如何被分配到Executor上去的。
  默认情况下,standalone模式,是使用的TaskSchedulerImpl,TaskScheduler只是一个trait,到TaskSchedulerImpl中找到submitTasks()方法,源码如下:

override def submitTasks(taskSet: TaskSet) {
    val tasks = taskSet.tasks
    logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
    this.synchronized {
      // 为TaskSet创建TaskSetManager,它会负责它的那个TaskSet的任务执行状况的监视和管理
      // TaskManager会负责追踪它所管理的那个TaskSet,如果task失败,它也会重试task等等
      val manager = createTaskSetManager(taskSet, maxTaskFailures)
      // 对TaskSet的信息进行提取和封装
      val stage = taskSet.stageId
      val stageTaskSets =
        taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])
      stageTaskSets(taskSet.stageAttemptId) = manager
      val conflictingTaskSet = stageTaskSets.exists { case (_, ts) =>
        ts.taskSet != taskSet && !ts.isZombie
      }
     
      // 将TaskSetManager放入调度池中,这是之前初始化的时候创建的调度池,默认是FIFO
      // 这里将TaskSet放入调度池,会对Task进行排序。
      schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)
	  // 省略部分代码
	  ....
     } 
    // 调用SparkDeploySchedulerBackend的reviveOffers,而SparkDeploySchedulerBackend
    // 又继承自CoarseGrainedSchedulerBackend。
    backend.reviveOffers()
  }

  CoarseGrainedSchedulerBackend的reviveOffers又会被DriverEndPoint发送ReviveOffers消息,而这个消息里面调用了makeOffers()方法,下面分析这个方法:

private def makeOffers() {
      // 过滤掉被kill的executor
      val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
      // 将Application所有可用的executor,将其封装成WorkerOffer,每个WorkerOffer
      // 代表了每个executor可用cpu资源数量
      val workOffers = activeExecutors.map { case (id, executorData) =>
        new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
      }.toSeq
      // 调用resourceOffers()方法,执行任务分配算法,将各个task分配到executor上去
      // 将分配好task到executor之后,执行launchTasks()方法,将分配的task发送LaunchTask消息
      // 到对应的executor上去,由executor启动并启动task
      launchTasks(scheduler.resourceOffers(workOffers))
    }

  首先将注册的executor的可用资源封装为workerOffers,接着执行TaskShcedulerImpl的resourceOffers方法,执行任务分配算法,将各个task分配到executor上去,最后执行自己的launchTasks()方法,将分配的task发送LaunckTask消息到对应的executor上去,由executor启动并执行。
  首先看TaskShcedulerImpl的resourceOffers()方法。

def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
    // 缓存每个executor的信息,查看是否有新的executor产生,也一并写入缓存中
    var newExecAvail = false
    for (o <- offers) {
      executorIdToHost(o.executorId) = o.host
      executorIdToTaskCount.getOrElseUpdate(o.executorId, 0)
      if (!executorsByHost.contains(o.host)) {
        executorsByHost(o.host) = new HashSet[String]()
        executorAdded(o.executorId, o.host)
        newExecAvail = true
      }
      for (rack <- getRackForHost(o.host)) {
        hostsByRack.getOrElseUpdate(rack, new HashSet[String]()) += o.host
      }
    }

    // 首先将可用的executor进行shuffle,进行打散,尽量进行负载均衡
    val shuffledOffers = Random.shuffle(offers)
    // Build a list of tasks to assign to each worker.
    // 针对Worker创建出所需的组件
    // 创建一个tasks列表,它是一个二维数组,其中一维是TaskDescription,
    // 它对应的子ArrayBuffer是这个task对应的executor可用的cpu数量
    val tasks = shuffledOffers.map(o => new ArrayBuffer[TaskDescription](o.cores))
    // 每个worker可用cpu数量
    val availableCpus = shuffledOffers.map(o => o.cores).toArray
    // 从rootPool中,取出了排序的TaskSet,
    // 刚开始创建的TaskSetManager被放入调度池中,会对提交上来的task进行排序
    val sortedTaskSets = rootPool.getSortedTaskSetQueue
    for (taskSet <- sortedTaskSets) {
      logDebug("parentName: %s, name: %s, runningTasks: %s".format(
        taskSet.parent.name, taskSet.name, taskSet.runningTasks))
      if (newExecAvail) {
        // 计算task的本地化级别 -- 这里是计算的新加入executor的taskset的本地化级别
        taskSet.executorAdded()
      }
    }
    // 这里就是任务分配算法的核心了
    // 双重for循环,遍历所有的TaskSet,以及每一种本地化级别
    var launchedTask = false
    // 对每个taskset,从最好的一种本地化级别开始遍历
    for (taskSet <- sortedTaskSets; maxLocality <- taskSet.myLocalityLevels) {
      do {
        // 对当前taskset,尝试优先使用最小的本地化级别,将taskset的task,在executor上进行启动
        // 如果启动不了,那么就跳出这个循环,进入下一个本地化级别,也就是放大本地化级别
        // 依次类推,直到尝试将TaskSet在某些本地化级别下,让task在executor上全部启动
        launchedTask = resourceOfferSingleTaskSet(
            taskSet, maxLocality, shuffledOffers, availableCpus, tasks)
      } while (launchedTask)
    }

    if (tasks.size > 0) {
      hasLaunchedTask = true
    }
    return tasks
  }

  上面的核心就是那个双重for循环,它就是任务分配算法的核心。首先介绍一下本地化级别有哪些,一共有5种:
PROCESS_LOCAL:进程本地化,RDD的partition和task进入到同一个executor中,速度快。
NODE_LOCAL:节点本地化,RDD的partition和task,不在一个executor中,但在同一个worker节点上。
NO_PREF:无本地化,计算数据在关系型数据库中,所以无论哪个节点都可以。
RACK_LOCAL:机架本地化,RDD的partition和task在同一个机架上,不同worker上。
ANY:任意本地化级别,就是在集群的任何一个节点上都可以,这是最高级别的,在其他级别都不可行的时候,会使用这个。
这些本地化级别,从好到坏,从小到大,越往前的本地化级别就越好。上面的双重for循环,就是对每个TaskSet,从最好的一种本地化级别开始遍历;对当前的TaskSet优先使用最好的本地化级别,将TaskSet中的task,在executor上进行启动;如果启动不了,跳出这个循环,进入下一个本地化级别,也就是放大本地化级别,以此类推,直到尝试将TaskSet在某些本地化级别下,让task在executor上启动
  实现上述功能的方法就是resourceOfferSingleTaskSet(),它会去找在这个executor上,使用某个本地化级别,taskset的哪些task可以启动。里面调用了TaskSetManager的resourceOffer()方法。这个方法去判断这个本地化级别的task能不能再这个executor上启动,它通过判断这个executor在这个本地化级别的等待时间是多少,如果在一定范围内,那么就认为这个本地化级别的task可以在这个executor上启动。

你可能感兴趣的:(Spark,Core原理与源码分析)