Spark Stage划分和Task提交源码清晰总结版(看不懂请举报作者)

Spark的Action算子会触发job的执行,job执行流程中的数据依赖关系是以Stage为单位的,同一Job里的Stage可以并行,但是一般如果有依赖则是串行。所有Action算子都会执行SparkContext的RunJob-》DagScheduler的Runjob->DagScheduler的submitJob()接下来进行源码分析

SubmitJob方法

def submitJob[T, U](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      callSite: CallSite,
      resultHandler: (Int, U) => Unit,
      properties: Properties): JobWaiter[U] = {
    // Check to make sure we are not launching a task on a partition that does not exist.
    val maxPartitions = rdd.partitions.length
    partitions.find(p => p >= maxPartitions || p < 0).foreach { p =>
      throw new IllegalArgumentException(
        "Attempting to access a non-existent partition: " + p + ". " +
          "Total number of partitions: " + maxPartitions)
    }

    val jobId = nextJobId.getAndIncrement()
    if (partitions.size == 0) {
      val time = clock.getTimeMillis()
      listenerBus.post(
        SparkListenerJobStart(jobId, time, Seq[StageInfo](), properties))
      listenerBus.post(
        SparkListenerJobEnd(jobId, time, JobSucceeded))
      // Return immediately if the job is running 0 tasks
      return new JobWaiter[U](this, jobId, 0, resultHandler)
    }

    assert(partitions.size > 0)
    val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
    // JobWaiter是一个阻塞线程,它等待job完成后j将结果交给resultHandler
    val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
    /* eventProcessLoop是DAGScheduler的事件队列
    因为可能集群同时运行着多个Job,而DAGSchduler默认是FIFO先进先出的资源调度
    这里传入的事件类型为JobSubmitted,post方法会将该JobSubmitted事件put到一个LinkedBlockingQueue当中
   队列的size设置为Integer.MAX_VALUE,如果处理线程处理不及时那么肯定会OOM。
    通过这个eventLoop的设计将job提交由同步改成异步。进入了双端阻塞队列以后会由一个监听器线程不停监听这个队列
    监听到了后就调用onReceive(event)它的底层实现是doOnReceive。
    doOnReceive方法监听到了case为JobSubmitted方法后就调用handleJobSubmitted方法
    */
    eventProcessLoop.post(JobSubmitted(
      jobId, rdd, func2, partitions.toArray, callSite, waiter,
      SerializationUtils.clone(properties)))
    waiter
  }

讲到这里理解到一下RDD的惰性计算特性,在这里所有的方法传递的RDD 都是Final RDD.也就是说我们触发Action算子是通过传递FinalRDD来进行一系列的操作的.

handleJobSubmitted方法做了两件事

1.finalRDD来创建final Stage, 具体是先获得这个final stage的所有父stage, 

获得父stage的过程比较复杂

在这里建议先阅读一下Dependency的源码,了解到ShuffleDependency和NarrowDependenCy,然后每个dependency会对应一个RDD, RDD又会对应一个Seq[DependenCy]序列

先讲getShuffleDependencies根据finalRDD获取他的dependencies, 但是这个方法的源码说过 只会获取最近的一个shuffle dependency, 所以还能返回一个HashSet[ShuffleDependenCy]的原因是CoGroupedRDD这种会有多个,这样父依赖对应的RDD 都会入栈, 再通过他们去获得ShuffleDependenCy这样便获得了一个HashSet, 获取了HashSet后再回到上图中的代码,针对HashSet当中的每个ShuffleDependency去执行getOrCreateShuffleMapStage方法,这个时候会从start到final的宽依赖顺序进行创建stage,此时ResultStage之前的所有stage都已经划分好并添加进shuffleIdToMapStage,回到createResultStagegetOrCreateParentStages方法获得了ResultStage的父ShuffleMapStage从而创建ResultStage.

2.定位到handleJobSubmitted方法的submit(finalStage)方法里。

这个方法会递归先提交它的父stage(为啥?惰性是惰性,但是顺序没变啊,得先找根源的呀),提交父stage的意思是提交task,也就是要定位到submitMissingTasks方法中来.

submitMissingTasks做了4件事情

1.获取Task的最佳计算位置

2.序列化Task的Binary,并进行广播。Executor端在执行task时会向反序列化Task。

3. 根据stage的不同类型创建,为stage的每个分区创建创建task,并封装成TaskSet。(ShuffleMapTask、ResultTask)

