Spark调度系统——任务集合管理器TaskSetManager

TaskSetManager实现了Schedulable特质,并参与到调度池的调度中。TaskSetManager对TaskSet进行管理,包括任务推断、Task本地性,并对Task进行资源分配。TaskSchedulerImpl依赖于TaskSetManager,本文将对TaskSetManager的实现进行分析。

1 Task集合

DAGScheduler将Task提交给TaskScheduler时,需要将多个Task打包为TaskSet。TaskSet是整个调度池中对Task进行调度管理的基本单位,由调度池中的TaskSetManager来管理,其定义如下:

//org.apache.spark.scheduler.TaskSet
private[spark] class TaskSet(
    val tasks: Array[Task[_]],
    val stageId: Int,
    val stageAttemptId: Int,
    val priority: Int,
    val properties: Properties) {
  val id: String = stageId + "." + stageAttemptId

  override def toString: String = "TaskSet " + id
}
  • tasks:TaskSet所包含的Task的数组
  • stageId:Task所属Stage的身份标识
  • stageAttemptId:Stage尝试的身份标识
  • priority:优先级。通常以JobId作为优先级
  • id:TaskSet的身份标识

2 TaskSetManager的成员属性

下面对部分成员属性进行了解

  • numTasks:TaskSet包含的Task数组,即tasks数组的长度
  • copiesRunning:对每个Task的复制运行数进行记录的数组。copiesRunning按照索引与tasks数组的同一索引位置的Task相对应,记录对应Task的复制运行数量
  • successful:对每个Task是否执行成功进行记录的数组。successful按照索引与tasks数组的同一索引位置的Task相对应,记录对应的Task是否执行成功
  • numFailures:对每个Task的执行失败次数进行记录的数组。numFailures按照索引与tasks数组的同一索引位置的Task相对应,记录对应Tasks的执行失败次数
  • taskAttempts:对每个Task的所有执行尝试信息进行记录的数组。taskAttempts按照索引与tasks数组的同一索引位置的Task相对应,记录对应Task的所有Task尝试信息
  • runningTasksSet:正在运行Task的集合
  • isZombie:当TaskSetManager所管理的TaskSet中的所有Task都执行成功了,不再有更多的Task尝试被启动时,就处理“僵尸”状态。例如,每个Task至少有一次尝试成功,或者TaskSet被舍弃了,TaskSetManager将会进入“僵尸”状态,直到所有的Task都运行成功为止,TaskSetManager将一保持在“僵尸”状态。TaskSetManager的“僵尸”状态并不是无用的,在这种状态下的TaskSetManager将继续跟踪、记录正在运行的Task。
  • pendingTasksForExecutor:每个Executor上待处理的Task的集合,即Executor的身份标识与待处理Task的身份标识的集合之间的映射关系
  • pendingTasksForHost:每个Host上待处理的Tasks的集合,即Host与待处理Task的身份标识的集合之间的映射关系
  • pendingTasksForRack:每个机架上待处理的Tasks的集合,即机架与待处理Tasks的身份标识的集合之间的映射关系
  • myLocalityLevels:Tasks的本地性级别的数组。是通过通用computeValidLocalityLevels方法获取的
  • localityWaits:与myLocalityLevels中的每个每个本地性级别相对应,表示对应本地性级别的等待时间。

3 调度池与推断执行

在Hadoop 2.x.x版本中,当一个应用向YARN集群提交作业后,此作业的多个任务由于负载不均衡、资源分布不均等原因都会导致各个任务运行完成的时间不一致,甚至会出现一个Task尝试明显慢于同一作业的其他Task尝试的情况。如果对这种情况不加优化,最慢的Task尝试明显会拖慢整个作业的整体执行进度。mapreduce框架提供了任务推断执行机制,当有必要时就启动一个备份任务,最张采用备份任务的原任务中率先执行完成的结果作为最终结果。

与Hadoop类似,Spark应用向Spark集群提交作业后,也会因为相似的原因导致出现慢任务拖慢整个作业执行进度的问题。为了解决这些问题,Pool和TaskSetManager提供了Spark任务推断执行的实现。Pool和TaskSetManager中对推断执行的操作分为两类:一类是可推断任务的检测与缓存;另一类是从缓存中找到可推断任务进行推断执行。Pool的checkSpeculatableTasks方法和TaskSetManager的checkSpeculatableTasks方法实现了按照深度遍历算法对可推断任务的检测与缓存。TaskSetManager的dequeueSpeculativeTask方法则实现了从缓存中找到可推断任务进行推断执行。

