Spark2.3.2源码解析: 10. 调度系统 Task任务提交 (二) TaskScheduler : 本地化计算

架构图:

Standalone模式提交运行流程图:

首先写一个WordCount代码(这个代码,为了观察多个stage操作,我写了两个reducebykey 函数)

源代码:

直接执行代码,查看spark执行程序时,将代码划分stage生成的DAG流程图

可知: WordCount 在stage划分的时候,划分为三个stage 

即在代码中如下标识:

本文继续说task的执行流程。

接上文: 

Spark2.3.2源码解析: 10. 调度系统 Task任务提交 (一) DAGScheduler 之 stage 提交

https://mp.csdn.net/postedit/85201386

接着调用执行的是:

org.apache.spark.scheduler.TaskSchedulerImpl#submitTasks

这个方法一共干了两件事:

1.创建TaskSetManager

2.资源调度&运行task

具体详情请参考注解:

直接看

backend.reviveOffers()

backend 为: 
YarnClusterSchedulerBackend  ==继承=》 YarnSchedulerBackend     ==继承=》 CoarseGrainedSchedulerBackend
所以这个方法执行的是CoarseGrainedSchedulerBackend 中的reviveOffers 方法:

最终走的是 makeOffers 这个方法

为所有的executor 提供虚拟的资源。。。。。。

从这里开始,文章再次分为两个章节,本文讲资源调度。下一篇将启动task

 


TaskSchedulerImpl#resourceOffers

这个方法有点长,具体如注解

被集群manager调用以提供slaves上的资源。我们通过按照优先顺序询问活动task集中的task来回应。
我们通过循环的方式将task调度到每个节点上以便tasks在集群中可以保持大致的均衡。

主体流程。如下:
        1、设置标志位newExecAvail为false,这个标志位是在新的slave被添加时被设置的一个标志,下面在计算任务的本地性规则时会用到;

        2、循环offers,WorkerOffer为包含executorId、host、cores的结构体,代表集群中的可用executor资源:

            2.1、更新executorIdToHost,executorIdToHost为利用HashMap存储executorId->host映射的集合;

            2.2、如果新的slave加入:

                2.2.1、executorsByHost中添加一条记录,key为host,value为new HashSet[String]();

                2.2.2、发送一个ExecutorAdded事件,并由DAGScheduler的handleExecutorAdded()方法处理;

                2.2.3、新的slave加入时,标志位newExecAvail设置为true;

            2.3、更新hostsByRack;

        3、随机shuffle offers(集群中可用executor资源)以避免总是把任务放在同一组workers上执行;

        4、构造一个task列表,以分配到每个worker,针对每个executor按照其上的cores数目构造一个cores数目大小的ArrayBuffer,实现最大程度并行化;

        5、获取可以使用的cpu资源availableCpus;

        6、调用Pool.getSortedTaskSetQueue()方法获得排序好的task集合,即sortedTaskSets;

        7、循环sortedTaskSets中每个taskSet:

               7.1、如果存在新加入的slave,则调用taskSet的executorAdded()方法,动态调整位置策略级别,这么做很容易理解,新的slave节点加入了,那么随之而来的是数据有可能存在于它上面,那么这时我们就需要重新调整任务本地性规则;

        8、循环sortedTaskSets,按照位置本地性规则调度每个TaskSet,最大化实现任务的本地性:

              8.1、对每个taskSet,调用resourceOfferSingleTaskSet()方法进行任务集调度;

        9、设置标志位hasLaunchedTask,并返回tasks。

接下来我们重点分析一下 一些重要的代码:

返回排序过的TaskSet队列,有FIFO及Fair两种排序规则,默认为FIFO,可通过配置修改

val sortedTaskSets = rootPool.getSortedTaskSetQueue

schedulableQueue为Pool中的一个调度队列,里面存储的是TaskSetManager
在TaskScheduler的submitTasks()方法中,通过层层调用,最终通过Pool的addSchedulable()方法将之前生成的TaskSetManager加入到schedulableQueue中
而TaskSetManager包含具体的tasks
taskSetSchedulingAlgorithm为调度算法,包括FIFO和FAIR两种
这里针对调度队列, 按照调度算法对其排序, 生成一个序列sortedSchedulableQueue,

FIFO: 先入先出

FAIR: 公平调度

首先,创建一个ArrayBuffer,用来存储TaskSetManager,然后,对Pool中已经存储好的TaskSetManager,即schedulableQueue队列,按照taskSetSchedulingAlgorithm调度规则或算法来排序,得到sortedSchedulableQueue,并循环其内的TaskSetManager,通过其getSortedTaskSetQueue()方法来填充sortedTaskSetQueue,最后返回。TaskSetManager的getSortedTaskSetQueue()方法也很简单,追加ArrayBuffer[TaskSetManager]即可,如下:
 

