Spark 源碼分析(九): Task 啓動

前面已經分析到了 DAGScheduler 對 stage 劃分,並對 Task 的最佳位置進行計算之後,通過調用 taskScheduler 的 submitTasks 方法,將每個 stage 的 taskSet 進行提交。

在 taskScheduler 的 submitTasks 方法中會爲每個 taskSet 創建一個 TaskSetManager,用於管理 taskSet。然後向調度池中添加該 TaskSetManager,最後會調用 backend.reviveOffers() 方法爲 task 分配資源。

override def submitTasks(taskSet: TaskSet) {
  	//獲取 taskSet 中的 task
    val tasks = taskSet.tasks
    logInfo("Adding task set " + taskSet.id + " with " + tasks.length + " tasks")
    this.synchronized {
      // 爲每個 taskSet 創建一個 TaskSetManager
      val manager = createTaskSetManager(taskSet, maxTaskFailures)
      // 拿到 stage 的 id
      val stage = taskSet.stageId
      // 創建一個 HashMap ,用來存儲 stage 對應的 TaskSetManager
      val stageTaskSets =
        taskSetsByStageIdAndAttempt.getOrElseUpdate(stage, new HashMap[Int, TaskSetManager])
      // 將上面創建的 taskSetManager 存入 map 中
      stageTaskSets(taskSet.stageAttemptId) = manager
      val conflictingTaskSet = stageTaskSets.exists { case (_, ts) =>
        ts.taskSet != taskSet && !ts.isZombie
      }
      if (conflictingTaskSet) {
        throw new IllegalStateException(s"more than one active taskSet for stage $stage:" +
          s" ${stageTaskSets.toSeq.map{_._2.taskSet.id}.mkString(",")}")
      }
      // 向調度池中添加剛纔創建的 TaskSetManager
      schedulableBuilder.addTaskSetManager(manager, manager.taskSet.properties)

      // 判斷程序是否爲 local 模式,並且 TaskSchedulerImpl 沒有收到 Task
      if (!isLocal && !hasReceivedTask) {
        // 創建一個定時器,通過指定時間檢查 TaskSchedulerImpl 的飢餓情況
        starvationTimer.scheduleAtFixedRate(new TimerTask() {
          override def run() {
            // 如果 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_MS, STARVATION_TIMEOUT_MS)
      }
      // 標記已經接收到 Task
      hasReceivedTask = true
    }
  	// 給 Task 分配資源
    backend.reviveOffers()
  }

下面主要看 backend.reviveOffers() 這個方法,在提交模式是 standalone 模式下,實際上是調用 StandaloneSchedulerBackend 的 reviveOffers 方法,實則調用的是其父類 CoarseGrainedSchedulerBackend 的 reviveOffers 方法,這個方法是向 driverEndpoint 發送一個 ReviveOffers 消息。

override def reviveOffers() {
  	// 向 driverEndpoint 發送 ReviveOffers 消息
    driverEndpoint.send(ReviveOffers)
  }

DriverEndpoint 收到信息後會調用 makeOffers 方法:

case ReviveOffers =>
        makeOffers()

makeOffers 方法內部會將 application 所有可用的 executor 封裝成一個 workOffers,每個 workOffers 內部封裝了每個 executor 的資源數量。

然後調用 taskScheduler 的 resourceOffers 從上面封裝的 workOffers 信息爲每個 task 分配合適的 executor。

最後調用 launchTasks 啓動 task。

private def makeOffers() {
      // 過濾出可用的 executor
      val activeExecutors = executorDataMap.filterKeys(executorIsAlive)
      // 將這些 executor 封裝成 workOffers
      val workOffers = activeExecutors.map { case (id, executorData) =>
        new WorkerOffer(id, executorData.executorHost, executorData.freeCores)
      }.toIndexedSeq
      // 給每個 task 分配 executor,然後調用 launchTasks 啓動這些 task
      launchTasks(scheduler.resourceOffers(workOffers))
    }

下面看一下 launchTasks 這個方法。

這個方法主要做了這些操作:

1,遍歷每個 task,然後將每個 task 信息序列化。

2,判斷序列化後的 task 信息,如果大於 rpc 發送消息的最大值,則停止,建議調整 rpc 的 maxRpcMessageSize,如果小於 rpc 發送消息的最大值,則找到 task 對應的 executor,然後更新該 executor 對應的一些內存資源信息。

3,向 executor 發送 LaunchTask 消息。

private def launchTasks(tasks: Seq[Seq[TaskDescription]]) {
  		// 遍歷所有的 task
      for (task <- tasks.flatten) {
        // 序列化 task 信息
        val serializedTask = ser.serialize(task)
        // 判斷序列化後的 task 信息是否大於 rpc 能夠傳送的最大信息量
        if (serializedTask.limit >= maxRpcMessageSize) {
          scheduler.taskIdToTaskSetManager.get(task.taskId).foreach { taskSetMgr =>
            try {
              var msg = "Serialized task %s:%d was %d bytes, which exceeds max allowed: " +
                "spark.rpc.message.maxSize (%d bytes). Consider increasing " +
                "spark.rpc.message.maxSize or using broadcast variables for large values."
              msg = msg.format(task.taskId, task.index, serializedTask.limit, maxRpcMessageSize)
              taskSetMgr.abort(msg)
            } catch {
              case e: Exception => logError("Exception in error callback", e)
            }
          }
        }
        else {
          // 找到對應的 executor
          val executorData = executorDataMap(task.executorId)
          // 更新 executor 的資源信息
          executorData.freeCores -= scheduler.CPUS_PER_TASK

          logDebug(s"Launching task ${task.taskId} on executor id: ${task.executorId} hostname: " +
            s"${executorData.executorHost}.")

          // 向 executor 發送 LaunchTask 消息
          executorData.executorEndpoint.send(LaunchTask(new SerializableBuffer(serializedTask)))
        }
      }
    }

下面就看 Executor 收到消息後做了哪些操作,這裏 executorData.executorEndpoint 實際上就是在創建 Executor 守護進程時候創建的那個 CoarseGrainedExecutorBackend。

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

首先他會判斷當前的 executor 是不是爲空,如果不爲空就會反序列化 task 的信息,然後調用 executor 的 launchTask 方法。

case LaunchTask(data) =>
  		// 判斷當前 executor 是不是空
      if (executor == null) {
        exitExecutor(1, "Received LaunchTask command but executor was null")
      } else {
        // 反序列化 task 的信息
        val taskDesc = ser.deserialize[TaskDescription](data.value)
        logInfo("Got assigned task " + taskDesc.taskId)
        // 調用 executor 的 lauchTask 方法
        executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,
          taskDesc.name, taskDesc.serializedTask)
      }

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

首先 executor 會爲每個 task 創建一個 TaskRunner,然後會會將 task 添加到 runningTasks 的集合中,並標記其爲運行狀態,最後將 taskRunner 放到一個線程池中執行。

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)
    runningTasks.put(taskId, tr)
    // 將 taskRunner 放到線程池中執行
    threadPool.execute(tr)
  }

在 taskRunner 的 run 方法中回去執行每個 task,並會輸出一系列的日誌。task 運行完成後迴向 driver 發送消息,driver 會更新 executor 的一些資源數據,並標記 task 已完成。

至此 task 啓動完成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章