《Spark源码分析之七:Task运行(一)》一文中,我们详细叙述了Task运行的整体流程,最终Task被传输到Executor上,启动一个对应的TaskRunner线程,并且在线程池中被调度执行。继而,我们对TaskRunner的run()方法进行了详细的分析,总结出了其内Task执行的三个主要步骤:

Step1:Task及其运行时需要的辅助对象构造,主要包括:

1、当前线程设置上下文类加载器;

2、获取序列化器ser;

3、更新任务状态TaskState;

4、计算垃圾回收时间;

5、反序列化得到Task运行的jar、文件、Task对象二进制数据;

6、反序列化Task对象二进制数据得到Task对象;

7、设置任务内存管理器;

Step2:Task运行:调用Task的run()方法,真正执行Task,并获得运行结果value
        Step3:Task运行结果处理:

1、序列化Task运行结果value,得到valueBytes;

2、根据Task运行结果大小处理Task运行结果valueBytes:

2.1、如果Task运行结果大小大于所有Task运行结果的最大大小,序列化IndirectTaskResult,IndirectTaskResult为存储在Worker上BlockManager中DirectTaskResult的一个引用;

2.2、如果 Task运行结果大小超过Akka除去需要保留的字节外最大大小,则将结果写入BlockManager,Task运行结果比较小的话,直接返回,通过消息传递;

2.3、Task运行结果比较小的话,直接返回,通过消息传递。

大体流程大概就是如此。我们先回顾到这里。那么,接下来的问题是,任务内存管理器是什么?如何计算开始垃圾回收时间?Task的run()方法的执行流程是什么?IndirectTaskResult,或者BlockManager又是如何传递任务运行结果至应用程序即客户端的?

不要着急,我们一个一个来解决。

关于任务内存管理器TaskMemoryManager,可以参照《Spark源码分析之九:内存管理模型》一文,只要知道它是任务运行期间各区域内存的管理者就行,这里不再赘述。

接下来,我们重点分析下Task的run()方法,看看Task实际运行时的处理逻辑。其代码如下:

  1. /**
  2. * Called by [[Executor]] to run this task.
  3. * 被Executor调用以执行Task
  4. *
  5. * @param taskAttemptId an identifier for this task attempt that is unique within a SparkContext.
  6. * @param attemptNumber how many times this task has been attempted (0 for the first attempt)
  7. * @return the result of the task along with updates of Accumulators.
  8. */
  9. final def run(
  10. taskAttemptId: Long,
  11. attemptNumber: Int,
  12. metricsSystem: MetricsSystem)
  13. : (T, AccumulatorUpdates) = {
  14. // 创建一个Task上下文实例:TaskContextImpl类型的context
  15. context = new TaskContextImpl(
  16. stageId,
  17. partitionId,
  18. taskAttemptId,
  19. attemptNumber,
  20. taskMemoryManager,
  21. metricsSystem,
  22. internalAccumulators,
  23. runningLocally = false)
  24. // 将context放入TaskContext的taskContext变量中
  25. // taskContext变量为ThreadLocal[TaskContext]
  26. TaskContext.setTaskContext(context)
  27. // 设置主机名localHostName、内部累加器internalAccumulators等Metrics信息
  28. context.taskMetrics.setHostname(Utils.localHostName())
  29. context.taskMetrics.setAccumulatorsUpdater(context.collectInternalAccumulators)
  30. // task线程为当前线程
  31. taskThread = Thread.currentThread()
  32. if (_killed) {// 如果需要杀死task,调用kill()方法,且调用的方式为不中断线程
  33. kill(interruptThread = false)
  34. }
  35. try {
  36. // 调用runTask()方法,传入Task上下文信息context,执行Task,并调用Task上下文的collectAccumulators()方法,收集累加器
  37. (runTask(context), context.collectAccumulators())
  38. } finally {
  39. // 上下文标记Task完成
  40. context.markTaskCompleted()
  41. try {
  42. Utils.tryLogNonFatalError {
  43. // Release memory used by this thread for unrolling blocks
  44. // 为unrolling块释放当前线程使用的内存
  45. SparkEnv.get.blockManager.memoryStore.releaseUnrollMemoryForThisTask()
  46. // Notify any tasks waiting for execution memory to be freed to wake up and try to
  47. // acquire memory again. This makes impossible the scenario where a task sleeps forever
  48. // because there are no other tasks left to notify it. Since this is safe to do but may
  49. // not be strictly necessary, we should revisit whether we can remove this in the future.
  50. val memoryManager = SparkEnv.get.memoryManager
  51. memoryManager.synchronized { memoryManager.notifyAll() }
  52. }
  53. } finally {
  54. // 释放TaskContext
  55. TaskContext.unset()
  56. }
  57. }
  58. }

