Spark源码分析之七:Task运行(一)

本文深入探讨Spark Task的运行流程,包括Task调度的异常处理和正常执行过程。当Task序列化大小超过限制时,TaskSet会被标记为失败;反之,Task将通过LaunchTask事件发送到executor执行。在executor端,CoarseGrainedExecutorBackend接收到LaunchTask事件后,反序列化Task并执行。TaskRunner负责Task的构造、运行和结果处理,涉及内存管理、序列化和状态更新等关键步骤。
摘要由CSDN通过智能技术生成

        在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 repea
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值