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()。