代码逻辑非常简单,概述如下:

1、需要创建一个Task上下文实例,即TaskContextImpl类型的context,这个TaskContextImpl主要包括以下内容:Task所属Stage的stageId、Task对应数据分区的partitionId、Task执行的taskAttemptId、Task执行的序号attemptNumber、Task内存管理器taskMemoryManager、指标度量系统metricsSystem、内部累加器internalAccumulators、是否本地运行的标志位runningLocally(为false);

2、将context放入TaskContext的taskContext变量中,这个taskContext变量为ThreadLocal[TaskContext];

3、在任务上下文context中设置主机名localHostName、内部累加器internalAccumulators等Metrics信息;

4、设置task线程为当前线程;

5、如果需要杀死task,调用kill()方法,且调用的方式为不中断线程;

6、调用runTask()方法,传入Task上下文信息context,执行Task,并调用Task上下文的collectAccumulators()方法,收集累加器;

7、最后,任务上下文context标记Task完成,为unrolling块释放当前线程使用的内存,清楚任务上下文等。

接下来自然要看下runTask()方法。但是Task中,runTask()方法却没有实现。我们知道,Task共分为两种类型,一个是最后一个Stage产生的ResultTask,另外一个是其parent Stage产生的ShuffleMapTask。那么,我们分开来分析下,首先看下ShuffleMapTask中的runTask()方法,定义如下:

  1. override def runTask(context: TaskContext): MapStatus = {
  2. // Deserialize the RDD using the broadcast variable.
  3. // 使用广播变量反序列化RDD
  4. // 反序列化的起始时间
  5. val deserializeStartTime = System.currentTimeMillis()
  6. // 获得反序列化器closureSerializer
  7. val ser = SparkEnv.get.closureSerializer.newInstance()
  8. // 调用反序列化器closureSerializer的deserialize()进行RDD和ShuffleDependency的反序列化,数据来源于taskBinary
  9. val (rdd, dep) = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
  10. ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
  11. // 计算Executor进行反序列化的时间
  12. _executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime
  13. metrics = Some(context.taskMetrics)
  14. var writer: ShuffleWriter[Any, Any] = null
  15. try {
  16. // 获得shuffleManager
  17. val manager = SparkEnv.get.shuffleManager
  18. // 通过shuffleManager的getWriter()方法,获得shuffle的writer
  19. // 启动的partitionId表示的是当前RDD的某个partition,也就是说write操作作用于partition之上
  20. writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
  21. // 针对RDD中的分区<span style="font-family: Arial, Helvetica, sans-serif;">partition</span><span style="font-family: Arial, Helvetica, sans-serif;">,调用rdd的iterator()方法后,再调用writer的write()方法,写数据</span>
  22. writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
  23. // 停止writer,并返回标志位
  24. writer.stop(success = true).get
  25. } catch {
  26. case e: Exception =>
  27. try {
  28. if (writer != null) {
  29. writer.stop(success = false)
  30. }
  31. } catch {
  32. case e: Exception =>
  33. log.debug("Could not stop writer", e)
  34. }
  35. throw e
  36. }
  37. }

运行的主要逻辑其实只有两步,如下:

1、通过使用广播变量反序列化得到RDD和ShuffleDependency:

