概述
TaskSetManager
是一个stage
的任务集合的抽象,主要进行当前Stage
任务集管理,跟踪每一个任务,进行失败重试,推测执行以及基于数据本地性的调度等,对外只提供两个接口,resourceOffer
来判断是否在指定的executor上面执行任务,statusUpdate
在任务成功或者失败时候,告知任务状态改变。本文将分析源码,解密它是如何进行调度任务执行,推测执行以及失败重试的。
基本属性
我们先来看下TaskSetManager
的主要成员变量,主要是任务的管理相关的变量:
taskSet
:是当前stage
对应的任务集合;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的集合,记录的是taskId;isZombie
:当TaskSetManager
所管理的TaskSet中的所有Task都执行成功了,不再有更多的Task尝试被启动时,就处理“僵尸”状态。例如,每个Task至少有一次尝试成功,或者TaskSet被舍弃了,TaskSetManager
将会进入“僵尸”状态,直到所有的Task都运行成功为止,TaskSetManage
r将一保持在“僵尸”状态。TaskSetManager
的“僵尸”状态并不是无用的,在这种状态下可以继续跟踪、记录正在运行的Task;taskInfos
:记录每次尝试对应的Task的信息;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
,所以继承了以下变量:
weight
和minShare
分别代表公平调度算法的权重和最小资源值,用于FAIR调度优先级比较;priority
用于FIFO调度优先级的比较,是JobId;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:
pendingTasksForExecutor
:每个Executor上待处理的Task的集合,即Executor的身份标识与待处理Task的身份标识的集合之间的映射关系;pendingTasksForHost
:每个Host上待处理的Tasks的集合,即Host与待处理Task的身份标识的集合之间的映射关系;pendingTasksForRack
:每个机架上待处理的Tasks的集合,即机架与待处理Tasks的身份标识的集合之间的映射关系;pendingTasksWithNoPrefs
:没有任何本地性偏好的待处理Task的身份标识的集合;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打包为TaskSet
。TaskSet
是整个调度池中对Task进行调度管理的基本单位,由调度池中的TaskSetManager
来管理,其定义如下:
-
tasks
是包含的Task的数组; -
stageId
是Task所属Stage的标识; -
stageAttemptId
是Stage尝试的标识; -
priority
是任务集优先级,通常以JobId作为优先级; -
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
TaskInfo
是TaskSetManager
进行运行一个任务封装的任务信息,包含了以下属性:
taskId
是TaskSchedulerImpl
生成的用于生成新提交Task的标识;index
是当前任务是在TaskSetManager
中TaskSet
中的下标;attemptNumber
是当前任务的第几次尝试,每次尝试都会在taskAttempts
中添加一条记录;launchTime
是任务启动时间;executorId
是将要运行在哪个executor的唯一标识;host
是executor所在机器的host;taskLocality
是任务运行的本地化级别;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_LOCAL
、NODE_LOCAL
、NO_PREF
、RACK_LOCAL
、ANY
:
PROCESS_LOCAL
: 进程本地化,task要计算的数据在同一个Executor中,即同一个JVM中;NODE_LOCAL
:节点本地化,速度比PROCESS_LOCAL
稍慢,因为数据需要在不同进程之间传递或从文件中读取;NO_PREF
:没有偏好;RACK_LOCAL
: 机架本地化,数据在同一机架的不同节点上。需要通过网络传输数据及文件IO,比NODE_LOCAL慢;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
是基于本地性级别的延迟调度策略,以下几个变量会参与到调度策略的执行中:
myLocalityLevels
是Task的本地性级别的数组,通过computeValidLocalityLevels()
方法计算;localityWaits
是myLocalityLevels
中每个本地性级别对应本地性级别的等待时间,有spark参数控制;spark.locality.wait
是默认的等待时间,是3s;spark.locality.wait.process
是PROCESS_LOCAL
级别的等待时间;spark.locality.wait.node
是NODE_LOCAL
级别的等待时间;spark.locality.wait.rack
是RACK_LOCAL
级别的等待时间;
currentLocalityIndex
是当前本地性级别在myLocalityLevels
数组中的下标;lastLaunchTime
是当前本地性级别上上次运行Task的时间,用于看延迟时间是否超过。
var myLocalityLevels = computeValidLocalityLevels()
var localityWaits = myLocalityLevels.map(getLocalityWait)
var currentLocalityIndex = 0
var lastLaunchTime = clock.getTimeMillis()
TaskSetManager
中实现的本地性操作包括对TaskSet的本地性级别进行计算、获取某个本地性级别的等待时间、给Task分配资源时获取允许的本地性级别等。
本地性计算
上面介绍到myLocalityLevels
是TaskSet
中任务支持的的本地化级别,通过调用的computeValidLocalityLevels
方法得到,用于计算有效的本地性级别,这样就可以将Task按照本地性级别,由高到低分配给允许的Executor,执行步骤如下:
- 如果存在
Executor
上待处理的Task的集合[即pendingTasksForExecutor不为空]且PROCESS_LOCAL
级别的等待时间不为0,还存在已被激活的Executor[即pendingTasksForExecutor
中的ExecutorId有存在于TaskSchedulerImpl
的executorIdToRunningTaskIds
中的],那么允许的本地性级别里包括PROCESS_LOCAL
; - 如果存在
Host
上待处理的Task的集合[即pendingTasksForHost不为空]且NODE_LOCAL
级别的等待时间不为0,除此以外,Host上存在已被激活的Executor[即pendingTasksForHost
中的Host有存在于TaskSchedulerImpl
的hostToExecutors
中的],那么允许的本地性级别里包括NODE_LOCAL
; - 如果存在没有任何本地性偏好的待处理Task,那么允许的本地性级别里包括
NO_PREF
; - 如果存在机架上待处理的Task的集合[即
pendingTasksForRack
不为空]且RACK_LOCAL
级别的等待时间不为0,除此以外,机架上存在已被激活的Executor[即pendingTasksForRack
中的机架有存在于TaskSchedulerImpl
的hostsByRack
中的],那么允许的本地性级别里包括RACK_LOCAL
; - 允许的本地性级别里增加ANY;
- 返回所有允许的本地性级别。
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)
// 更新支持的本地化级别