我们着重来讲解下这个调度准则或算法taskSetSchedulingAlgorithm,其定义如下:

FIFO:

//  FIFO排序类中的比较函数的实现很简单:
//  Schedulable A和Schedulable B的优先级,优先级值越小,优先级越高
//  A优先级与B优先级相同,若A对应stage id越小,优先级越高

公平调度:

//  结合以上代码,我们可以比较容易看出Fair调度模式的比较逻辑:
//
//  正在运行的task个数小于   最小共享核心数的要比不小于的优先级高
//  若两者正在运行的task个数都小于最小共享核心数,则比较minShare使用率的值,
//  即runningTasks.toDouble / math.max(minShare, 1.0).toDouble,越小则优先级越高
//  若minShare使用率相同,则比较权重使用率,即runningTasks.toDouble / s.weight.toDouble,越小则优先级越高
//  如果权重使用率还相同,则比较两者的名字
//
//  对于Fair调度模式,需要先对RootPool的各个子Pool进行排序,再对子Pool中的TaskSetManagers进行排序,
//  使用的算法都是FairSchedulingAlgorithm.FairSchedulingAlgorithm

 它的调度逻辑主要如下:
        1、优先看正在运行的tasks数目是否小于最小共享cores数,如果两者只有一个小于,则优先调度小于的那个,原因是既然正在运行的Tasks数目小于共享cores数,说明该节点资源比较充足,应该优先利用;

        2、如果不是只有一个的正在运行的tasks数目是否小于最小共享cores数的话,则再判断正在运行的tasks数目与最小共享cores数的比率;

        3、最后再比较权重使用率,即正在运行的tasks数目与该TaskSetManager的权重weight的比,weight代表调度池对资源获取的权重,越大需要越多的资源。
 

  到此为止,获得了排序好的task集合, 如果存在新加入的slave,则调用taskSet的executorAdded()方法,即TaskSetManager的executorAdded()方法,代码如下:

 

实际方法: recomputeLocality

好了,我们现在一行一行的说:

def recomputeLocality() {
  //currentLocalityIndex = 0 // Index of our current locality level in validLocalityLevels
  // 它是有效位置策略级别中的索引,指示当前的位置信息。也就是我们上一个task被launched所使用的Locality Level。

  // 首先获取之前的位置Level
  // currentLocalityIndex为有效位置策略级别中的索引,默认为0
  val previousLocalityLevel = myLocalityLevels(currentLocalityIndex)

  // 计算有效的位置Level
  myLocalityLevels = computeValidLocalityLevels()

  // 获得位置策略级别的等待时间
  localityWaits = myLocalityLevels.map(getLocalityWait)

  // 设置当前使用的位置策略级别的索引
  currentLocalityIndex = getLocalityIndex(previousLocalityLevel)
}

先看这个:

val previousLocalityLevel = myLocalityLevels(currentLocalityIndex)

确定在我们的任务集TaskSet中应该使用哪种位置Level,以便我们做延迟调度

computeValidLocalityLevels

这里,我们先看下其中几个比较重要的数据结构。在TaskSetManager中,存在如下几个数据结构:

// 每个executor上即将被执行的tasks的映射集合
private val pendingTasksForExecutor = new HashMap[String, ArrayBuffer[Int]]

// Set of pending tasks for each host. Similar to pendingTasksForExecutor,
// but at host level.

// 每个host上即将被执行的tasks的映射集合
private val pendingTasksForHost = new HashMap[String, ArrayBuffer[Int]]

// Set of pending tasks for each rack -- similar to the above.
// 每个rack上即将被执行的tasks的映射集合
private val pendingTasksForRack = new HashMap[String, ArrayBuffer[Int]]

// Set containing pending tasks with no locality preferences.
// 存储所有没有位置信息的即将运行tasks的index索引的集合
private[scheduler] var pendingTasksWithNoPrefs = new ArrayBuffer[Int]

// Set containing all pending tasks (also used as a stack, as above).
// 存储所有即将运行tasks的index索引的集合
private val allPendingTasks = new ArrayBuffer[Int]

这些数据结构,存储了task与不同位置的载体的对应关系。在TaskSetManager对象被构造时,有如下代码被执行:

添加一个任务的索引到所有相关的pending-task索引列表
它是根据task的preferredLocations,来决定该往哪个数据结构存储的。
最终,将task的位置信息,存储到不同的数据结构中,方便后续任务调度的处理。