1.1、获得反序列化的起始时间deserializeStartTime;

1.2、通过SparkEnv获得反序列化器ser;

1.3、调用反序列化器ser的deserialize()进行RDD和ShuffleDependency的反序列化,数据来源于taskBinary,得到rdd、dep;

1.4、计算Executor进行反序列化的时间_executorDeserializeTime;

2、利用shuffleManager的writer进行数据的写入:

2.1、通过SparkEnv获得shuffleManager;

2.2、通过shuffleManager的getWriter()方法,获得shuffle的writer,其中的partitionId表示的是当前RDD的某个partition,也就是说write操作作用于partition之上;

2.3、针对RDD中的分区partition,调用rdd的iterator()方法后,再调用writer的write()方法,写数据;

2.4、停止writer,并返回标志位。

至于shuffle的详细内容,我会在后续的博文中深入分析。

下面,再看下ResultTask,其runTask()方法更简单,代码如下:

  1. override def runTask(context: TaskContext): U = {
  2. // Deserialize the RDD and the func using the broadcast variables.
  3. // 获取反序列化的起始时间
  4. val deserializeStartTime = System.currentTimeMillis()
  5. // 获取反序列化器
  6. val ser = SparkEnv.get.closureSerializer.newInstance()
  7. // 调用反序列化器ser的deserialize()方法,得到RDD和FUNC,数据来自taskBinary
  8. val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](
  9. ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
  10. // 计算反序列化时间_executorDeserializeTime
  11. _executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime
  12. metrics = Some(context.taskMetrics)
  13. // 调针对RDD中的每个分区,迭代执行func方法,执行Task
  14. func(context, rdd.iterator(partition, context))
  15. }

首先,获取反序列化的起始时间deserializeStartTime;

其次,通过SparkEnv获取反序列化器ser;

然后,调用反序列化器ser的deserialize()方法,得到RDD和FUNC,数据来自taskBinary;

紧接着,计算反序列化时间_executorDeserializeTime;

最后,调针对RDD中的每个分区,迭代执行func方法,执行Task。

到了这里,读者可能会有一个很大的疑问,Task的运行就这样完了?ReusltTask还好说,它会执行反序列化后得到的func函数,那么ShuffleMapTask呢?仅仅是shuffle的数据写入吗?它的分区数据需要执行什么函数来继续转换呢?现在,我就来为大家解答下这个问题。

首先,在ShuffleMapTask的runTask()方法中,反序列化得到rdd后,在执行writer的write()方法之前,会调用rdd的iterator()函数,对rdd的分区partition进行处理。那么我们看下RDD中的iterator()函数是如何定义的?

  1. /**
  2. * Internal method to this RDD; will read from cache if applicable, or otherwise compute it.
  3. * This should ''not'' be called by users directly, but is available for implementors of custom
  4. * subclasses of RDD.
  5. */
  6. final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
  7. if (storageLevel != StorageLevel.NONE) {
  8. SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel)
  9. } else {
  10. computeOrReadCheckpoint(split, context)
  11. }
  12. }

很简单,它会根据存储级别,来决定:

1、如果存储级别storageLevel不为空,调用SparkEnv中的cacheManager的getOrCompute()方法;

2、如果存储级别storageLevel为空,则调用computeOrReadCheckpoint()方法;
        我们先看下SparkEnv中cacheManager的定义:

  1. val cacheManager = new CacheManager(blockManager)