3.1 checkSpeculatableTasks

用于检查当前TaskSetManager中是否有需要推断的任务

//org.apache.spark.scheduler.TaskSetManager
override def checkSpeculatableTasks(): Boolean = {
  if (isZombie || numTasks == 1) {
    return false //没有可推断的Task
  }
  var foundTasks = false
  val minFinishedForSpeculation = (SPECULATION_QUANTILE * numTasks).floor.toInt
  logDebug("Checking for speculative tasks: minFinished = " + minFinishedForSpeculation)
  if (tasksSuccessful >= minFinishedForSpeculation && tasksSuccessful > 0) {
    val time = clock.getTimeMillis()
    val durations = taskInfos.values.filter(_.successful).map(_.duration).toArray
    Arrays.sort(durations)
    val medianDuration = durations(min((0.5 * tasksSuccessful).round.toInt, durations.length - 1))
    val threshold = max(SPECULATION_MULTIPLIER * medianDuration, 100)
    logDebug("Task length threshold for speculation: " + threshold)
    for ((tid, info) <- taskInfos) { //遍历taskInfos,寻找符合推断条件的Task
      val index = info.index
      if (!successful(index) && copiesRunning(index) == 1 && info.timeRunning(time) > threshold &&
        !speculatableTasks.contains(index)) {
        logInfo(
          "Marking task %d in stage %s (on %s) as speculatable because it ran more than %.0f ms"
            .format(index, taskSet.id, info.host, threshold))
        speculatableTasks += index
        foundTasks = true
      }
    }
  }
  foundTasks
}
  • 1)如果TaskSetManager处理“僵尸”状态且TaskSet只包含一个Task,那么返回false(即没有可以推断的Task)
  • 2)计算进行推断的最小完成任务数量(minFinishedForSpeculation)
  • 3)如果执行成功的Task数量(tasksSuccessful)大于等于minFinishedForSpeculation并且tasksSuccessful大于0,就进入下一步,否则返回false
  • 4)找出taskInfos中执行成功的任务尝试信息(TaskInfo)中执行时间处于中间的时间medianDuration
  • 5)计算进行推断的最小时间(threshold)
  • 6)遍历taskInfos,将符合推断条件的Task在TaskSet中的索引放入speculatableTasks中。推断条件包括还未执行成功、复制运行数为1、运行时间大于threshold。如果Task的索引被放入了speculatableTasks,那么返回true。

3.2 dequeueSpeculativeTask

用于根据指定的Host、Executor和本地性级别,从可推断的Task中找出可推断的Task在TaskSet中的索引和相应的本地性级别