我们再回来:

看这句

获得位置策略级别的等待时间
localityWaits = myLocalityLevels.map(getLocalityWait)

可以通过 SparkConf 进行调整:

new SparkConf() 
.set(“spark.locality.wait”, “10”)

默认值: spark.locality.wait,默认为3s

PROCESS_LOCAL : spark.locality.wait.process   默认为3s

NODE_LOCAL:spark.locality.wait.node

RACK_LOCAL: spark.locality.wait.rack

最后:

再回到方法:

org.apache.spark.scheduler.TaskSchedulerImpl#resourceOffers

org.apache.spark.scheduler.TaskSchedulerImpl#resourceOfferSingleTaskSet

该方法的主体流程如下:
        1、标志位launchedTask初始化为false,用它来标记是否有task被成功分配或者launched;

        2、循环shuffledOffers,即每个可用executor:

             2.1、获取其executorId和host;

             2.2、如果executor上可利用cpu数目大于每个task需要的数目,则继续task分配;

             2.3、调用TaskSetManager的resourceOffer()方法,处理返回的每个TaskDescription:

                2.3.1、分配task成功,将task加入到tasks对应位置(注意,tasks为一个空的,根据shuffledOffers和其可用cores生成的有一定结构的列表);

                2.3.2、更新taskIdToTaskSetManager、taskIdToExecutorId、executorIdToTaskCount、executorsByHost、availableCpus等数据结构;

                2.3.3、确保availableCpus(i)不小于0;

                2.3.4、标志位launchedTask设置为true;

       3、返回launchedTask。

org.apache.spark.scheduler.TaskSetManager#resourceOffer

org.apache.spark.scheduler.TaskSetManager#getAllowedLocalityLevel
/**
  * 根据当前的等待时间,根据延迟调度获取我们可以启动任务的级别。
  *
 */
private def getAllowedLocalityLevel(curTime: Long): TaskLocality.TaskLocality = {
  // Remove the scheduled or finished tasks lazily

  // 判断task是否可以被调度
  def tasksNeedToBeScheduledFrom(pendingTaskIds: ArrayBuffer[Int]): Boolean = {
    var indexOffset = pendingTaskIds.size

    // 循环
    while (indexOffset > 0) {
      // 索引递减
      indexOffset -= 1
      // 获得task索引
      val index = pendingTaskIds(indexOffset)
      // 如果对应task不存在任何运行实例,且未执行成功,可以调度,返回true
      if (copiesRunning(index) == 0 && !successful(index)) {
        return true
      } else {
        // 从pendingTaskIds中移除
        pendingTaskIds.remove(indexOffset)
      }
    }
    false
  }

  // Walk through the list of tasks that can be scheduled at each location and returns true
  // if there are any tasks that still need to be scheduled. Lazily cleans up tasks that have
  // already been scheduled.
  def moreTasksToRunIn(pendingTasks: HashMap[String, ArrayBuffer[Int]]): Boolean = {
    val emptyKeys = new ArrayBuffer[String]

    // 循环pendingTasks
    val hasTasks = pendingTasks.exists {
      case (id: String, tasks: ArrayBuffer[Int]) =>

        // 判断task是否可以被调度
        if (tasksNeedToBeScheduledFrom(tasks)) {
          true
        } else {
          emptyKeys += id
          false
        }
    }
    // The key could be executorId, host or rackId
    // 移除数据
    emptyKeys.foreach(id => pendingTasks.remove(id))
    hasTasks
  }


  // 从当前索引currentLocalityIndex开始,循环myLocalityLevels
  while (currentLocalityIndex < myLocalityLevels.length - 1) {


    // 是否存在待调度task,根据不同的Locality Level,调用moreTasksToRunIn()方法从不同的数据结构中获取,
    // NO_PREF直接看pendingTasksWithNoPrefs是否为空
    val moreTasks = myLocalityLevels(currentLocalityIndex) match {

      case TaskLocality.PROCESS_LOCAL => moreTasksToRunIn(pendingTasksForExecutor)

      case TaskLocality.NODE_LOCAL => moreTasksToRunIn(pendingTasksForHost)

      case TaskLocality.NO_PREF => pendingTasksWithNoPrefs.nonEmpty

      case TaskLocality.RACK_LOCAL => moreTasksToRunIn(pendingTasksForRack)

    }

    // 不存在可以被调度的task
    if (!moreTasks) {
      // This is a performance optimization: if there are no more tasks that can
      // be scheduled at a particular locality level, there is no point in waiting
      // for the locality wait timeout (SPARK-4939).

      // 记录lastLaunchTime
      lastLaunchTime = curTime
      logInfo(s"No tasks for locality level ${myLocalityLevels(currentLocalityIndex)}, " +
        s"so moving to locality level ${myLocalityLevels(currentLocalityIndex + 1)}")

      // 位置策略索引加1
      currentLocalityIndex += 1

    } else if (curTime - lastLaunchTime >= localityWaits(currentLocalityIndex)) {
      // Jump to the next locality level, and reset lastLaunchTime so that the next locality
      // wait timer doesn't immediately expire

      // 更新localityWaits
      lastLaunchTime += localityWaits(currentLocalityIndex)
      logInfo(s"Moving to ${myLocalityLevels(currentLocalityIndex + 1)} after waiting for " +
        s"${localityWaits(currentLocalityIndex)}ms")

      // 位置策略索引加1
      currentLocalityIndex += 1


    } else {

      // 返回当前位置策略级别
      return myLocalityLevels(currentLocalityIndex)
    }

  }
  // 返回当前位置策略级别
  myLocalityLevels(currentLocalityIndex)
}