它是一个CacheManager类型的对象。而CacheManager中getOrCompute()方法的定义如下:

  1. /** Gets or computes an RDD partition. Used by RDD.iterator() when an RDD is cached. */
  2. // 获取或计算一个RDD的分区
  3. def getOrCompute[T](
  4. rdd: RDD[T],
  5. partition: Partition,
  6. context: TaskContext,
  7. storageLevel: StorageLevel): Iterator[T] = {
  8. // 通过rdd的id和分区的索引号,获取RDDBlockId类型的key
  9. val key = RDDBlockId(rdd.id, partition.index)
  10. logDebug(s"Looking for partition $key")
  11. // 在blockManager中根据key查找
  12. blockManager.get(key) match {
  13. // 如果为blockResult,意味着分区Partition已经被物化,直接获取结果即可
  14. case Some(blockResult) =>
  15. // Partition is already materialized, so just return its values
  16. val existingMetrics = context.taskMetrics
  17. .getInputMetricsForReadMethod(blockResult.readMethod)
  18. existingMetrics.incBytesRead(blockResult.bytes)
  19. val iter = blockResult.data.asInstanceOf[Iterator[T]]
  20. new InterruptibleIterator[T](context, iter) {
  21. override def next(): T = {
  22. existingMetrics.incRecordsRead(1)
  23. delegate.next()
  24. }
  25. }
  26. // 如果没有,则需要计算
  27. case None =>
  28. // Acquire a lock for loading this partition
  29. // If another thread already holds the lock, wait for it to finish return its results
  30. // 首先需要为load该分区申请锁,如果其它线程已经获取对应的锁,那么该线程则会一直等待其他线程处理完毕后的返回结果,然后直接返回这个结果即可
  31. val storedValues = acquireLockForPartition[T](key)
  32. if (storedValues.isDefined) {// 如果storedValues被定义的话,直接返回结果
  33. return new InterruptibleIterator[T](context, storedValues.get)
  34. }
  35. // Otherwise, we have to load the partition ourselves
  36. // 当获得了锁后,我们不得不自己load分区
  37. try {
  38. logInfo(s"Partition $key not found, computing it")
  39. // 调用RDD的computeOrReadCheckpoint()方法进行计算
  40. val computedValues = rdd.computeOrReadCheckpoint(partition, context)
  41. // If the task is running locally, do not persist the result
  42. // 如果task是本地运行,不需要持久化数据,直接返回
  43. if (context.isRunningLocally) {
  44. return computedValues
  45. }
  46. // Otherwise, cache the values and keep track of any updates in block statuses
  47. // 否则,需要缓存结果,并对block状态的更新保持追踪
  48. val updatedBlocks = new ArrayBuffer[(BlockId, BlockStatus)]
  49. val cachedValues = putInBlockManager(key, computedValues, storageLevel, updatedBlocks)
  50. val metrics = context.taskMetrics
  51. val lastUpdatedBlocks = metrics.updatedBlocks.getOrElse(Seq[(BlockId, BlockStatus)]())
  52. metrics.updatedBlocks = Some(lastUpdatedBlocks ++ updatedBlocks.toSeq)
  53. new InterruptibleIterator(context, cachedValues)
  54. } finally {
  55. loading.synchronized {
  56. loading.remove(key)
  57. loading.notifyAll()
  58. }
  59. }
  60. }
  61. }

getOrCompute()方法的大体逻辑如下:

1、通过rdd的id和分区的索引号,获取RDDBlockId类型的key;

2、在blockManager中根据key查找:

2.1、如果为blockResult,意味着分区Partition已经被物化,直接获取结果即可;

2.2、如果没有,则需要计算:

2.2.1、首先需要为load该分区申请锁,如果其它线程已经获取对应的锁,那么该线程则会一直等待其他线程处理完毕后的返回结果,然后直接返回这个结果即可;

2.2.2、当获得了锁后,我们不得不自己load分区:

2.2.2.1、调用RDD的computeOrReadCheckpoint()方法进行计算,得到computedValues;

2.2.2.2、如果task是本地运行,不需要持久化数据,直接返回;

2.2.2.3、否则,需要缓存结果,并对block状态的更新保持追踪。

然后,问题又统一性的扔给了RDD的computeOrReadCheckpoint()方法,我们来看下它的实现:

  1. /**
  2. * Compute an RDD partition or read it from a checkpoint if the RDD is checkpointing.
  3. * 计算一个RDD分区,或者如果该RDD正在做checkpoint,直接读取
  4. */
  5. private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] =
  6. {
  7. if (isCheckpointedAndMaterialized) {
  8. firstParent[T].iterator(split, context)
  9. } else {
  10. compute(split, context)
  11. }
  12. }

