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