• Spark技术内幕: Task向Executor提交的源代码解析


    在上文《Spark技术内幕:Stage划分及提交源代码分析》中,我们分析了Stage的生成和提交。可是Stage的提交,仅仅是DAGScheduler完毕了对DAG的划分,生成了一个计算拓扑,即须要依照顺序计算的Stage,Stage中包括了能够以partition为单位并行计算的Task。我们并没有分析Stage中得Task是怎样生成而且终于提交到Executor中去的。

    这就是本文的主题。

    从org.apache.spark.scheduler.DAGScheduler#submitMissingTasks開始,分析Stage是怎样生成TaskSet的。

    假设一个Stage的全部的parent stage都已经计算完毕或者存在于cache中。那么他会调用submitMissingTasks来提交该Stage所包括的Tasks。

    org.apache.spark.scheduler.DAGScheduler#submitMissingTasks的计算流程例如以下:

    1. 首先得到RDD中须要计算的partition,对于Shuffle类型的stage。须要推断stage中是否缓存了该结果;对于Result类型的Final Stage。则推断计算Job中该partition是否已经计算完毕。
    2. 序列化task的binary。Executor能够通过广播变量得到它。每一个task执行的时候首先会反序列化。这样在不同的executor上执行的task是隔离的,不会相互影响。
    3. 为每一个须要计算的partition生成一个task:对于Shuffle类型依赖的Stage,生成ShuffleMapTask类型的task;对于Result类型的Stage,生成一个ResultTask类型的task
    4. 确保Task是能够被序列化的。由于不同的cluster有不同的taskScheduler,在这里推断能够简化逻辑。保证TaskSet的task都是能够序列化的
    5. 通过TaskScheduler提交TaskSet。

    TaskSet就是能够做pipeline的一组全然同样的task,每一个task的处理逻辑全然同样。不同的是处理数据。每一个task负责处理一个partition。

    pipeline。能够称为大数据处理的基石。仅仅有数据进行pipeline处理,才干将其放到集群中去执行。

    对于一个task来说,它从数据源获得逻辑。然后依照拓扑顺序,顺序执行(实际上是调用rdd的compute)。

    TaskSet是一个数据结构,存储了这一组task:
    private[spark] class TaskSet(
        val tasks: Array[Task[_]],
        val stageId: Int,
        val attempt: Int,
        val priority: Int,
        val properties: Properties) {
        val id: String = stageId + "." + attempt
    
      override def toString: String = "TaskSet " + id
    }


    管理调度这个TaskSet的时org.apache.spark.scheduler.TaskSetManager。TaskSetManager会负责task的失败重试。跟踪每一个task的执行状态。处理locality-aware的调用。
    具体的调用堆栈例如以下:
    1. org.apache.spark.scheduler.TaskSchedulerImpl#submitTasks
    2. org.apache.spark.scheduler.SchedulableBuilder#addTaskSetManager
    3. org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend#reviveOffers
    4. org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend.DriverActor#makeOffers
    5. org.apache.spark.scheduler.TaskSchedulerImpl#resourceOffers
    6. org.apache.spark.scheduler.cluster.CoarseGrainedSchedulerBackend.DriverActor#launchTasks
    7. org.apache.spark.executor.CoarseGrainedExecutorBackend.receiveWithLogging#launchTask
    8. org.apache.spark.executor.Executor#launchTask

    首先看一下org.apache.spark.executor.Executor#launchTask:
      def launchTask(
          context: ExecutorBackend, taskId: Long, taskName: String, serializedTask: ByteBuffer) {
        val tr = new TaskRunner(context, taskId, taskName, serializedTask)
        runningTasks.put(taskId, tr)
        threadPool.execute(tr) // 開始在executor中执行
      }


    TaskRunner会从序列化的task中反序列化得到task。这个须要看 org.apache.spark.executor.Executor.TaskRunner#run 的实现:task.run(taskId.toInt)。而task.run的实现是:
     final def run(attemptId: Long): T = {
        context = new TaskContext(stageId, partitionId, attemptId, runningLocally = false)
        context.taskMetrics.hostname = Utils.localHostName()
        taskThread = Thread.currentThread()
        if (_killed) {
          kill(interruptThread = false)
        }
        runTask(context)
      }

    对于原来提到的两种Task,即
    1.  org.apache.spark.scheduler.ShuffleMapTask
    2.  org.apache.spark.scheduler.ResultTask
    分别实现了不同的runTask:
    org.apache.spark.scheduler.ResultTask#runTask即顺序调用rdd的compute,通过rdd的拓扑顺序依次对partition进行计算:
      override def runTask(context: TaskContext): U = {
        // Deserialize the RDD and the func using the broadcast variables.
        val ser = SparkEnv.get.closureSerializer.newInstance()
        val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](
          ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
    
        metrics = Some(context.taskMetrics)
        try {
          func(context, rdd.iterator(partition, context))
        } finally {
          context.markTaskCompleted()
        }
      }


    而org.apache.spark.scheduler.ShuffleMapTask#runTask则是写shuffle的结果。

      override def runTask(context: TaskContext): MapStatus = {
        // Deserialize the RDD using the broadcast variable.
        val ser = SparkEnv.get.closureSerializer.newInstance()
        val (rdd, dep) = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
          ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)
          //此处的taskBinary即为在org.apache.spark.scheduler.DAGScheduler#submitMissingTasks序列化的task的广播变量取得的
    
        metrics = Some(context.taskMetrics)
        var writer: ShuffleWriter[Any, Any] = null
        try {
          val manager = SparkEnv.get.shuffleManager
          writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
          writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]]) // 将rdd计算的结果写入memory或者disk
          return writer.stop(success = true).get
        } catch {
          case e: Exception =>
            if (writer != null) {
              writer.stop(success = false)
            }
            throw e
        } finally {
          context.markTaskCompleted()
        }
      }


    这两个task都不要依照拓扑顺序调用rdd的compute来完毕对partition的计算。不同的是ShuffleMapTask须要shuffle write。以供child stage读取shuffle的结果。

    对于这两个task都用到的taskBinary,即为在org.apache.spark.scheduler.DAGScheduler#submitMissingTasks序列化的task的广播变量取得的。


    通过上述几篇博文,实际上我们已经粗略的分析了从用户定义SparkContext開始。集群是假设为每一个Application分配Executor的,回想一下这个序列图:

    还有就是用户触发某个action,集群是怎样生成DAG,假设将DAG划分为能够成Stage,已经Stage是怎样将这些能够pipeline执行的task提交到Executor去执行的。当然了,具体细节还是很值得推敲的。

    以后的每一个周末。都会奉上某个细节的实现。

    歇息了。明天又会開始忙碌的一周。



  • 相关阅读:
    NodeMCU快速上云集锦
    云数据库 MySQL 8.0 重磅发布,更适合企业使用场景的RDS数据库
    MySQL 8.0 技术详解
    为更强大而生的开源关系型数据库来了!阿里云RDS for MySQL 8.0 正式上线!
    阿里云CDN技术掌舵人文景:相爱相杀一路狂奔的这十年
    容器服务kubernetes federation v2实践五:多集群流量调度
    Helm V3 新版本发布
    Serverless助力AI计算:阿里云ACK Serverless/ECI发布GPU容器实例
    详解TableStore模糊查询——以订单场景为例
    洛谷P2727 01串 Stringsobits
  • 原文地址:https://www.cnblogs.com/llguanli/p/8601055.html
Copyright © 2020-2023  润新知