Spark Task执行流程源码分析系列之三: TaskSetManager分析

概述

TaskSetManager是一个stage的任务集合的抽象,主要进行当前Stage任务集管理,跟踪每一个任务,进行失败重试,推测执行以及基于数据本地性的调度等,对外只提供两个接口,resourceOffer来判断是否在指定的executor上面执行任务,statusUpdate在任务成功或者失败时候,告知任务状态改变。本文将分析源码,解密它是如何进行调度任务执行,推测执行以及失败重试的。

基本属性

我们先来看下TaskSetManager的主要成员变量,主要是任务的管理相关的变量:

  1. taskSet:是当前stage对应的任务集合;
  2. numTasks:TaskSet包含的Task数组,即tasks数组的长度;
  3. copiesRunning:对每个Task的复制运行数进行记录的数组。copiesRunning按照索引与tasks数组的同一索引位置的Task相对应,记录对应Task的复制运行数量;
  4. successful:对每个Task是否执行成功进行记录的数组,successful按照索引与tasks数组的同一索引位置的Task相对应,记录对应的Task是否执行成功;
  5. numFailures:对每个Task的执行失败次数进行记录的数组,numFailures按照索引与tasks数组的同一索引位置的Task相对应,记录对应Tasks的执行失败次数;
  6. taskAttempts:对每个Task的所有执行尝试信息进行记录的数组,taskAttempts按照索引与tasks数组的同一索引位置的Task相对应,记录对应Task的所有Task尝试信息;
  7. runningTasksSet:正在运行Task的集合,记录的是taskId;
  8. isZombie:当TaskSetManager所管理的TaskSet中的所有Task都执行成功了,不再有更多的Task尝试被启动时,就处理“僵尸”状态。例如,每个Task至少有一次尝试成功,或者TaskSet被舍弃了,TaskSetManager将会进入“僵尸”状态,直到所有的Task都运行成功为止,TaskSetManager将一保持在“僵尸”状态。TaskSetManager的“僵尸”状态并不是无用的,在这种状态下可以继续跟踪、记录正在运行的Task
  9. taskInfos:记录每次尝试对应的Task的信息;
  10. speculatableTasks:推测执行的任务集合。
// TaskSet包含的Task数组,即TaskSet的tasks属性。
val tasks = taskSet.tasks
// TaskSet包含的Task的数量,即tasks数组的长度。
val numTasks = tasks.length

// 对每个Task的复制运行数进行记录的数组
val copiesRunning = new Array[Int](numTasks)
// 对每个Task是否执行成功进行记录的数组。
val successful = new Array[Boolean](numTasks)
// 对每个Task的执行失败次数进行记录的数组
private val numFailures = new Array[Int](numTasks)
// 对每个Task的所有执行尝试信息进行记录的数组。
val taskAttempts = Array.fill[List[TaskInfo]](numTasks)(Nil)

// 正在运行的Task的集合
val runningTasksSet = new HashSet[Long]
var isZombie = false

// Task的身份标识与TaskAttempt的信息(如启动时间、完成时间等)之间的映射关系。
val taskInfos = new HashMap[Long, TaskInfo]

// 推测执行
val speculatableTasks = new HashSet[Int]

TaskSetManager继承于Schedulable,所以继承了以下变量:

  1. weightminShare分别代表公平调度算法的权重和最小资源值,用于FAIR调度优先级比较;
  2. priority用于FIFO调度优先级的比较,是JobId;
  3. stageId是当前的stage对应的id,是FIFO调度在JobId相同的情况下的优先级比较;
// 用于公平调度算法的权重
var weight = 1
// 用于公平调度算法的参考值
var minShare = 0
// 进行调度的优先级,jobId
var priority = taskSet.priority
// 调度池所属的Stage的身份标识,stageId
var stageId = taskSet.stageId

TaskSetManager是基于数据本地性来调度执行任务的,以下变量来记录各种级别存储级别所对应task的index:

  1. pendingTasksForExecutor:每个Executor上待处理的Task的集合,即Executor的身份标识与待处理Task的身份标识的集合之间的映射关系;
  2. pendingTasksForHost:每个Host上待处理的Tasks的集合,即Host与待处理Task的身份标识的集合之间的映射关系;
  3. pendingTasksForRack:每个机架上待处理的Tasks的集合,即机架与待处理Tasks的身份标识的集合之间的映射关系;
  4. pendingTasksWithNoPrefs:没有任何本地性偏好的待处理Task的身份标识的集合;
  5. allPendingTasks:所有待处理的Task的身份标识的集合;
private val pendingTasksForExecutor = new HashMap[String, ArrayBuffer[Int]]
private val pendingTasksForHost = new HashMap[String, ArrayBuffer[Int]]
private val pendingTasksForRack = new HashMap[String, ArrayBuffer[Int]] 
var pendingTasksWithNoPrefs = new ArrayBuffer[Int]
val allPendingTasks = new ArrayBuffer[Int]

