Spark1.3.1源码(3)-Task提交

前面我们已经分析了DAGScheduler对Stage划分,并对Task的最佳位置进行计算之后,通过调用taskScheduler的submitTasks方法,将每个stage的taskSet进行提交。
今天给大家讲一下taskScheduler是如何将Task发送到各个Executor上执行的。

image

TaskScheduler

在taskScheduler的submitTasks方法中会为每个TaskSet创建一个TaskSetManager,
用于管理taskSet。然后向调度池中添加该TaskSetManager,
最后调用backend.reviveOffers()方法为Task分配资源
//TODO 该方法用于提交Tasks
  override def submitTasks(taskSet: TaskSet) {
    //TODO 获取taskSet 中的 task
    val tasks = taskSet.tasks
    logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
    this.synchronized {
      //TODO 为taskSet 创建一个TaskSetManager
      val manager = createTaskSetManager(taskSet, maxTaskFailures)
      //TODO 保存到一个map中
      activeTaskSets(taskSet.id) = manager
      //TODO 向调度池中添加刚才创建TaskSetManager
      schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)

      //TODO 判断程序是否为local,并且TaskSchedulerImpl没有收到Task
      if (!isLocal && !hasReceivedTask) {
        //TODO 创建一个定时器定时检查TaskSchedulerImpl的饥饿情况
        starvationTimer.scheduleAtFixedRate(new TimerTask() {
          override def run() {
            //TODO 如果TaskSchedulerImpl已经安排执行了Task,则取消定时器
            if (!hasLaunchedTask) {
              logWarning("Initial job has not accepted any resources; " +
                "check your cluster UI to ensure that workers are registered " +
                "and have sufficient resources")
            } else {
              this.cancel()
            }
          }
        }, STARVATION_TIMEOUT, STARVATION_TIMEOUT)
      }
      //TODO 标记已经接收到Task
      hasReceivedTask = true
    }
    //TODO 给 Task 分配资源
    backend.reviveOffers()
  }

下面我们就来看一下backend.reviveOffers()这个方法,在提交模式是standalone模式下,
实际上是调用SparkDeploySchedulerBackend的reviveOffers方法,由于没有对父类的方法进行重写,
因此调用的是父类CoarseGrainedSchedulerBackend的reviveOffers方法,这个方法是向Driver发送一个ReviveOffers消息

CoarseGrainedSchedulerBackend

override def reviveOffers() {
    //TODO 向DriverActor发送一个reviceOffers消息
    driverActor ! ReviveOffers
  }

DriverActor收到信息后会调用makeOffers方法

 //TODO 调用makeOffers向Executor提交Task
      case ReviveOffers =>
        makeOffers()

makeOffers方法内部会将application所有可用的executor封装成多个workOffer,每个workOffer内部封装了每个executor的资源信息。
然后调用taskScheduler的resourceOffers从上面封装的workOffer信息为每个task分配合适的executor。
最后调用launchTasks启动task

// Make fake resource offers on all executors
    def makeOffers() {
      //TODO 调用launchTask向Executor提交Task
      launchTasks(scheduler.resourceOffers(executorDataMap.map { case (id, executorData) =>
        new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
      }.toSeq))
    }

下面看一下launchTasks这个方法。
这个方法主要做了这些操作:

1.遍历每个task,然后将每个task信息序列化

2.判断序列化后的task信息,如果大于rpc发送消息的最大值,则停止,建议调整rpc的spark.akka.frameSize,如果小于rpc发送消息的最大值,则找到task对应的executor,然后更新executor对应的一些内存资源信息

3.DriverActor向executor发送LaunchTask消息。

// Launch tasks returned by a set of resource offers
    def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
      for (task <- tasks.flatten) {
        //拿到序列化器
        val ser = SparkEnv.get.closureSerializer.newInstance()
        //TODO 序列化Task
        val serializedTask = ser.serialize(task)
        if (serializedTask.limit >= akkaFrameSize - AkkaUtils.reservedSizeBytes) {
          val taskSetId = scheduler.taskIdToTaskSetId(task.taskId)
          scheduler.activeTaskSets.get(taskSetId).foreach { taskSet =>
            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)
              taskSet.abort(msg)
            } catch {
              case e: Exception => logError("Exception in error callback", e)
            }
          }
        }
        else {
          //TODO 找到task对应的executor
          val executorData = executorDataMap(task.executorId)
          executorData.freeCores -= scheduler.CPUS_PER_TASK
          //TODO 向Executor发送序列化好的Task
          executorData.executorActor ! LaunchTask(new SerializableBuffer(serializedTask))
        }
      }
    }

CoarseGrainedExecutorBackend

下面就看Executor收到消息后做了哪些操作,这里executorData.executorActor实际上就是在创建Executor守护进程时候创建的那个CoarseGrainedExecutorBackend

所以找到CoarseGrainedExecutorBackend处理接收到LaunchTask消息后做了哪些操作。

首先判断当前的executor是不是为空,如果不为空就会反序列化task的信息,然后调用executor的launchTask方法。

//TODO DirverActor发送给Executor的消息,让Executor启动计算任务
    case LaunchTask(data) =>
      if (executor == null) {
        logError("Received LaunchTask command but executor was null")
        System.exit(1)
      } else {
        //获得序列化器
        val ser = env.closureSerializer.newInstance()
        //TODO 反序列化Task
        val taskDesc = ser.deserialize[TaskDescription](data.value)
        logInfo("Got assigned task " + taskDesc.taskId)
        //TODO 将反序列化后的Task放到线程池里面
        executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,
          taskDesc.name, taskDesc.serializedTask)
      }

下面就看下executor的launchTask都做了哪些操作

首先executor会为每个task创建一个TaskRunner(实现了Runnable),然后会将task添加到runningTasks的集合中,并标记其为运行状态,最后将taskRunner放到一个线程池中执行

//TODO 启动Task
  def launchTask(
      context: ExecutorBackend,
      taskId: Long,
      attemptNumber: Int,
      taskName: String,
      serializedTask: ByteBuffer) {
    //TODO 创建一个TaskRunner对象,把Task的信息封装到TaskRunner里面
    val tr = new TaskRunner(context, taskId = taskId, attemptNumber = attemptNumber, taskName,
      serializedTask)
    runningTasks.put(taskId, tr)
    //TODO 把TaskRunner丢到线程池中
    threadPool.execute(tr)
  }

在taskRunner的run方法中会去执行每个task,并输出一系列的日志。task运行完成后会向driver发送消息,driver会更新executor的一些资源数据,并标记task已完成。

至此task启动完成

微信公众号:喜讯Xicent

image

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值