4.调用TaskScheduler的submitTasks,提交TaskSet

task最佳位置即数据本地化级别分为以下几种.

  • PROCESS_LOCAL: 数据在同一个 JVM 中,即同一个 executor 上。这是最佳数据 locality。
  • NODE_LOCAL: 数据在同一个节点(worker)上。比如数据在同一个节点的另一个 executor上;或在 HDFS 上,恰好有 block 在同一个节点上。速度比 PROCESS_LOCAL 稍慢,因为数据需要在不同进程之间传递或从文件中读取
  • NO_PREF: 数据从哪里访问都一样快,不需要位置优先
  • RACK_LOCAL: 数据在同一机架的不同节点上。需要通过网络传输数据及文件 IO,比 NODE_LOCAL 慢
  • ANY: 数据在非同一机架的网络上,速度最慢.

task最佳位置的计算是通过DAGScheduler.getPreferredLocsInternal方法来操作的 源码已经做了清晰的解释如下:

private def getPreferredLocsInternal(
      rdd: RDD[_],
      partition: Int,
      visited: HashSet[(RDD[_], Int)]): Seq[TaskLocation] = {
    // If the partition has already been visited, no need to re-visit.
    // This avoids exponential path exploration.  SPARK-695
    if (!visited.add((rdd, partition))) {
      // Nil has already been returned for previously visited partitions.
      return Nil
    }
    // If the partition is cached, return the cache locations
    val cached = getCacheLocs(rdd)(partition)
    if (cached.nonEmpty) {
      return cached
    }
    // If the RDD has some placement preferences (as is the case for input RDDs), get those
    val rddPrefs = rdd.preferredLocations(rdd.partitions(partition)).toList
    if (rddPrefs.nonEmpty) {
      return rddPrefs.map(TaskLocation(_))
    }

    // If the RDD has narrow dependencies, pick the first partition of the first narrow dependency
    // that has any placement preferences. Ideally we would choose based on transfer sizes,
    // but this will do for now.
    rdd.dependencies.foreach {
      case n: NarrowDependency[_] =>
        for (inPart <- n.getParents(partition)) {
          val locs = getPreferredLocsInternal(n.rdd, inPart, visited)
          if (locs != Nil) {
            return locs
          }
        }

      case _ =>
    }

    Nil
  }

通过上述代码获取了任务计算最佳位置。 然后回到上面说的submissingtask方法来 看到第四步调用TaskScheduler的submitTasks,提交TaskSet。

这个submitTasks做了几件事

为每一个taskset创建一个tasksetManager用于管理TaskSet,包括任务推断,Task本地性,并对Task的资源进行分配,如果task提交失败它也会帮忙重新提交,这里重点了解task资源分配, 具体在submitTasks方法里调用backend.reviveOffers()。然后通过模式匹配调用makeoffers方法.

case ReviveOffers =>
        makeOffers()

makeoffer方法里做了几件事。

1、从executorDataMap当中过滤掉已死的executor,然后将executor封装成workoffers对象。

2、在这个方法的最后有行代码是这样的 launchTasks(scheduler.resourceOffers(workOffers)),所以我们只需要最后把resourceOffers看懂任务就完成了。

resourceOffers做了几件事

1、遍历workOffers来更新host和executor关系、host和机架的关系。

2.可用的executor进行shuffle分散,避免将task放在同一个worker上,进行负载均衡(这里还没看懂,源码就这么注释了一下 很懵逼)

3、在一个循环 里做了任务分配 也就是resourceOfferSingleTaskSet方法(也就是在executor当中启动task了).

[for (currentMaxLocality <- taskSet.myLocalityLevels)  ,   myLocalityLevels是一个ArrayBuffer依次顺序存储了本地化级别参数]

这里面有个cpu的相关配置cpus_per_task,也就是我们一个task占几个核, 默认是1核。 可以修改。

那么叽叽歪歪了半天对实践的应用有没有好处呢? 有, 了解了数据本地化后就可以查看自己的任务的数据本地化级别, 如果process_local很少的话就可以调节那个降级的等待时间来增加Process_local的task,调节完后对比总运行时间来进行调优。

task分配的算法理念叫做移动数据不如移动计算,就是Spark会尽可能的将计算任务分配到要计算的数据所处的存储位置。

参考文章:http://www.louisvv.com/archives/1836.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值