哦,它原来是调用RDD的compute()方法(其实,通过读了那么多Spark介绍的文章,我早就知道了,这里故作深沉,想真正探寻下它是如何调用到compute()方法的)。

接下来,我们再深入分析下两种Task的执行流程中涉及到的公共部分:反序列化器。它是通过SparkEnv的closureSerializer来获取的,而在SparkEnv中,是如何定义closureSerializer的呢?代码如下:

  1. val closureSerializer = instantiateClassFromConf[Serializer](
  2. "spark.closure.serializer", "org.apache.spark.serializer.JavaSerializer")

也就是说,它实际上取得是参数spark.closure.serializer配置的类,默认是org.apache.spark.serializer.JavaSerializer类。而接下来的instantiateClassFromConf()方法很简单,就是从配置中实例化class得到对象,其定义如下:

  1. // Create an instance of the class named by the given SparkConf property, or defaultClassName
  2. // if the property is not set, possibly initializing it with our conf
  3. def instantiateClassFromConf[T](propertyName: String, defaultClassName: String): T = {
  4. instantiateClass[T](conf.get(propertyName, defaultClassName))
  5. }

继续看instantiateClass()方法,它是根据指定name来创建一个类的实例,代码如下:

  1. // Create an instance of the class with the given name, possibly initializing it with our conf
  2. def instantiateClass[T](className: String): T = {
  3. val cls = Utils.classForName(className)
  4. // Look for a constructor taking a SparkConf and a boolean isDriver, then one taking just
  5. // SparkConf, then one taking no arguments
  6. try {
  7. cls.getConstructor(classOf[SparkConf], java.lang.Boolean.TYPE)
  8. .newInstance(conf, new java.lang.Boolean(isDriver))
  9. .asInstanceOf[T]
  10. } catch {
  11. case _: NoSuchMethodException =>
  12. try {
  13. cls.getConstructor(classOf[SparkConf]).newInstance(conf).asInstanceOf[T]
  14. } catch {
  15. case _: NoSuchMethodException =>
  16. cls.getConstructor().newInstance().asInstanceOf[T]
  17. }
  18. }
  19. }

同过类名来获得类,并调用其构造方法进行对象的构造。我们看下序列化器的默认实现org.apache.spark.serializer.JavaSerializer的deserialize()方法,代码如下:

  1. override def deserialize[T: ClassTag](bytes: ByteBuffer, loader: ClassLoader): T = {
  2. val bis = new ByteBufferInputStream(bytes)
  3. val in = deserializeStream(bis, loader)
  4. in.readObject()
  5. }

首先,通过ByteBuffer类型的bytes构造ByteBufferInputStream类型的bis;

其次,调用deserializeStream()方法,获得反序列化输入流in;

最后,通过反序列化输入流in的readObject()方法获得对象。

经历了上述过程,RDD、ShuffleDependency或者RDD、FUNC就不难获取到了。

先发表出来,余下的一些细节,或者没有讲到的部分,未完待续吧!

博客原地址:http://blog.csdn.net/lipeng_bigdata/article/details/50752101

