Spark源码分析之七:Task运行(一)
在Task调度相关的两篇文章《Spark源码分析之五:Task调度(一)》与《Spark源码分析之六:Task调度(二)》中,我们大致了解了Task调度相关的主要逻辑,并且在Task调度逻辑的最后,CoarseGrainedSchedulerBackend的内部类DriverEndpoint中的makeOffers()方法的最后,我们通过调用TaskSchedulerImpl的resourceOffers()方法,得到了TaskDescription序列的序列Seq[Seq[TaskDescription]],相关代码如下:
- // 调用scheduler的resourceOffers()方法,分配资源,并在得到资源后,调用launchTasks()方法,启动tasks
- // 这个scheduler就是TaskSchedulerImpl
- launchTasks(scheduler.resourceOffers(workOffers))
- /**
- * Called by cluster manager to offer resources on slaves. We respond by asking our active task
- * sets for tasks in order of priority. We fill each node with tasks in a round-robin manner so
- * that tasks are balanced across the cluster.
- *
- * 被集群manager调用以提供slaves上的资源。我们通过按照优先顺序询问活动task集中的task来回应。
- * 我们通过循环的方式将task调度到每个节点上以便tasks在集群中可以保持大致的均衡。
- */
- def resourceOffers(offers: Seq[WorkerOffer]): Seq[Seq[TaskDescription]] = synchronized {
这个TaskDescription很简单,是传递到executor上即将被执行的Task的描述,通常由TaskSetManager的resourceOffer()方法生成。代码如下:
- /**
- * Description of a task that gets passed onto executors to be executed, usually created by
- * [[TaskSetManager.resourceOffer]].
- */
- private[spark] class TaskDescription(
- val taskId: Long,
- val attemptNumber: Int,
- val executorId: String,
- val name: String,
- val index: Int, // Index within this task's TaskSet
- _serializedTask: ByteBuffer)
- extends Serializable {
- // Because ByteBuffers are not serializable, wrap the task in a SerializableBuffer
- // 由于ByteBuffers不可以被序列化,所以将task包装在SerializableBuffer中,_serializedTask为ByteBuffer类型的Task
- private val buffer = new SerializableBuffer(_serializedTask)
- // 序列化后的Task, 取buffer的value
- def serializedTask: ByteBuffer = buffer.value
- override def toString: String = "TaskDescription(TID=%d, index=%d)".format(taskId, index)
- }
此时,得到Seq[Seq[TaskDescription]],即Task被调度到相应executor上后(仅是逻辑调度,实际上并未分配到executor上执行),接下来要做的,便是真正的将Task分配到指定的executor上去执行,也就是本篇我们将要讲的Task的运行。而这部分的开端,源于上述提到的CoarseGrainedSchedulerBackend的内部类DriverEndpoint中的launchTasks()方法,代码如下:
- // Launch tasks returned by a set of resource offers
- private def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
- // 循环每个task
- for (task <- tasks.flatten) {
- // 序列化Task
- val serializedTask = ser.serialize(task)
- // 序列化后的task的大小超出规定的上限
- // 即如果序列化后task的大小大于等于框架配置的Akka消息最大大小减去除序列化task或task结果外,一个Akka消息需要保留的额外大小的值
- if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {
- // 根据task的taskId,在TaskSchedulerImpl的taskIdToTaskSetManager中获取对应的TaskSetManager
- scheduler.taskIdToTaskSetManager.get(task.taskId).foreach { taskSetMgr =>
- try {
- var msg = "Serialized task %s:%d was %d bytes, which exceeds max allowed: " +
- "spark.akka.frameSize (%d bytes) - reserved (%d bytes). Consider increasing " +
- "spark.akka.frameSize or using broadcast variables for large values."
- msg = msg.format(task.taskId, task.index, serializedTask.limit, akkaFrameSize,
- AkkaUtils.reservedSizeBytes)
- // 调用TaskSetManager的abort()方法,标记对应TaskSetManager为失败
- taskSetMgr.abort(msg)
- } catch {
- case e: Exception => logError("Exception in error callback", e)
- }
- }
- }
- else {// 序列化后task的大小在规定的大小内
- // 从executorDataMap中,根据task.executorId获取executor描述信息executorData
- val executorData = executorDataMap(task.executorId)
- // executorData中,freeCores做相应减少
- executorData.freeCores -= scheduler.CPUS_PER_TASK
- // 利用executorData中的executorEndpoint,发送LaunchTask事件,LaunchTask事件中包含序列化后的task
- executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask)))
- }
- }
- }
launchTasks的执行逻辑很简单,针对传入的TaskDescription序列,循环每个Task,做以下处理:
1、首先对Task进行序列化,得到serializedTask;
2、针对序列化后的Task:serializedTask,判断其大小:
2.1、序列化后的task的大小达到或超出规定的上限,即框架配置的Akka消息最大大小,减去除序列化task或task结果外,一个Akka消息需要保留的额外大小的值,则根据task的taskId,在TaskSchedulerImpl的taskIdToTaskSetManager中获取对应的TaskSetManager,并调用其abort()方法,标记对应TaskSetManager为失败;
2.2、序列化后的task的大小未达到上限,在规定的大小范围内,则:
2.2.1、从executorDataMap中,根据task.executorId获取executor描述信息executorData;
2.2.2、在executorData中,freeCores做相应减少;
2.2.3、利用executorData中的executorEndpoint,即Driver端executor通讯端点的引用,发送LaunchTask事件,LaunchTask事件中包含序列化后的task,将Task传递到executor中去执行。
接下来,我们重点分析下上述流程。
先说下异常流程,即序列化后Task的大小超过上限时,对TaskSet标记为失败的处理。入口方法为TaskSetManager的abort()方法,代码如下:
- def abort(message: String, exception: Option[Throwable] = None): Unit = sched.synchronized {
- // TODO: Kill running tasks if we were not terminated due to a Mesos error
- // 调用DAGScheduler的taskSetFailed()方法,标记TaskSet运行失败
- sched.dagScheduler.taskSetFailed(taskSet, message, exception)
- // 标志位isZombie设置为true
- isZombie = true
- // 满足一定条件的情况下,将TaskSet标记为Finished
- maybeFinishTaskSet()
- }
abort()方法处理逻辑共分三步:
第一,调用DAGScheduler的taskSetFailed()方法,标记TaskSet运行失败;
第二,标志位isZombie设置为true;
第三,满足一定条件的情况下,将TaskSet标记为Finished。
首先看下DAGScheduler的taskSetFailed()方法,代码如下:
- /**
- * Called by the TaskSetManager to cancel an entire TaskSet due to either repeated failures or
- * cancellation of the job itself.
- */
- def taskSetFailed(taskSet: TaskSet, reason: String, exception: Option[Throwable]): Unit = {
- eventProcessLoop.post(TaskSetFailed(taskSet, reason, exception))
- }
和第二篇文章《Spark源码分析之二:Job的调度模型与运行反馈》中Job的调度模型一致,都是依靠事件队列eventProcessLoop来完成事件的调度执行的,这里,我们在事件队列eventProcessLoop中放入了一个TaskSetFailed事件。在DAGScheduler的事件处理调度函数doOnReceive()方法中,明确规定了事件的处理方法,代码如下:
- // 如果是TaskSetFailed事件,调用dagScheduler.handleTaskSetFailed()方法处理
- case TaskSetFailed(taskSet, reason, exception) =>
- dagScheduler.handleTaskSetFailed(taskSet, reason, exception)
下面,我们看下handleTaskSetFailed()这个方法。
- private[scheduler] def handleTaskSetFailed(
- taskSet: TaskSet,
- reason: String,
- exception: Option[Throwable]): Unit = {
- // 根据taskSet的stageId获取到对应的Stage,循环调用abortStage,终止该Stage
- stageIdToStage.get(taskSet.stageId).foreach { abortStage(_, reason, exception) }
- // 提交等待的Stages
- submitWaitingStages()
- }
很简单,首先通过taskSet的stageId获取到对应的Stage,针对Stage,循环调用abortStage()方法,终止该Stage,然后调用submitWaitingStages()方法提交等待的Stages。我们先看下abortStage()方法,代码如下:
- /**
- * Aborts all jobs depending on a particular Stage. This is called in response to a task set
- * being canceled by the TaskScheduler. Use taskSetFailed() to inject this event from outside.
- * 终止给定Stage上的所有Job。
- */
- private[scheduler] def abortStage(
- failedStage: Stage,
- reason: String,
- exception: Option[Throwable]): Unit = {
- // 如果stageIdToStage中不存在对应的stage,说明stage已经被移除,直接返回
- if (!stageIdToStage.contains(failedStage.id)) {
- // Skip all the actions if the stage has been removed.
- return
- }
- // 遍历activeJobs中的ActiveJob,逐个调用stageDependsOn()方法,找出存在failedStage的祖先stage的activeJob,即dependentJobs
- val dependentJobs: Seq[ActiveJob] =
- activeJobs.filter(job => stageDependsOn(job.finalStage, failedStage)).toSeq
- // 标记failedStage的完成时间completionTime
- failedStage.latestInfo.completionTime = Some(clock.getTimeMillis())
- // 遍历dependentJobs,调用failJobAndIndependentStages()
- for (job <- dependentJobs) {
- failJobAndIndependentStages(job, s"Job aborted due to stage failure: $reason", exception)
- }
- if (dependentJobs.isEmpty) {
- logInfo("Ignoring failure of " + failedStage + " because all jobs depending on it are done")
- }
- }
这个方法的处理逻辑主要分为四步:
1、如果stageIdToStage中不存在对应的stage,说明stage已经被移除,直接返回,这是对异常情况下的一种特殊处理;
2、遍历activeJobs中的ActiveJob,逐个调用stageDependsOn()方法,找出存在failedStage的祖先stage的activeJob,即dependentJobs;
3、标记failedStage的完成时间completionTime;
4、遍历dependentJobs,调用failJobAndIndependentStages()。
其它都好说,我们主要看下stageDependsOn()和failJobAndIndependentStages()这两个方法。首先看下stageDependsOn()方法,代码如下:
- /** Return true if one of stage's ancestors is target. */
- // 如果参数stage的祖先是target,返回true
- private def stageDependsOn(stage: Stage, target: Stage): Boolean = {
- // 如果stage即为target,返回true
- if (stage == target) {
- return true
- }
- // 存储处理过的RDD
- val visitedRdds = new HashSet[RDD[_]]
- // We are manually maintaining a stack here to prevent StackOverflowError
- // caused by recursively visiting
- // 存储待处理的RDD
- val waitingForVisit = new Stack[RDD[_]]
- // 定义一个visit()方法
- def visit(rdd: RDD[_]) {
- // 如果该RDD未被处理过的话,继续处理
- if (!visitedRdds(rdd)) {
- // 将RDD添加到visitedRdds中
- visitedRdds += rdd
- // 遍历RDD的依赖
- for (dep <- rdd.dependencies) {
- dep match {
- // 如果是ShuffleDependency
- case shufDep: ShuffleDependency[_, _, _] =>
- // 获得mapStage,并且如果stage的isAvailable为false的话,将其压入waitingForVisit
- val mapStage = getShuffleMapStage(shufDep, stage.firstJobId)
- if (!mapStage.isAvailable) {
- waitingForVisit.push(mapStage.rdd)
- } // Otherwise there's no need to follow the dependency back
- // 如果是NarrowDependency,直接将其压入waitingForVisit
- case narrowDep: NarrowDependency[_] =>
- waitingForVisit.push(narrowDep.rdd)
- }
- }
- }
- }
- // 从stage的rdd开始处理,将其入栈waitingForVisit
- waitingForVisit.push(stage.rdd)
- // 当waitingForVisit中存在数据,就调用visit()方法进行处理
- while (waitingForVisit.nonEmpty) {
- visit(waitingForVisit.pop())
- }
- // 根据visitedRdds中是否存在target的rdd判断参数stage的祖先是否为target
- visitedRdds.contains(target.rdd)
- }
这个方法主要是判断参数stage是否为参数target的祖先stage,其代码风格与stage划分和提交中的部分代码一样,这在前面的两篇文章中也提到过,在此不再赘述。而它主要是通过stage的rdd,并遍历其上层依赖的rdd链,将每个stage的rdd加入到visitedRdds中,最后根据visitedRdds中是否存在target的rdd判断参数stage的祖先是否为target。值得一提的是,如果RDD的依赖是NarrowDependency,直接将其压入waitingForVisit,如果为ShuffleDependency,则需要判断stage的isAvailable,如果为false,则将对应RDD压入waitingForVisit。关于isAvailable,我在《Spark源码分析之四:Stage提交》一文中具体阐述过,这里不再赘述。
接下来,我们再看下failJobAndIndependentStages()方法,这个方法的主要作用就是使得一个Job和仅被该Job使用的所有stages失败,并清空有关状态。代码如下:
- /** Fails a job and all stages that are only used by that job, and cleans up relevant state. */
- // 使得一个Job和仅被该Job使用的所有stages失败,并清空有关状态
- private def failJobAndIndependentStages(
- job: ActiveJob,
- failureReason: String,
- exception: Option[Throwable] = None): Unit = {
- // 构造一个异常,内容为failureReason
- val error = new SparkException(failureReason, exception.getOrElse(null))
- // 标志位,是否能取消Stages
- var ableToCancelStages = true
- // 标志位,是否应该中断线程
- val shouldInterruptThread =
- if (job.properties == null) false
- else job.properties.getProperty(SparkContext.SPARK_JOB_INTERRUPT_ON_CANCEL, "false").toBoolean
- // Cancel all independent, running stages.
- // 取消所有独立的,正在运行的stages
- // 根据Job的jobId,获取其stages
- val stages = jobIdToStageIds(job.jobId)
- // 如果stages为空,记录错误日志
- if (stages.isEmpty) {
- logError("No stages registered for job " + job.jobId)
- }
- // 遍历stages,循环处理
- stages.foreach { stageId =>
- // 根据stageId,获取jobsForStage,即每个Job所包含的Stage信息
- val jobsForStage: Option[HashSet[Int]] = stageIdToStage.get(stageId).map(_.jobIds)
- // 首先处理异常情况,即jobsForStage为空,或者jobsForStage中不包含当前Job
- if (jobsForStage.isEmpty || !jobsForStage.get.contains(job.jobId)) {
- logError(
- "Job %d not registered for stage %d even though that stage was registered for the job"
- .format(job.jobId, stageId))
- } else if (jobsForStage.get.size == 1) {
- // 如果stageId对应的stage不存在
- if (!stageIdToStage.contains(stageId)) {
- logError(s"Missing Stage for stage with id $stageId")
- } else {
- // This is the only job that uses this stage, so fail the stage if it is running.
- //
- val stage = stageIdToStage(stageId)
- if (runningStages.contains(stage)) {
- try { // cancelTasks will fail if a SchedulerBackend does not implement killTask
- // 调用taskScheduler的cancelTasks()方法,取消stage内的tasks
- taskScheduler.cancelTasks(stageId, shouldInterruptThread)
- // 标记Stage为完成
- markStageAsFinished(stage, Some(failureReason))
- } catch {
- case e: UnsupportedOperationException =>
- logInfo(s"Could not cancel tasks for stage $stageId", e)
- ableToCancelStages = false
- }
- }
- }
- }
- }
- if (ableToCancelStages) {// 如果能取消Stages
- // 调用job监听器的jobFailed()方法
- job.listener.jobFailed(error)
- // 为Job和独立Stages清空状态,独立Stages的意思为该stage仅为该Job使用
- cleanupStateForJobAndIndependentStages(job)
- // 发送一个SparkListenerJobEnd事件
- listenerBus.post(SparkListenerJobEnd(job.jobId, clock.getTimeMillis(), JobFailed(error)))
- }
- }
处理过程还是很简单的,读者可以通过上述源码和注释自行补脑,这里就先略过了。
下面,再说下正常情况下,即序列化后Task大小未超过上限时,LaunchTask事件的发送及executor端的响应。代码再跳转到CoarseGrainedSchedulerBackend的内部类DriverEndpoint中的launchTasks()方法。正常情况下处理流程主要分为三大部分:
1、从executorDataMap中,根据task.executorId获取executor描述信息executorData;
2、在executorData中,freeCores做相应减少;
3、利用executorData中的executorEndpoint,即Driver端executor通讯端点的引用,发送LaunchTask事件,LaunchTask事件中包含序列化后的task,将Task传递到executor中去执行。
我们重点看下第3步,利用Driver端持有的executor描述信息executorData中的executorEndpoint,即Driver端executor通讯端点的引用,发送LaunchTask事件给executor,将Task传递到executor中去执行。那么executor中是如何接收LaunchTask事件的呢?答案就在CoarseGrainedExecutorBackend中。
我们先说下这个CoarseGrainedExecutorBackend,类的定义如下所示:
- private[spark] class CoarseGrainedExecutorBackend(
- override val rpcEnv: RpcEnv,
- driverUrl: String,
- executorId: String,
- hostPort: String,
- cores: Int,
- userClassPath: Seq[URL],
- env: SparkEnv)
- extends ThreadSafeRpcEndpoint with ExecutorBackend with Logging {
由上面的代码我们可以知道,它实现了ThreadSafeRpcEndpoint和ExecutorBackend两个trait,而ExecutorBackend的定义如下:
- /**
- * A pluggable interface used by the Executor to send updates to the cluster scheduler.
- * 一个被Executor用来发送更新到集群调度器的可插拔接口。
- */
- private[spark] trait ExecutorBackend {
- // 唯一的一个statusUpdate()方法
- // 需要Long类型的taskId、TaskState类型的state、ByteBuffer类型的data三个参数
- def statusUpdate(taskId: Long, state: TaskState, data: ByteBuffer)
- }
那么它自然就有两种主要的任务,第一,作为endpoint提供driver与executor间的通讯功能;第二,提供了executor任务执行时状态汇报的功能。
CoarseGrainedExecutorBackend到底是什么呢?这里我们先不深究,留到以后分析,你只要知道它是Executor的一个后台辅助进程,和Executor是一对一的关系,向Executor提供了与Driver通讯、任务执行时状态汇报两个基本功能即可。
接下来,我们看下CoarseGrainedExecutorBackend是如何处理LaunchTask事件的。做为RpcEndpoint,在其处理各类事件或消息的receive()方法中,定义如下:
- case LaunchTask(data) =>
- if (executor == null) {
- logError("Received LaunchTask command but executor was null")
- System.exit(1)
- } else {
- // 反序列话task,得到taskDesc
- val taskDesc = ser.deserialize[TaskDescription](data.value)
- logInfo("Got assigned task " + taskDesc.taskId)
- // 调用executor的launchTask()方法加载task
- executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,
- taskDesc.name, taskDesc.serializedTask)
- }
首先,会判断对应的executor是否为空,为空的话,记录错误日志并退出,不为空的话,则按照如下流程处理:
1、反序列话task,得到taskDesc;
2、调用executor的launchTask()方法加载task。
那么,重点就落在了Executor的launchTask()方法中,代码如下:
- def launchTask(
- context: ExecutorBackend,
- taskId: Long,
- attemptNumber: Int,
- taskName: String,
- serializedTask: ByteBuffer): Unit = {
- // 新建一个TaskRunner
- val tr = new TaskRunner(context, taskId = taskId, attemptNumber = attemptNumber, taskName,
- serializedTask)
- // 将taskId与TaskRunner的对应关系存入runningTasks
- runningTasks.put(taskId, tr)
- // 线程池执行TaskRunner
- threadPool.execute(tr)
- }
非常简单,创建一个TaskRunner对象,然后将taskId与TaskRunner的对应关系存入runningTasks,将TaskRunner扔到线程池中去执行即可。
我们先看下这个TaskRunner类。我们先看下Class及其成员变量的定义,如下:
- class TaskRunner(
- execBackend: ExecutorBackend,
- val taskId: Long,
- val attemptNumber: Int,
- taskName: String,
- serializedTask: ByteBuffer)
- extends Runnable {
- // TaskRunner继承了Runnable
- /** Whether this task has been killed. */
- // 标志位,task是否被杀掉
- @volatile private var killed = false
- /** How much the JVM process has spent in GC when the task starts to run. */
- @volatile var startGCTime: Long = _
- /**
- * The task to run. This will be set in run() by deserializing the task binary coming
- * from the driver. Once it is set, it will never be changed.
- *
- * 需要运行的task。它将在反序列化来自driver的task二进制数据时在run()方法被设置,一旦被设置,它将不会再发生改变。
- */
- @volatile var task: Task[Any] = _
- }
由类的定义我们可以看出,TaskRunner继承了Runnable,所以它本质上是一个线程,故其可以被放到线程池中去运行。它所包含的成员变量,主要有以下几个:
1、execBackend:Executor后台辅助进程,提供了与Driver通讯、状态汇报等两大基本功能,实际上传入的是CoarseGrainedExecutorBackend实例;
2、taskId:Task的唯一标识;
3、attemptNumber:Task运行的序列号,Spark与MapReduce一样,可以为拖后腿任务启动备份任务,即推测执行原理,如此,就需要通过taskId加attemptNumber来唯一标识一个Task运行实例;
4、serializedTask:ByteBuffer类型,序列化后的Task,包含的是Task的内容,通过发序列化它来得到Task,并运行其中的run()方法来执行Task;
5、killed:Task是否被杀死的标志位;
6、task:Task[Any]类型,需要运行的Task,它将在反序列化来自driver的task二进制数据时在run()方法被设置,一旦被设置,它将不会再发生改变;
7、startGCTime:JVM在task开始运行后,进行垃圾回收的时间。
另外,既然是一个线程,TaskRunner必须得提供run()方法,该run()方法就是TaskRunner线程在线程池中被调度时,需要执行的方法,我们来看下它的定义:
- override def run(): Unit = {
- // Step1:Task及其运行时需要的辅助对象构造
- // 获取任务内存管理器
- val taskMemoryManager = new TaskMemoryManager(env.memoryManager, taskId)
- // 反序列化开始时间
- val deserializeStartTime = System.currentTimeMillis()
- // 当前线程设置上下文类加载器
- Thread.currentThread.setContextClassLoader(replClassLoader)
- // 从SparkEnv中获取序列化器
- val ser = env.closureSerializer.newInstance()
- logInfo(s"Running $taskName (TID $taskId)")
- // execBackend更新状态TaskState.RUNNING
- execBackend.statusUpdate(taskId, TaskState.RUNNING, EMPTY_BYTE_BUFFER)
- var taskStart: Long = 0
- // 计算垃圾回收的时间
- startGCTime = computeTotalGcTime()
- try {
- // 调用Task的deserializeWithDependencies()方法,反序列化Task,得到Task运行需要的文件taskFiles、jar包taskFiles和Task二进制数据taskBytes
- val (taskFiles, taskJars, taskBytes) = Task.deserializeWithDependencies(serializedTask)
- updateDependencies(taskFiles, taskJars)
- // 反序列化Task二进制数据taskBytes,得到task实例
- task = ser.deserialize[Task[Any]](taskBytes, Thread.currentThread.getContextClassLoader)
- // 设置Task的任务内存管理器
- task.setTaskMemoryManager(taskMemoryManager)
- // If this task has been killed before we deserialized it, let's quit now. Otherwise,
- // continue executing the task.
- // 如果此时Task被kill,抛出异常,快速退出
- if (killed) {
- // Throw an exception rather than returning, because returning within a try{} block
- // causes a NonLocalReturnControl exception to be thrown. The NonLocalReturnControl
- // exception will be caught by the catch block, leading to an incorrect ExceptionFailure
- // for the task.
- throw new TaskKilledException
- }
- logDebug("Task " + taskId + "'s epoch is " + task.epoch)
- // mapOutputTracker更新Epoch
- env.mapOutputTracker.updateEpoch(task.epoch)
- // Run the actual task and measure its runtime.
- // 运行真正的task,并度量它的运行时间
- // Step2:Task运行
- // task开始时间
- taskStart = System.currentTimeMillis()
- // 标志位threwException设置为true,标识Task真正执行过程中是否抛出异常
- var threwException = true
- // 调用Task的run()方法,真正执行Task,并获得运行结果value
- val (value, accumUpdates) = try {
- // 调用Task的run()方法,真正执行Task
- val res = task.run(
- taskAttemptId = taskId,
- attemptNumber = attemptNumber,
- metricsSystem = env.metricsSystem)
- // 标志位threwException设置为false
- threwException = false
- // 返回res,Task的run()方法中,res的定义为(T, AccumulatorUpdates)
- // 这里,前者为任务运行结果,后者为累加器更新
- res
- } finally {
- // 通过任务内存管理器清理所有的分配的内存
- val freedMemory = taskMemoryManager.cleanUpAllAllocatedMemory()
- if (freedMemory > 0) {
- val errMsg = s"Managed memory leak detected; size = $freedMemory bytes, TID = $taskId"
- if (conf.getBoolean("spark.unsafe.exceptionOnMemoryLeak", false) && !threwException) {
- throw new SparkException(errMsg)
- } else {
- logError(errMsg)
- }
- }
- }
- // task完成时间
- val taskFinish = System.currentTimeMillis()
- // If the task has been killed, let's fail it.
- // 如果task被杀死,抛出TaskKilledException异常
- if (task.killed) {
- throw new TaskKilledException
- }
- // Step3:Task运行结果处理
- // 通过Spark获取Task运行结果序列化器
- val resultSer = env.serializer.newInstance()
- // 结果序列化前的时间点
- val beforeSerialization = System.currentTimeMillis()
- // 利用Task运行结果序列化器序列化Task运行结果,得到valueBytes
- val valueBytes = resultSer.serialize(value)
- // 结果序列化后的时间点
- val afterSerialization = System.currentTimeMillis()
- // 度量指标体系相关,暂不介绍
- for (m <- task.metrics) {
- // Deserialization happens in two parts: first, we deserialize a Task object, which
- // includes the Partition. Second, Task.run() deserializes the RDD and function to be run.
- m.setExecutorDeserializeTime(
- (taskStart - deserializeStartTime) + task.executorDeserializeTime)
- // We need to subtract Task.run()'s deserialization time to avoid double-counting
- m.setExecutorRunTime((taskFinish - taskStart) - task.executorDeserializeTime)
- m.setJvmGCTime(computeTotalGcTime() - startGCTime)
- m.setResultSerializationTime(afterSerialization - beforeSerialization)
- m.updateAccumulators()
- }
- // 构造DirectTaskResult,同时包含Task运行结果valueBytes和累加器更新值accumulator updates
- val directResult = new DirectTaskResult(valueBytes, accumUpdates, task.metrics.orNull)
- // 序列化DirectTaskResult,得到serializedDirectResult
- val serializedDirectResult = ser.serialize(directResult)
- // 获取Task运行结果大小
- val resultSize = serializedDirectResult.limit
- // directSend = sending directly back to the driver
- // directSend的意思就是直接发送结果至Driver端
- val serializedResult: ByteBuffer = {
- // 如果Task运行结果大小大于所有Task运行结果的最大大小,序列化IndirectTaskResult
- // IndirectTaskResult为存储在Worker上BlockManager中DirectTaskResult的一个引用
- if (maxResultSize > 0 && resultSize > maxResultSize) {
- logWarning(s"Finished $taskName (TID $taskId). Result is larger than maxResultSize " +
- s"(${Utils.bytesToString(resultSize)} > ${Utils.bytesToString(maxResultSize)}), " +
- s"dropping it.")
- ser.serialize(new IndirectTaskResult[Any](TaskResultBlockId(taskId), resultSize))
- }
- // 如果 Task运行结果大小超过Akka除去需要保留的字节外最大大小,则将结果写入BlockManager
- // 即运行结果无法通过消息传递
- else if (resultSize >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {
- val blockId = TaskResultBlockId(taskId)
- env.blockManager.putBytes(
- blockId, serializedDirectResult, StorageLevel.MEMORY_AND_DISK_SER)
- logInfo(
- s"Finished $taskName (TID $taskId). $resultSize bytes result sent via BlockManager)")
- ser.serialize(new IndirectTaskResult[Any](blockId, resultSize))
- }
- // Task运行结果比较小的话,直接返回,通过消息传递
- else {
- logInfo(s"Finished $taskName (TID $taskId). $resultSize bytes result sent to driver")
- serializedDirectResult
- }
- }
- // execBackend更新状态TaskState.FINISHED
- execBackend.statusUpdate(taskId, TaskState.FINISHED, serializedResult)
- } catch {// 处理各种异常信息
- case ffe: FetchFailedException =>
- val reason = ffe.toTaskEndReason
- execBackend.statusUpdate(taskId, TaskState.FAILED, ser.serialize(reason))
- case _: TaskKilledException | _: InterruptedException if task.killed =>
- logInfo(s"Executor killed $taskName (TID $taskId)")
- execBackend.statusUpdate(taskId, TaskState.KILLED, ser.serialize(TaskKilled))
- case cDE: CommitDeniedException =>
- val reason = cDE.toTaskEndReason
- execBackend.statusUpdate(taskId, TaskState.FAILED, ser.serialize(reason))
- case t: Throwable =>
- // Attempt to exit cleanly by informing the driver of our failure.
- // If anything goes wrong (or this was a fatal exception), we will delegate to
- // the default uncaught exception handler, which will terminate the Executor.
- logError(s"Exception in $taskName (TID $taskId)", t)
- val metrics: Option[TaskMetrics] = Option(task).flatMap { task =>
- task.metrics.map { m =>
- m.setExecutorRunTime(System.currentTimeMillis() - taskStart)
- m.setJvmGCTime(computeTotalGcTime() - startGCTime)
- m.updateAccumulators()
- m
- }
- }
- val serializedTaskEndReason = {
- try {
- ser.serialize(new ExceptionFailure(t, metrics))
- } catch {
- case _: NotSerializableException =>
- // t is not serializable so just send the stacktrace
- ser.serialize(new ExceptionFailure(t, metrics, false))
- }
- }
- // execBackend更新状态TaskState.FAILED
- execBackend.statusUpdate(taskId, TaskState.FAILED, serializedTaskEndReason)
- // Don't forcibly exit unless the exception was inherently fatal, to avoid
- // stopping other tasks unnecessarily.
- if (Utils.isFatalError(t)) {
- SparkUncaughtExceptionHandler.uncaughtException(t)
- }
- } finally {
- // 最后,无论运行成功还是失败,将task从runningTasks中移除
- runningTasks.remove(taskId)
- }
- }
如此长的一个方法,好长好大,哈哈!不过,纵观全篇,无非三个Step就可搞定:
1、Step1:Task及其运行时需要的辅助对象构造;
2、Step2:Task运行;
3、Step3:Task运行结果处理。
对, 就这么简单!鉴于时间与篇幅问题,我们这里先讲下主要流程,细节方面的东西留待下节继续。
下面,我们一个个Step来看,首先看下Step1:Task及其运行时需要的辅助对象构造,主要包括以下步骤:
1.1、构造TaskMemoryManager任务内存管理器,即taskMemoryManager;
1.2、记录反序列化开始时间;
1.3、当前线程设置上下文类加载器;
1.4、从SparkEnv中获取序列化器ser;
1.5、execBackend更新状态TaskState.RUNNING;
1.6、计算垃圾回收时间;
1.7、调用Task的deserializeWithDependencies()方法,反序列化Task,得到Task运行需要的文件taskFiles、jar包taskFiles和Task二进制数据taskBytes;
1.8、反序列化Task二进制数据taskBytes,得到task实例;
1.9、设置Task的任务内存管理器;
1.10、如果此时Task被kill,抛出异常,快速退出;
接下来,是Step2:Task运行,主要流程如下:
2.1、获取task开始时间;
2.2、标志位threwException设置为true,标识Task真正执行过程中是否抛出异常;
2.3、调用Task的run()方法,真正执行Task,并获得运行结果value,和累加器更新accumUpdates;
2.4、标志位threwException设置为false;
2.5、通过任务内存管理器taskMemoryManager清理所有的分配的内存;
2.6、获取task完成时间;
2.7、如果task被杀死,抛出TaskKilledException异常。
最后一步,Step3:Task运行结果处理,大体流程如下:
3.1、通过SparkEnv获取Task运行结果序列化器;
3.2、获取结果序列化前的时间点;
3.3、利用Task运行结果序列化器序列化Task运行结果value,得到valueBytes;
3.4、获取结果序列化后的时间点;
3.5、度量指标体系相关,暂不介绍;
3.6、构造DirectTaskResult,同时包含Task运行结果valueBytes和累加器更新值accumulator updates;
3.7、序列化DirectTaskResult,得到serializedDirectResult;
3.8、获取Task运行结果大小;
3.9、处理Task运行结果:
3.9.1、如果Task运行结果大小大于所有Task运行结果的最大大小,序列化IndirectTaskResult,IndirectTaskResult为存储在Worker上BlockManager中DirectTaskResult的一个引用;
3.9.2、如果 Task运行结果大小超过Akka除去需要保留的字节外最大大小,则将结果写入BlockManager,Task运行结果比较小的话,直接返回,通过消息传递;
3.9.3、Task运行结果比较小的话,直接返回,通过消息传递
3.10、execBackend更新状态TaskState.FINISHED;
最后,无论运行成功还是失败,将task从runningTasks中移除。
至此,Task的运行主体流程已经介绍完毕,剩余的部分细节,包括Task内run()方法的具体执行,还有任务内存管理器、序列化器、累加更新,还有部分异常情况处理,状态汇报等等其他更为详细的内容留到下篇再讲吧!
明天还要工作,洗洗睡了!
博客原地址:http://blog.csdn.net/lipeng_bigdata/article/details/50726216
Spark源码分析之七:Task运行(一)的更多相关文章
- spark 源码分析之七--Spark RPC剖析之RpcEndPoint和RpcEndPointRef剖析
RpcEndpoint 文档对RpcEndpoint的解释:An end point for the RPC that defines what functions to trigger given ...
- Spark源码分析之八:Task运行(二)
在<Spark源码分析之七:Task运行(一)>一文中,我们详细叙述了Task运行的整体流程,最终Task被传输到Executor上,启动一个对应的TaskRunner线程,并且在线程池中 ...
- Spark源码分析之九:内存管理模型
Spark是现在很流行的一个基于内存的分布式计算框架,既然是基于内存,那么自然而然的,内存的管理就是Spark存储管理的重中之重了.那么,Spark究竟采用什么样的内存管理模型呢?本文就为大家揭开Sp ...
- Spark 源码分析系列
如下,是 spark 源码分析系列的一些文章汇总,持续更新中...... Spark RPC spark 源码分析之五--Spark RPC剖析之创建NettyRpcEnv spark 源码分析之六- ...
- spark 源码分析之十二 -- Spark内置RPC机制剖析之八Spark RPC总结
在spark 源码分析之五 -- Spark内置RPC机制剖析之一创建NettyRpcEnv中,剖析了NettyRpcEnv的创建过程. Dispatcher.NettyStreamManager.T ...
- Spark源码分析之六:Task调度(二)
话说在<Spark源码分析之五:Task调度(一)>一文中,我们对Task调度分析到了DriverEndpoint的makeOffers()方法.这个方法针对接收到的ReviveOffer ...
- Spark源码分析之五:Task调度(一)
在前四篇博文中,我们分析了Job提交运行总流程的第一阶段Stage划分与提交,它又被细化为三个分阶段: 1.Job的调度模型与运行反馈: 2.Stage划分: 3.Stage提交:对应TaskSet的 ...
- Spark源码分析之二:Job的调度模型与运行反馈
在<Spark源码分析之Job提交运行总流程概述>一文中,我们提到了,Job提交与运行的第一阶段Stage划分与提交,可以分为三个阶段: 1.Job的调度模型与运行反馈: 2.Stage划 ...
- spark 源码分析之二十一 -- Task的执行流程
引言 在上两篇文章 spark 源码分析之十九 -- DAG的生成和Stage的划分 和 spark 源码分析之二十 -- Stage的提交 中剖析了Spark的DAG的生成,Stage的划分以及St ...
随机推荐
- Codevs 搜索刷题 集合篇
2919 选择题 时间限制: 1 s 空间限制: 16000 KB 题目等级 : 黄金 Gold 题目描述 Description 某同学考试,在N*M的答题卡上写了A,B,C,D四种答案. 他做完了 ...
- Educational Codeforces Round 37 A B C D E F
A. water the garden Code #include <bits/stdc++.h> #define maxn 210 using namespace std; typede ...
- 多线程设计模式 - Future模式
Future模式是多线程开发中非常常见的一种设计模式,它的核心思想是异步调用.这类似我们日常生活中的在线购物流程,带在购物网看着一件商品时可以提交表单,当订单完成后就可以在家里等待商品送货上门.或者说 ...
- Codeforces Round #466 (Div. 2) B. Our Tanya is Crying Out Loud[将n变为1,有两种方式,求最小花费/贪心]
B. Our Tanya is Crying Out Loud time limit per test 1 second memory limit per test 256 megabytes inp ...
- PHP利用lua实现Redis Sorted set的zPop操作
function zPop($key) { $script = <<<EOD local v = redis.call('zrange', KEYS[1], 0, 0); if v[ ...
- 一个完整的Core Data应用
在这篇文章中,我们将建立一个小型但却全面支持Core Data的应用.应用允许你创建嵌套的列表:每个列表的item都可以有子列表,这将允许你创建非常深层次的item.为了让大家完整的了解发生了什么,我 ...
- visual studio 2010 调试
非startup project网站 通过attach to process 添加进程w3wp可以实现断点调试 若有多个,可以在iis中添加应用程序池,然后在网站的高级设置里设置应用程序池里,选择对 ...
- Rebound动画框架简单介绍
Rebound动画框架简单介绍 Android菜鸟一枚,有不对的地方希望大家指出,谢谢. 最近在接手了一个老项目,发现里面动画框架用的是facebook中的Rebound框架,由于以前没听说过,放假时 ...
- Error Code: 1055 incompatible with sql_mode=only_full_group_by
OperationalError at / (1055, "Expression #1 of ORDER BY clause is not in GROUP BY clause and co ...
- Testin云測手游质量管家 七大兵器助CP称霸江湖
Testin云測手游质量管家 七大兵器助CP称霸江湖 2014/09/29 · Testin · 产品评測 在武侠江湖里,高手不须要武功高强.亦要具备厉害的武器.有人的地方就有江湖.手游行业相同腥风血 ...