这几个变量的初始化是通过addPendingTask来进行的,TaskSetManager的调度就是从pendingTask里面利用调度策略拿取最优的任务进行计算,主要是通过任务的偏好数据位置匹配相应的本地化级别,符合就加入其中。

// 将待处理Task的索引按照Task的偏好位置, 添加到pendingTasksForExecutor、pendingTasksForHost、pendingTasksForRack、
// pendingTasksWithNoPrefs、allPendingTasks等缓存中。
private def addPendingTask(index: Int) {
   
  // 获取tasks中指定索引位置上的Task的偏好位置序列,并遍历这些偏好位置序列
  for (loc <- tasks(index).preferredLocations) {
    // 遍历的是TaskLocation对象
    // 进行匹配,根据不同的偏好位置,更新到不同的字典中进行记录
    loc match {
   
      case e: ExecutorCacheTaskLocation => // Task所需要计算的数据在Executor上
      // 更新到pendingTasksForExecutor字典
      pendingTasksForExecutor.getOrElseUpdate(e.executorId, new ArrayBuffer) += index
      case e: HDFSCacheTaskLocation => // Task所需要计算的数据存储在HDFS上
      // 获取对应节点上还存活的Executor集合
      val exe = sched.getExecutorsAliveOnHost(loc.host)
      exe match {
   
        case Some(set) => // 存在还存活的Executor
        // 遍历所有存活的Executor
        for (e <- set) {
   
          // 更新到pendingTasksForExecutor字典
          pendingTasksForExecutor.getOrElseUpdate(e, new ArrayBuffer) += index
        }
        case None => logDebug(s"Pending task $index has a cached location at ${e.host} " +
                              ", but there are no executors alive there.")
      }
      case _ => // 其它偏好位置,无处理
    }
    // 更新pendingTasksForHost
    pendingTasksForHost.getOrElseUpdate(loc.host, new ArrayBuffer) += index
    // 获取Task本地性级别中节点所在的机架,更新pendingTasksForRack
    for (rack <- sched.getRackForHost(loc.host)) {
   
      pendingTasksForRack.getOrElseUpdate(rack, new ArrayBuffer) += index
    }
  }
  // 如果Task没有偏好位置信息,则将其索引添加到pendingTasksWithNoPrefs中进行记录
  if (tasks(index).preferredLocations == Nil) {
   
    pendingTasksWithNoPrefs += index
  }
  // 将所有Task的索引添加到allPendingTasks
  allPendingTasks += index 
}

TaskSet

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

  1. tasks是包含的Task的数组;

  2. stageId是Task所属Stage的标识;

  3. stageAttemptId是Stage尝试的标识;

  4. priority是任务集优先级,通常以JobId作为优先级;

  5. id是TaskSet的身份标识。

private[spark] class TaskSet(val tasks: Array[Task[_]],  val stageId: Int, 
                             val stageAttemptId: Int,  val priority: Int, 
                             val properties: Properties) {
   

  // TaskSet的身份标识。
  val id: String = stageId + "." + stageAttemptId

  override def toString: String = "TaskSet " + id
}

TaskInfo

TaskInfoTaskSetManager进行运行一个任务封装的任务信息,包含了以下属性:

  1. taskIdTaskSchedulerImpl生成的用于生成新提交Task的标识;
  2. index是当前任务是在TaskSetManagerTaskSet中的下标;
  3. attemptNumber是当前任务的第几次尝试,每次尝试都会在taskAttempts中添加一条记录;
  4. launchTime是任务启动时间;
  5. executorId是将要运行在哪个executor的唯一标识;host是executor所在机器的host;
  6. taskLocality是任务运行的本地化级别;
  7. speculative是否是推测执行的标识。
class TaskInfo(
    val taskId: Long,
    val index: Int,
    val attemptNumber: Int,
    val launchTime: Long,
    val executorId: String,
    val host: String,
    val taskLocality: TaskLocality.TaskLocality,
    val speculative: Boolean) {
   
}

资源本地性

网络和IO是制约任务运行速度的重要因素,为了使任务运行的更快,提高执行效率,尽可能的使用本地数据,减少网络开销,TaskSetManager对任务的调度是基于任务本地性的延迟调度策略,本节我们详细介绍是如何进行的。

TaskLocality

Spark对任务的处理会考虑数据的本地性,好的数据本地性能够大幅减少节点间的数据传输, 提升程序执行效率。Spark目前支持五种本地性级别,Spark现在支持五种本地化级别,级别从高到低顺序为:PROCESS_LOCALNODE_LOCALNO_PREFRACK_LOCALANY

  1. PROCESS_LOCAL: 进程本地化,task要计算的数据在同一个Executor中,即同一个JVM中
  2. NODE_LOCAL:节点本地化,速度比PROCESS_LOCAL稍慢,因为数据需要在不同进程之间传递或从文件中读取
  3. NO_PREF:没有偏好;
  4. RACK_LOCAL: 机架本地化,数据在同一机架的不同节点上。需要通过网络传输数据及文件IO,比NODE_LOCAL慢;
  5. ANY:跨机架,数据在非同一机架的网络上,速度最慢