Spark源码分析之八:Task运行(二)的更多相关文章

  1. spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析

    spark 源码分析之八--Spark RPC剖析之TransportContext和TransportClientFactory剖析 TransportContext 首先官方文档对Transpor ...

  2. spark 源码分析之十二 -- Spark内置RPC机制剖析之八Spark RPC总结

    在spark 源码分析之五 -- Spark内置RPC机制剖析之一创建NettyRpcEnv中,剖析了NettyRpcEnv的创建过程. Dispatcher.NettyStreamManager.T ...

  3. Spark 源码分析系列

    如下,是 spark 源码分析系列的一些文章汇总,持续更新中...... Spark RPC spark 源码分析之五--Spark RPC剖析之创建NettyRpcEnv spark 源码分析之六- ...

  4. Spark源码分析之七:Task运行(一)

    在Task调度相关的两篇文章<Spark源码分析之五:Task调度(一)>与<Spark源码分析之六:Task调度(二)>中,我们大致了解了Task调度相关的主要逻辑,并且在T ...

  5. spark 源码分析之二十一 -- Task的执行流程

    引言 在上两篇文章 spark 源码分析之十九 -- DAG的生成和Stage的划分 和 spark 源码分析之二十 -- Stage的提交 中剖析了Spark的DAG的生成,Stage的划分以及St ...

  6. Spark源码分析之六:Task调度(二)

    话说在<Spark源码分析之五:Task调度(一)>一文中,我们对Task调度分析到了DriverEndpoint的makeOffers()方法.这个方法针对接收到的ReviveOffer ...

  7. Spark源码分析之二:Job的调度模型与运行反馈

    在<Spark源码分析之Job提交运行总流程概述>一文中,我们提到了,Job提交与运行的第一阶段Stage划分与提交,可以分为三个阶段: 1.Job的调度模型与运行反馈: 2.Stage划 ...

  8. spark 源码分析之二十二-- Task的内存管理

    问题的提出 本篇文章将回答如下问题: 1.  spark任务在执行的时候,其内存是如何管理的? 2. 堆内内存的寻址是如何设计的?是如何避免由于JVM的GC的存在引起的内存地址变化的?其内部的内存缓存 ...

  9. Spark源码分析之五:Task调度(一)

    在前四篇博文中,我们分析了Job提交运行总流程的第一阶段Stage划分与提交,它又被细化为三个分阶段: 1.Job的调度模型与运行反馈: 2.Stage划分: 3.Stage提交:对应TaskSet的 ...

随机推荐

  1. 【SQL Server】修改DB逻辑文件名称

    步骤一:查询当前DB逻辑文件名称(主逻辑文件.日志逻辑文件) ; 步骤二:步骤二改变(还原)DB逻辑文件名称 RESTORE DATABASE AW831 FROM DISK='D:\AW831.DA ...

  2. angular-关于分页

    列表渲染数据量庞大的时候,我们需要用到一个filter来控制我们的列表进行循环渲染. 这就要用到一个filter,limitTo. 在此,我使用了变量来进行控制,可以随时调换每页的数量,并且配合分页按 ...

  3. 利用linux信号机制调试段错误(Segment fault)【转】

    转自:http://blog.csdn.net/ab198604/article/details/6164517 版权声明:本文为博主原创文章,未经博主允许不得转载. 在实际开发过程中,大家可能会遇到 ...

  4. Vim查找替换及正则表达式的使用

    原文地址:http://tanqisen.github.io/blog/2013/01/13/vim-search-replace-regex/ 简单替换表达式 :[range]s/from/to/[ ...

  5. 用Wireshark分析Socket连接建立的过程

    0. 安装Wireshark,但是默认情况下,Wireshark无法捕获127.0.0.1的报文 解决方案:安装npcap,替换默认的winpacp,重新启动Wireshark,就可以看到一个名字中含 ...

  6. NewCode

    1.[数论]给你N,求不大于N的最大完全平方数. #include<bits/stdc++.h> #define FOR(i,a,b) for(int i=(a),_b=(b);i< ...

  7. python调用phantomjs组件(windows和linux)

    phantomjs在windows和linux系统,可以通selenium的webdriver直接调用,所以只要将phantomjs程序加载到python程序目录下. 示例代码如下所示: #建立Pha ...

  8. 10.1综合强化刷题 Day2

    a[问题描述]你是能看到第一题的 friends呢.                                                —— hja世界上没有什么比卖的这 贵弹丸三还令人绝 ...

  9. POJ 3710 Christmas Game [博弈]

    题意:略. 思路:这是个删边的博弈游戏. 关于删边游戏的预备知识:http://blog.csdn.net/acm_cxlove/article/details/7854532 学习完预备知识后,这一 ...

  10. CSS 布局整理(************************************************)

    1.css垂直水平居中 效果: HTML代码: <div id="container"> <div id="center-div">&l ...