在确定allowedLocality后,我们就需要调用dequeueTask()方法,出列task,进行调度。代码如下:

/**
 * Dequeue a pending task for a given node and return its index and locality level.
 * Only search for tasks matching the given locality constraint.
 *
 * @return An option containing (task index within the task set, locality, is speculative?)
 */
private def dequeueTask(execId: String, host: String, maxLocality: TaskLocality.Value)
  : Option[(Int, TaskLocality.Value, Boolean)] =
{

  //< dequeueTaskFromList: 该方法获取list中一个可以launch的task,
  // 同时清除扫描过的已经执行的task。其实它从第二次开始首先扫描的一定是已经运行完成的task,因此是延迟清除
  // 同一个Executor,通过execId来查找相应的等待的task

  // 首先调用dequeueTaskFromList()方法,对PROCESS_LOCAL级别的task进行调度
  for (index <- dequeueTaskFromList(execId, host, getPendingTasksForExecutor(execId))) {
    return Some((index, TaskLocality.PROCESS_LOCAL, false))
  }

  // 通过主机名找到相应的Task,不过比之前的多了一步判断
  // PROCESS_LOCAL未调度到task的话,再调度NODE_LOCAL级别
  if (TaskLocality.isAllowed(maxLocality, TaskLocality.NODE_LOCAL)) {
    for (index <- dequeueTaskFromList(execId, host, getPendingTasksForHost(host))) {
      return Some((index, TaskLocality.NODE_LOCAL, false))
    }
  }

  // NODE_LOCAL未调度到task的话,再调度NO_PREF级别
  if (TaskLocality.isAllowed(maxLocality, TaskLocality.NO_PREF)) {
    // Look for noPref tasks after NODE_LOCAL for minimize cross-rack traffic
    for (index <- dequeueTaskFromList(execId, host, pendingTasksWithNoPrefs)) {
      return Some((index, TaskLocality.PROCESS_LOCAL, false))
    }
  }

  // NO_PREF未调度到task的话,再调度RACK_LOCAL级别
  if (TaskLocality.isAllowed(maxLocality, TaskLocality.RACK_LOCAL)) {
    for {
      rack <- sched.getRackForHost(host)
      index <- dequeueTaskFromList(execId, host, getPendingTasksForRack(rack))
    } {
      return Some((index, TaskLocality.RACK_LOCAL, false))
    }
  }

  // 最好是ANY级别的调度
  if (TaskLocality.isAllowed(maxLocality, TaskLocality.ANY)) {
    for (index <- dequeueTaskFromList(execId, host, allPendingTasks)) {
      return Some((index, TaskLocality.ANY, false))
    }
  }


  // find a speculative task if all others tasks have been scheduled
  // 最后没办法了,拖的时间太长了,只能启动推测执行了
  // 如果所有的class都被调度的话,寻找一个speculative task,同MapReduce的推测执行原理的思想
  dequeueSpeculativeTask(execId, host, maxLocality).map {
    case (taskIndex, allowedLocality) => (taskIndex, allowedLocality, true)}
}

按照PROCESS_LOCAL、NODE_LOCAL、NO_PREF、RACK_LOCAL、ANY的顺序进行调度。

得到TaskDescription

参考链接:

https://blog.csdn.net/lipeng_bigdata/article/details/50699939

猜你喜欢

转载自blog.csdn.net/zhanglong_4444/article/details/85249376