protected def dequeueSpeculativeTask(execId: String, host: String, locality: TaskLocality.Value)
  : Option[(Int, TaskLocality.Value)] =
{
  speculatableTasks.retain(index => !successful(index)) //移除已经完成的Task
  def canRunOnHost(index: Int): Boolean =
    !hasAttemptOnHost(index, host) && !executorIsBlacklisted(execId, index)
  if (!speculatableTasks.isEmpty) {
    for (index <- speculatableTasks if canRunOnHost(index)) {
      val prefs = tasks(index).preferredLocations
      val executors = prefs.flatMap(_ match {
        case e: ExecutorCacheTaskLocation => Some(e.executorId)
        case _ => None
      });
      if (executors.contains(execId)) { //找到了在指定的Executor上推断执行的Task
        speculatableTasks -= index
        return Some((index, TaskLocality.PROCESS_LOCAL))
      }
    }
    if (TaskLocality.isAllowed(locality, TaskLocality.NODE_LOCAL)) {
      for (index <- speculatableTasks if canRunOnHost(index)) {
        val locations = tasks(index).preferredLocations.map(_.host)
        if (locations.contains(host)) { //找到了在本地节点上推断执行的Task
          speculatableTasks -= index
          return Some((index, TaskLocality.NODE_LOCAL))
        }
      }
    }
    if (TaskLocality.isAllowed(locality, TaskLocality.NO_PREF)) {
      for (index <- speculatableTasks if canRunOnHost(index)) {
        val locations = tasks(index).preferredLocations
        if (locations.size == 0) { //对于没有本地性偏好的Task,让它在指定的Executor上推断执行
          speculatableTasks -= index
          return Some((index, TaskLocality.PROCESS_LOCAL))
        }
      }
    }
    if (TaskLocality.isAllowed(locality, TaskLocality.RACK_LOCAL)) {
      for (rack <- sched.getRackForHost(host)) {
        for (index <- speculatableTasks if canRunOnHost(index)) {
          val racks = tasks(index).preferredLocations.map(_.host).flatMap(sched.getRackForHost)
          if (racks.contains(rack)) { //找到了本地机架上推断执行的Task
            speculatableTasks -= index
            return Some((index, TaskLocality.RACK_LOCAL))
          }
        }
      }
    }
    if (TaskLocality.isAllowed(locality, TaskLocality.ANY)) {
      for (index <- speculatableTasks if canRunOnHost(index)) {
        speculatableTasks -= index //找到可以在任何节点、机架上推断执行的Task
        return Some((index, TaskLocality.ANY))
      }
    }
  }
  None
}
  • 1)从speculatableTasks中移除已经完成的Task,保留还未完成的Task
  • 2)对于speculatableTasks中的所有未指定的Host上尝试运行,且指定的Host和Executor不在黑名单的所有Task进行以下处理:
    • ①获取Task偏好的Executor
    • ②如果Task偏好的Executor中包含指定的Executor,那么将此Task的索引从speculatableTasks中移除,并返回此Task的索引与PROCESS_LOCAL的对偶
  • 3)后面步骤跟1、2步类似

4 Task本地性

与Hadoop类似,Spark对任务的处理也要考虑数据的本地性(Locality),好的数据本地性能够大幅减少节点间的数据传输, 提升程序执行效率。Spark目前支持五种本地性级别,由高到低分别为:PROCESS_LOCAL(本地进程),NODE_LOCAL(本地节点),NO_PREF(没有偏好),RACK_LOCAL(本地机架),ANY(任何)

Task本地性的分配优先考虑有较高的本地性的级别,否则分配较低的本地性级别,直到ANY。TaskSet可以有一到多个本地性级别,但在给Task分配本地性时只能是其中的一个。TaskSet中的所有Task都具有相同的允许使用的本地性级别,但在运行期可能因为资源不足、运行时间等因素,导致同一TaskSet的本地性级别进行计算、获取某个本地性级别的等待时间、给Task分配资源时获取允许的本地性级别等。

TaskSet中实现的本地性操作包括对TaskSet的本地性级别进行计算、获取某个本地性级别的等待时间、给Task分配资源时获取允许的本地性级别等。

4.1 computeValidLocalityLevels

用于计算有效的本地性级别,这样就可以将Task按照本地性级别,由高到低分配给允许的Executor

private def computeValidLocalityLevels(): Array[TaskLocality.TaskLocality] = {
  import TaskLocality.{PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY}
  val levels = new ArrayBuffer[TaskLocality.TaskLocality]
  if (!pendingTasksForExecutor.isEmpty && getLocalityWait(PROCESS_LOCAL) != 0 &&
      pendingTasksForExecutor.keySet.exists(sched.isExecutorAlive(_))) {
    levels += PROCESS_LOCAL //允许的本地性级别里包括PROCESS_LOCAL
  }
  if (!pendingTasksForHost.isEmpty && getLocalityWait(NODE_LOCAL) != 0 &&
      pendingTasksForHost.keySet.exists(sched.hasExecutorsAliveOnHost(_))) {
    levels += NODE_LOCAL //允许的本地性级别里包括NODE_LOCAL
  }
  if (!pendingTasksWithNoPrefs.isEmpty) {
    levels += NO_PREF //允许的本地性级别里包括NO_PREF
  }
  if (!pendingTasksForRack.isEmpty && getLocalityWait(RACK_LOCAL) != 0 &&
      pendingTasksForRack.keySet.exists(sched.hasHostAliveOnRack(_))) {
    levels += RACK_LOCAL //允许的本地级别里包括RACK_LOCAL
  }
  levels += ANY //允许的本地性级别里增加ANY
  logDebug("Valid locality levels for " + taskSet + ": " + levels.mkString(", "))
  levels.toArray //返回所有允许的本地性级别
}