object TaskLocality extends Enumeration {
   
  val PROCESS_LOCAL, NODE_LOCAL, NO_PREF, RACK_LOCAL, ANY = Value
  type TaskLocality = Value

  def isAllowed(constraint: TaskLocality, condition: TaskLocality): Boolean = {
   
    // condition的级别小于或等于constraint的本地性级别时,说明constraint支持condition的级别
    condition <= constraint
  }
}

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

相关变量

TaskSetManager是基于本地性级别的延迟调度策略,以下几个变量会参与到调度策略的执行中:

  1. myLocalityLevels是Task的本地性级别的数组,通过computeValidLocalityLevels()方法计算;
  2. localityWaitsmyLocalityLevels中每个本地性级别对应本地性级别的等待时间,有spark参数控制;
    1. spark.locality.wait是默认的等待时间,是3s;
    2. spark.locality.wait.processPROCESS_LOCAL级别的等待时间;
    3. spark.locality.wait.nodeNODE_LOCAL级别的等待时间;
    4. spark.locality.wait.rackRACK_LOCAL级别的等待时间;
  3. currentLocalityIndex是当前本地性级别在myLocalityLevels数组中的下标;
  4. lastLaunchTime是当前本地性级别上上次运行Task的时间,用于看延迟时间是否超过。
var myLocalityLevels = computeValidLocalityLevels()
var localityWaits = myLocalityLevels.map(getLocalityWait)
var currentLocalityIndex = 0
var lastLaunchTime = clock.getTimeMillis()

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

本地性计算

上面介绍到myLocalityLevelsTaskSet中任务支持的的本地化级别,通过调用的computeValidLocalityLevels方法得到,用于计算有效的本地性级别,这样就可以将Task按照本地性级别,由高到低分配给允许的Executor,执行步骤如下:

  1. 如果存在Executor上待处理的Task的集合[即pendingTasksForExecutor不为空]且PROCESS_LOCAL级别的等待时间不为0,还存在已被激活的Executor[即pendingTasksForExecutor中的ExecutorId有存在于TaskSchedulerImplexecutorIdToRunningTaskIds中的],那么允许的本地性级别里包括PROCESS_LOCAL;
  2. 如果存在Host上待处理的Task的集合[即pendingTasksForHost不为空]且NODE_LOCAL级别的等待时间不为0,除此以外,Host上存在已被激活的Executor[即pendingTasksForHost中的Host有存在于TaskSchedulerImplhostToExecutors中的],那么允许的本地性级别里包括NODE_LOCAL;
  3. 如果存在没有任何本地性偏好的待处理Task,那么允许的本地性级别里包括NO_PREF;
  4. 如果存在机架上待处理的Task的集合[即pendingTasksForRack不为空]且RACK_LOCAL级别的等待时间不为0,除此以外,机架上存在已被激活的Executor[即pendingTasksForRack中的机架有存在于TaskSchedulerImplhostsByRack中的],那么允许的本地性级别里包括RACK_LOCAL;
  5. 允许的本地性级别里增加ANY;
  6. 返回所有允许的本地性级别。
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 && // Executor上待处理Task集合不为空
      getLocalityWait(PROCESS_LOCAL) != 0 && // PROCESS_LOCAL级别的等待时间不为0
      pendingTasksForExecutor.keySet.exists(sched.isExecutorAlive(_))) {
    // 还存在已被激活的Executor
    levels += PROCESS_LOCAL // 允许的本地性级别里包括PROCESS_LOCAL
  }
  if (!pendingTasksForHost.isEmpty && // Host上待处理的Task集合不为空
      getLocalityWait(NODE_LOCAL) != 0 && // NODE_LOCAL级别的等待时间不为0
      pendingTasksForHost.keySet.exists(sched.hasExecutorsAliveOnHost(_))) {
    // Host上存在已被激活的Executor
    levels += NODE_LOCAL // 允许的本地性级别里包括NODE_LOCAL
  }
  if (!pendingTasksWithNoPrefs.isEmpty) {
    // 存在没有任何本地性偏好的待处理Task
    levels += NO_PREF // 允许的本地性级别里包括NO_PREF
  }
  if (!pendingTasksForRack.isEmpty && // 机架上待处理的Task的集合不为空
      getLocalityWait(RACK_LOCAL) != 0 && // RACK_LOCAL级别的等待时间不为0
      pendingTasksForRack.keySet.exists(sched.hasHostAliveOnRack(_))) {
    // 机架上存在已被激活的Executor
    levels += RACK_LOCAL // 允许的本地性级别里包括RACK_LOCAL
  }
  levels += ANY // 允许的本地性级别里增加ANY
  levels.toArray // 返回所有允许的本地性级别
}

重新计算本地性

当外界资源发生变化时候需要重新计算支持的本地化级别。

def recomputeLocality() {
   
  // 获取currentLocalityIndex索引值所记录的本地化级别
  val previousLocalityLevel = myLocalityLevels(currentLocalityIndex)
  // 更新支持的本地化级别
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值