SparkCore — Task執行源碼分析之TaskRunner.run()

Task原理與源碼分析

  在Executor註冊完成之後,接收到Driver發送的LaunchTask消息之後,會調用executor執行句柄的launchTask()方法,裏面封裝了TaskRunner線程,然後將其放入線程池中運行,下面看一下TaskRunner的run()方法。
  由於run方法代碼比較長,我把它分爲三個部分來說,一個是準備工作,一個是task的執行,一個是task執行結束後的工作。先看準備工作:

override def run(): Unit = {
      // 創建task運行時需要的一些組件
      // 創建task的內存管理器
      val taskMemoryManager = new TaskMemoryManager(env.memoryManager, taskId)
      // 反序列化task開始時間
      val deserializeStartTime = System.currentTimeMillis()
      // 創建類加載器
      Thread.currentThread.setContextClassLoader(replClassLoader)
      // 序列化對象
      val ser = env.closureSerializer.newInstance()
      logInfo(s"Running $taskName (TID $taskId)")
      // 更新executor的狀態, 向(Driver)SparkDeploySchedulerBackend發送StatusUpdate,也即Task正在運行的消息
      execBackend.statusUpdate(taskId, TaskState.RUNNING, EMPTY_BYTE_BUFFER)
      var taskStart: Long = 0
      // 記錄GC開始時間
      startGCTime = computeTotalGcTime()

      try {
        // 對序列化的task數據,進行反序列化
        val (taskFiles, taskJars, taskBytes) = Task.deserializeWithDependencies(serializedTask)
        // 通過網絡通信,將需要的資源、文件、Jar拷貝過來。
        updateDependencies(taskFiles, taskJars)
        // 通過正式的反序列化操作,將整個task的數據集反序列化回來
        // 類加載器,Java的ClassLoader的作用,比如可以使用反射的方式來動態加載一個類,然後創建這個類的對象
        // 還有可以對指定上下文的相關資源,進行加載和讀取
        task = ser.deserialize[Task[Any]](taskBytes, Thread.currentThread.getContextClassLoader)
        task.setTaskMemoryManager(taskMemoryManager)
        // 如果task被kill,拋異常
        if (killed) {
          throw new TaskKilledException
        }
   }

  上面是Task運行前的一些準備工作,主要是開啓一些計時器,其中比較重要的是updateDependencies()這個方法,它會從網絡拉取Task運行所需的文件和Jar包,支持拉取Hadoop兼容的文件系統等等。
  接着就是執行task的部分:

	// 統計Task的開始時間
    taskStart = System.currentTimeMillis()
     var threwException = true
     // 這裏的value對於ShuffleMapTask來說,其實就是MapStatus,
     // 封裝了ShuffleMapTask計算的數據,輸出的位置
     // 後面如果還是一個ShuffleMapTask,就會去聯繫MapOutputTracker。來獲取上一個ShuffleMapTask的輸出位置
     // 然後通過網絡拉取數據,ResultTask也是一樣的。
     val (value, accumUpdates) = try {
       // 執行task,用的task的run方法
       val res = task.run(
         taskAttemptId = taskId,
         attemptNumber = attemptNumber,
         metricsSystem = env.metricsSystem)
       threwException = false
       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)
         }
       }
     }
     // 統計結束時間
     val taskFinish = System.currentTimeMillis()

  上面就是task的運行部分,主要是調用了task.run()方法,這裏的它有兩個返回值,一個是value,一個是accumUpdates(這個沒有研究,有大神知道的話,希望能夠告知)。其中value對於ShuffleMapTask而言就是Task執行結束後Map的狀態,MapStatus,裏面包含了執行結果數據的位置等信息。
  Task運行結束之後,下面看一下對task運行結果的一些操作:

 		// 獲取序列化的對象
        val resultSer = env.serializer.newInstance()
        val beforeSerialization = System.currentTimeMillis()
        // 對task輸出結果進行序列化
        val valueBytes = resultSer.serialize(value)
        val afterSerialization = System.currentTimeMillis()

        // 統計出Task相關的運行時間,這些會在Spark UI上顯示,大家爭着在企業中運行我們的長時間
        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)
          // JVM GC的時間
          m.setJvmGCTime(computeTotalGcTime() - startGCTime)
          // values的序列化耗費多久時間
          m.setResultSerializationTime(afterSerialization - beforeSerialization)
          m.updateAccumulators()
        }

        // 將Task序列化的運行結果封裝爲DirectTaskResult
        val directResult = new DirectTaskResult(valueBytes, accumUpdates, task.metrics.orNull)
        // 在進行序列化
        val serializedDirectResult = ser.serialize(directResult)
        // 序列化後的大小
        val resultSize = serializedDirectResult.limit

        // directSend = sending directly back to the driver
        val serializedResult: ByteBuffer = {
          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))
          } 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))
          } else {
            logInfo(s"Finished $taskName (TID $taskId). $resultSize bytes result sent to driver")
            serializedDirectResult
          }
        }

        // 這個就非常重要,就是調用了executor所在的CoarseGrainedExecutorBackend的statusUpdate方法
        execBackend.statusUpdate(taskId, TaskState.FINISHED, serializedResult)

  從源碼來看,先對Task運行結果value進行一些序列化操作, 然後統計Task運行時候的一些時間信息,比如GC的時間,Task運行時間等;在將結果進行一些封裝之後,在序列化爲serializedDirectResult,這裏面用到了BlockManager組件(shuffle底層的內存管理組件,後面再單獨分析它),接着就調用executor所在的CoarseGrainedExecutorBackend的statusUpdate()方法發送StatusUpdate消息給Driver的SparkDeploySchedulerBackend。

  上面是TaskRunner.run()線程的執行邏輯,下一篇博客分析TaskRunner.run()方法中調用的兩個比較重要方法的task.run()和statusUpdate()。

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