4.2 getLocalityWait

用于获取某个本地性级别的等待时间

private def getLocalityWait(level: TaskLocality.TaskLocality): Long = {
  val defaultWait = conf.get("spark.locality.wait", "3s")
  val localityWaitKey = level match {
    case TaskLocality.PROCESS_LOCAL => "spark.locality.wait.process"
    case TaskLocality.NODE_LOCAL => "spark.locality.wait.node"
    case TaskLocality.RACK_LOCAL => "spark.locality.wait.rack"
    case _ => null
  }
  if (localityWaitKey != null) {
    conf.getTimeAsMs(localityWaitKey, defaultWait)
  } else {
    0L
  }
}
  • 1)获取默认的等待时间。默认的等待时间可以通过spark.locality.wait属性配置,默认为3秒
  • 2)根据本地性级别匹配到对应的配置属性。

4.3 getLocalityIndex

用于从myLocalityLevels中找出指定的本地性级别所对应的索引

def getLocalityIndex(locality: TaskLocality.TaskLocality): Int = {
  var index = 0
  while (locality > myLocalityLevels(index)) {
    index += 1
  }
  index
}

4.4 getAllowedLocalityLevel

用于获取允许的本地性级别

private def getAllowedLocalityLevel(curTime: Long): TaskLocality.TaskLocality = {
while (currentLocalityIndex < myLocalityLevels.length - 1) {
  val moreTasks = myLocalityLevels(currentLocalityIndex) match { //查找本地性级别有Task要运行
    case TaskLocality.PROCESS_LOCAL => moreTasksToRunIn(pendingTasksForExecutor)
    case TaskLocality.NODE_LOCAL => moreTasksToRunIn(pendingTasksForHost)
    case TaskLocality.NO_PREF => pendingTasksWithNoPrefs.nonEmpty
    case TaskLocality.RACK_LOCAL => moreTasksToRunIn(pendingTasksForRack)
  }
  if (!moreTasks) {
    lastLaunchTime = curTime //没有Task需要处理,则将最后的运行时间设置为curTime
    logDebug(s"No tasks for locality level ${myLocalityLevels(currentLocalityIndex)}, " +
      s"so moving to locality level ${myLocalityLevels(currentLocalityIndex + 1)}")
    currentLocalityIndex += 1
  } else if (curTime - lastLaunchTime >= localityWaits(currentLocalityIndex)) {
    lastLaunchTime += localityWaits(currentLocalityIndex) //跳入更低的本地性级别
    logDebug(s"Moving to ${myLocalityLevels(currentLocalityIndex + 1)} after waiting for " +
      s"${localityWaits(currentLocalityIndex)}ms")
    currentLocalityIndex += 1
  } else {
    return myLocalityLevels(currentLocalityIndex) //返回当前本地性级别
  }
}
myLocalityLevels(currentLocalityIndex) //未能找到允许的本地性级别,那么返回最低的本地性级别
}

执行步骤如下:

  • 1)按照索引由高到低从myLocalityLevels读取本地性级别,然后执行以下操作
    • ①调用moreTasksToRunIn方法判断本地性级别对应的待处理Task的缓存结构中是否有Task需要处理
    • ②如果没有Task需要处理,则将最后的运行时间设置为curTime
    • ③如果有Task需要处理且curTime与最后运行时间的差值大于当前本地性级别的等待时间,则将最后的运行时间增加当前本地性级别的等待时间(这样实际将直接跳入更低的本地性级别)
    • ④如果有Task需要处理且curTime与最后运行时间的差值小于等于当前本地性级别的等待时间,则返回当前本地性级别
  • 2)如果上一步未能找到允许的本地性级别,那么返回最低的本地性级别

Spark任务在获取本地性级别时都要等待一段本地性级别的等待时长,任何任务都希望被分配 到可以从本地读取数据的节点上,以得到最大的性能提升。然而每个任务的运行时长都不是事先可以预料的,当一个任务在分配时,如果没有满足最佳本地性(PROCESS_LOCAL)的资源,而一直固执地期盼得到最佳的资源,很有可能被已经占用最佳资源但运行时间很长的任务耽搁,所以这些代码实现了当没有最佳本地性时,退而求其次,选择稍微差点的资源。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值