Spark运行原理及任务调度源码解析(基于Spark3.0)

Spark运行模式

  1. 可以在本地多线程运行
  2. 伪分布式运行
  3. Yarn运行模式
  4. Mesos运行模式

 基本概念

1.application

也就是API用户编写的程序,这个程序是分为两部分执行的,一部分是放在Driver端执行的,用于驱动整个程序运行的逻辑,还有一部分是放在各个节点上,让集群协同计算的部分

2.Driver

Driver在Spark中是用户定义在main方法中,驱动整个Spark程序运行创建SparkContext,为Spark的运行创建环境,负责计算资源的申请,销毁,任务的监控分配

3.Executor

运行在worker节点上的一个进程,可以创建多个线程,运行多个Task,系那个中间结果存在本地磁盘或者内存中,运行的时候从线程池中找一个空闲线程运行task,并行度取决去CPU的逻辑核心数

4.ClusterManger,也就是调度资源的服务,Yarn,standalone,Mesos等

5.Task

Excutor运行的最小单元,由Driver端生成,通过序列化发送到Worker,反序列化之后运行,一个Job可以呗划分为多个阶段,Task的数量取决于父RDD的分区数量,一个JOB可以有多个stage,每个stage可以有多个分区,每个分区对应一个Task,Task由TaskScheduler负责

6.JOB

通过Action触发,一个APP中可以由多个job,运行完一个之后可以再启动一个job,通常我们写的都是一个

7.Stage

也就是任务划分的阶段,一个JOB被划分成很多的Task,Stage有ShuffleMapStage(非最终stage),和ResultStage(结果结点),Stage的切分就在shuffle发生的地方,每次shuffle都会将任务一刀两断

8.RDD

Spark最核心的东西,是对分区有序数据的抽象,里面记录的是描述信息(哪个文件,这些文件其实是分布在不同机器上的,序列化的,不可变,容错弹性的),RDD就是这些文件的抽象集合,让我们操作的时候不用关心怎么整合各个分区的文件之类的繁琐

9.共享变量

1.广播变量,也就是每个结点都有一个

2.累加变量,多个结点共同操作的一个变量

10,宽依赖,

宽依赖是Shuffle的根源,

11窄依赖,

窄依赖不需要shuffle,因为他只有一个父依赖,直接一个task就可以完成了,不需要划分另外的阶段,也不需要shuffle

12.DAGscheduler

DAG是根据stage和RDD之间的依赖关系构建出来的执行顺序,DAGSheduler通过DAG调度任务(stage)执行顺序

13TaskScheduler

将TaskSet提交给Worker运行,每个Worker运行什么Task通过他进行分配

任务调度原理

任务调度最重要的两个部分,即DAGSheduler和TaskSheduler,负责将用户提交的任务生成执行关系(DAG),划分为不同的阶段,提交到集群中进行最终的计算,整个过程如下图:

Spark运行原理及任务调度源码解析(基于Spark3.0)_第1张图片

DAGScheduler和TaskSche中传输的是TaskSet,TackSche和Worker中间传输的是Task

1.通过用户的操作,创建DAG,生成DAG后发送给DAGScheduler,TaskShed这里对任务进行切分,切分成不同的stage,将每个stage中的TaskSet发送给TaskScher

2.TaskShe通过ClusterManager发送Task给Worker结点的Executor,Executor将开启线程进行计算执行,并且保存任务到本地或者回传到Driver端

TaskScheduler

为SparkContext调度任务,从不同的DAGscheduler(不同的任务),接收不同的Stage向集群提交任务,备份任务,他会调用schedulerBackend(在新任务提交的时候,任务执行失败,计算结点挂掉了,执行过慢重新分配任务)

schedulerBackend

对Exechutor分配任务Task,并且在Executor上执行Task

有多个TaskSheduler,也有多个schedilerbackend,他们是一对一的关系,并且都被SparkContext持有

调度总览

Spark运行原理及任务调度源码解析(基于Spark3.0)_第2张图片

DAGScheduler实现

DAGScheduler主要负责对应用进行划分阶段,为什么划分阶段?应为程序是放在集群中运行的,一个阶段依赖于另一个阶段的结果,并且执行是有先后顺序的,所以要划分

划分出来的stage是放在集群中运行的,其中每个stage有多个task,每个task的逻辑都是一样的,只是对应的分区是不相同,

这多个task被分布在不同的机器上并发执行.

不同的资源调度框架,yarn,或者是mesos,local等生成的DAG是完全一样的,可以跨平台运行保证结果

DAGScheduler随着sparkContext的创建而创建,

RunJob函数,SparkContext类的这个方法是所有程序运行的入口

/**
*在RDD中的给定分区集上运行函数,并将结果传递给给定的
*处理程序函数。这是Spark中所有操作的主要入口点。
*
*运行任务的@param rdd target rdd
*@param func在RDD的每个分区上运行的函数
*@param partitions要在其上运行的分区集;某些作业可能不希望在所有分区上进行计算
*目标RDD的分区,例如用于“first()之类的操作`
*@param resultHandler  回调函数
**/
  def runJob[T, U: ClassTag](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      resultHandler: (Int, U) => Unit): Unit = {
    if (stopped.get()) {
      throw new IllegalStateException("SparkContext has been shutdown")
    }
    val callSite = getCallSite
    val cleanedFunc = clean(func)
    logInfo("Starting job: " + callSite.shortForm)
    if (conf.getBoolean("spark.logLineage", false)) {
      logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
    }
    dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
    progressBar.foreach(_.finishAll())
    rdd.doCheckpoint()
  }

DAGScheduler的创建

private[spark] class DAGScheduler(
    private[scheduler] val sc: SparkContext,
    private[scheduler] val taskScheduler: TaskScheduler,
    listenerBus: LiveListenerBus,
    mapOutputTracker: MapOutputTrackerMaster,
    blockManagerMaster: BlockManagerMaster,
    env: SparkEnv,
    clock: Clock = new SystemClock())

MapOutputTrackerMaster,是运行在Driver端管理ShuffleMapTask的输出的,下游可以通过他获取shuffle的输出位置

BlockManagerMaster,管理整个job的block信息

还会初始化一个Actor通信,用来接收外部的信息,提供服务0

 

Job的提交

以RDD.count方法为例

def count(): Long = sc.runJob(this, Utils.getIteratorSize _).sum

调用了Runjob

/**
*在RDD中的所有分区上运行一个作业,并在数组中返回结果。
*
*运行任务的@param rdd target rdd
*@param func在RDD的每个分区上运行的函数
*@return in memory collection和作业的结果(每个collection元素将包含
*一个分区的结果)
*/
  def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {
    runJob(rdd, func, 0 until rdd.partitions.length)
  }

Runjob调用了

DAGScheduler.runjob
/**
*在RDD中的给定分区集上运行函数,并将结果传递给给定的
*处理程序函数。这是Spark中所有操作的主要入口点。
*
*运行任务的@param rdd target rdd
*@param func在RDD的每个分区上运行的函数
*@param partitions要在其上运行的分区集;某些作业可能不希望在所有分区上进行计算
*目标RDD的分区,例如用于“first()之类的操作`
*@param resultHandler回调以将每个结果传递给
*/
  def runJob[T, U](
      rdd: RDD[T],
      func: (TaskContext, Iterator[T]) => U,
      partitions: Seq[Int],
      callSite: CallSite,
      resultHandler: (Int, U) => Unit,
      properties: Properties): Unit = {
    val start = System.nanoTime
    val waiter = submitJob(rdd, func, partitions, callSite, resultHandler, properties)
    ThreadUtils.awaitReady(waiter.completionFuture, Duration.Inf)
    waiter.completionFuture.value.get match {
      case scala.util.Success(_) =>
        logInfo("Job %d finished: %s, took %f s".format
          (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
      case scala.util.Failure(exception) =>
        logInfo("Job %d failed: %s, took %f s".format
          (waiter.jobId, callSite.shortForm, (System.nanoTime - start) / 1e9))
        // SPARK-8644: Include user stack trace in exceptions coming from DAGScheduler.
        val callerStackTrace = Thread.currentThread().getStackTrace.tail
        exception.setStackTrace(exception.getStackTrace ++ callerStackTrace)
        throw exception
    }
  }

其中最关键的 def submitJob[T, U]()的调用

/**
*在RDD中的给定分区集上运行函数,并将结果传递给给定的
*处理程序函数。这是Spark中所有操作的主要入口点。
*
*运行任务的@param rdd target rdd
*@param func在RDD的每个分区上运行的函数
*@param partitions要在其上运行的分区集;某些作业可能不希望在所有分区上进行计算
*目标RDD的分区,例如用于“first()之类的操作`
*@param resultHandler回调以将每个结果传递给
*/
  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.isEmpty) {
      val clonedProperties = Utils.cloneProperties(properties)
      if (sc.getLocalProperty(SparkContext.SPARK_JOB_DESCRIPTION) == null) {
        clonedProperties.setProperty(SparkContext.SPARK_JOB_DESCRIPTION, callSite.shortForm)
      }
      val time = clock.getTimeMillis()
      listenerBus.post(
        SparkListenerJobStart(jobId, time, Seq.empty, clonedProperties))
      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.nonEmpty)
    val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
启动一个Job等待者,将生成的jobid和分区数量,等作为参数,并且将这个时间放入事件池中,返回等待结果

    val waiter = new JobWaiter[U](this, jobId, partitions.size, resultHandler)
    eventProcessLoop.post(JobSubmitted(
      jobId, rdd, func2, partitions.toArray, callSite, waiter,
      Utils.cloneProperties(properties)))
    waiter
  }

启动一个Job等待者,将生成的jobid和分区数量,等作为参数,并且将这个时间放入事件池中,返回等待结果

后面就交给事件监听者处理了,

private[scheduler] class DAGSchedulerEventProcessLoop(dagScheduler: DAGScheduler)

这个类定义了很多的监听事件

  private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
    case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
      dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)

    case MapStageSubmitted(jobId, dependency, callSite, listener, properties) =>
      dagScheduler.handleMapStageSubmitted(jobId, dependency, callSite, listener, properties)

    case StageCancelled(stageId, reason) =>
      dagScheduler.handleStageCancellation(stageId, reason)

    case JobCancelled(jobId, reason) =>
      dagScheduler.handleJobCancellation(jobId, reason)

    case JobGroupCancelled(groupId) =>
      dagScheduler.handleJobGroupCancelled(groupId)

    case AllJobsCancelled =>
      dagScheduler.doCancelAllJobs()

    case ExecutorAdded(execId, host) =>
      dagScheduler.handleExecutorAdded(execId, host)

    case ExecutorLost(execId, reason) =>
      val workerLost = reason match {
        case SlaveLost(_, true) => true
        case _ => false
      }
      dagScheduler.handleExecutorLost(execId, workerLost)

    case WorkerRemoved(workerId, host, message) =>
      dagScheduler.handleWorkerRemoved(workerId, host, message)

    case BeginEvent(task, taskInfo) =>
      dagScheduler.handleBeginEvent(task, taskInfo)

    case SpeculativeTaskSubmitted(task) =>
      dagScheduler.handleSpeculativeTaskSubmitted(task)

    case GettingResultEvent(taskInfo) =>
      dagScheduler.handleGetTaskResult(taskInfo)

    case completion: CompletionEvent =>
      dagScheduler.handleTaskCompletion(completion)

    case TaskSetFailed(taskSet, reason, exception) =>
      dagScheduler.handleTaskSetFailed(taskSet, reason, exception)

    case ResubmitFailedStages =>
      dagScheduler.resubmitFailedStages()
  }

经过对前面事件的处理,放入到处理队列中等待结果的返回,前面的JobWaiter会监听Job的执行状态,

下面我们来详细看一下是怎样处理任务执行结果的

  private[scheduler] def handleJobSubmitted(jobId: Int,
      finalRDD: RDD[_],
      func: (TaskContext, Iterator[_]) => _,
      partitions: Array[Int],
      callSite: CallSite,
      listener: JobListener,
      properties: Properties): Unit = {
    var finalStage: ResultStage = null
    try {
     //如果执行的文件缺失了,会报错
      finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
    } catch {
//匹配
      case e: BarrierJobSlotsNumberCheckFailed =>
        // 如果jobid不存在了,就在Int:0的基础上+1
        val numCheckFailures = barrierJobIdToNumTasksCheckFailures.compute(jobId,
          (_: Int, value: Int) => value + 1)
            //如果失败次数小于最大失败次数,就开启一个线程,再次发送到处理队列
        if (numCheckFailures <= maxFailureNumTasksCheck) {
          messageScheduler.schedule(
            new Runnable {
              override def run(): Unit = eventProcessLoop.post(JobSubmitted(jobId, finalRDD, func,
                partitions, callSite, listener, properties))
            },
            timeIntervalNumTasksCheck,
            TimeUnit.SECONDS
          )
          return
        } else {
          //否则就认定作业失败,清楚内部数据,发送失败消息
          barrierJobIdToNumTasksCheckFailures.remove(jobId)
          listener.jobFailed(e)
          return
        }
        //最大的错误兜底
      case e: Exception =>
        logWarning("Creating new stage failed due to exception - job: " + jobId, e)
        listener.jobFailed(e)
        return
    }
    // 再次清楚
    barrierJobIdToNumTasksCheckFailures.remove(jobId)

    val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
    clearCacheLocs()
    logInfo("Got job %s (%s) with %d output partitions".format(
      job.jobId, callSite.shortForm, partitions.length))
    logInfo("Final stage: " + finalStage + " (" + finalStage.name + ")")
    logInfo("Parents of final stage: " + finalStage.parents)
    logInfo("Missing parents: " + getMissingParentStages(finalStage))

    val jobSubmissionTime = clock.getTimeMillis()
    jobIdToActiveJob(jobId) = job
    activeJobs += job
    finalStage.setActiveJob(job)
    val stageIds = jobIdToStageIds(jobId).toArray
    val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
//发送具体的任务到任务消费者
    listenerBus.post(
      SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))
    submitStage(finalStage)
  }

这个方法会创建 createResultStage 然后创建  val job = new ActiveJob(jobId, finalStage, callSite, listener, properties) 通过listenerBus.post(...)提交资源

Stage的划分

为什么需要stage上面已经提到了这里再来详细的讨论一下

划分Stage的原因是由于任务类型的不一致,有的任务只依赖于一个父的RDD,所以可以和父RDD放到同一task中执行,构成一个阶段,但是有的任务(例如reduce,group)等,需要的是全局数据,依赖于多个分区的处理结果,所以只等等待前面的执行,这就有了先后顺序(Stage)

每个stage由多个task组成,这些task在任务逻辑上是一致的只是对应不同的分区,Partition的数量和同一任务逻辑的Task的数量是一致的,也就是说一个分区会被对应的stage的一个task处理,而多个Executor争抢,如果计算资源充足,一个Exe执行一个,不足会计算多个task

如果不知道什么是宽窄依赖,可以看前面的博客[Spark实战学习]

宽依赖是DAG的分界线,也是stage的分界线

stage的划分是从最后RDD一个开始的,也就是触发action的RDD

Spark运行原理及任务调度源码解析(基于Spark3.0)_第3张图片

 

handleJobSubmitted开始stage的划分,从G开始,G依赖于B,F,先处理B还是F是随机的,由于B是G的窄依赖,所以GB在同一个stage中,而对于F是G的宽依赖,中间存在Shuffle,只能在F执行完才能执行G,所以F比G早一个stage,AB之间也是宽依赖,所以B跟A不能是同一个阶段,A单独一个阶段,所以BG所在的stage3有两个父依赖阶段,

stage1和2是独立的,可以并发,但是他们对于stage3来说是要等待其执行结果的

实现源码

 

submitMapStage,调用了stage的创建
doOnReceive中定义了消息监听,来创建map阶段的stage
case MapStageSubmitted(jobId, dependency, callSite, listener, properties) =>
  dagScheduler.handleMapStageSubmitted(jobId, dependency, callSite, listener, properties)

 

  private[scheduler] def handleMapStageSubmitted(jobId: Int,
      dependency: ShuffleDependency[_, _, _],
      callSite: CallSite,
      listener: JobListener,
      properties: Properties): Unit = {
    // Submitting this map stage might still require the creation of some parent stages, so make
    // sure that happens.
    var finalStage: ShuffleMapStage = null
    try {
      // New stage creation may throw an exception if, for example, jobs are run on a
      // HadoopRDD whose underlying HDFS files have been deleted.
      finalStage = getOrCreateShuffleMapStage(dependency, jobId)
    } catch {
      case e: Exception =>
        logWarning("Creating new stage failed due to exception - job: " + jobId, e)
        listener.jobFailed(e)
        return
    }

    val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
    clearCacheLocs()
    logInfo("Got map stage job %s (%s) with %d output partitions".format(
      jobId, callSite.shortForm, dependency.rdd.partitions.length))
    logInfo("Final stage: " + finalStage + " (" + finalStage.name + ")")
    logInfo("Parents of final stage: " + finalStage.parents)
    logInfo("Missing parents: " + getMissingParentStages(finalStage))

    val jobSubmissionTime = clock.getTimeMillis()
    jobIdToActiveJob(jobId) = job
    activeJobs += job
    finalStage.addActiveJob(job)
    val stageIds = jobIdToStageIds(jobId).toArray
    val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
    listenerBus.post(
      SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))
    submitStage(finalStage)

    // If the whole stage has already finished, tell the listener and remove it
    if (finalStage.isAvailable) {
      markMapStageJobAsFinished(job, mapOutputTracker.getStatistics(dependency))
    }
  }
  /**
   * Create a ResultStage associated with the provided jobId.
   */
  private def createResultStage(
      rdd: RDD[_],
      func: (TaskContext, Iterator[_]) => _,
      partitions: Array[Int],
      jobId: Int,
      callSite: CallSite): ResultStage = {
    checkBarrierStageWithDynamicAllocation(rdd)
    checkBarrierStageWithNumSlots(rdd)
    checkBarrierStageWithRDDChainPattern(rdd, partitions.toSet.size)
    val parents = getOrCreateParentStages(rdd, jobId)
    val id = nextStageId.getAndIncrement()
    val stage = new ResultStage(id, rdd, func, partitions, parents, jobId, callSite)
    stageIdToStage(id) = stage
    updateJobIdStageIdMaps(jobId, stage)
    stage
  }

其中最重要的是

finalStage = getOrCreateShuffleMapStage(dependency, jobId)getOrCreateShuffleMapStage是个递归方法
/**
*获取shuffleIdToMapStage中存在的shuffle映射阶段。否则,如果
*shuffle map stage不存在,此方法将在中创建shuffle map stage
*除了任何丢失的祖先洗牌地图阶段。
*/
  private def getOrCreateShuffleMapStage(
      shuffleDep: ShuffleDependency[_, _, _],
      firstJobId: Int): ShuffleMapStage = {
    shuffleIdToMapStage.get(shuffleDep.shuffleId) match {
      case Some(stage) =>
        stage

      case None =>
      //为所有丢失的祖先洗牌依赖项创建阶段。
        getMissingAncestorShuffleDependencies(shuffleDep.rdd).foreach { dep =>
     //即使getMissingAncestorShuffleDependencies只返回shuffle依赖项
//在shuffleIdToMapStage中还没有出现,可能在我们
//在foreach循环中获取一个特定的依赖项,它被添加到
//shuffleIdToMapStage由早期依赖项的阶段创建过程执行。看到了吗
//SPARK-13902了解更多信息。
          if (!shuffleIdToMapStage.contains(dep.shuffleId)) {
            createShuffleMapStage(dep, firstJobId)
          }
        }
        // Finally, create a stage for the given shuffle dependency.
        createShuffleMapStage(shuffleDep, firstJobId)
    }
  }

查找shuffled的stage,如果没有就创建,所以

提交stage,将所有没有提交的parentstage提交运行,然后提交当前stage

  /** Submits stage, but first recursively submits any missing parents. */
  private def submitStage(stage: Stage): Unit = {
    val jobId = activeJobForStage(stage)
    if (jobId.isDefined) {
      logDebug(s"submitStage($stage (name=${stage.name};" +
        s"jobs=${stage.jobIds.toSeq.sorted.mkString(",")}))")
      if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
        val missing = getMissingParentStages(stage).sortBy(_.id)
        logDebug("missing: " + missing)
        if (missing.isEmpty) {
          logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
          submitMissingTasks(stage, jobId.get)
        } else {
          for (parent <- missing) {
            submitStage(parent)
          }
          waitingStages += stage
        }
      }
    } else {
      abortStage(stage, "No active job for stage " + stage.id, None)
    }
  }

只有父stage提交了才能提交,这里对顺序进行了控制,submitmissingTask,会将最后的工作提交

submitmissingTask里面进行了Task的创建

 

如果找不到父Stag说明是最开始的,也就是触发action的RDD

 /**查找尚未在shuffleToMapStage中注册的祖先shuffle依赖项*/

  private def getMissingAncestorShuffleDependencies(
      rdd: RDD[_]): ListBuffer[ShuffleDependency[_, _, _]] = {
    val ancestors = new ListBuffer[ShuffleDependency[_, _, _]]
//存祖先依赖
    val visited = new HashSet[RDD[_]]
//存RDD的set集合
    // We are manually maintaining a stack here to prevent StackOverflowError
    // caused by recursively visiting
    val waitingForVisit = new ListBuffer[RDD[_]]
    waitingForVisit += rdd
    while (waitingForVisit.nonEmpty) {
      val toVisit = waitingForVisit.remove(0)
      if (!visited(toVisit)) {
        visited += toVisit
        getShuffleDependencies(toVisit).foreach { shuffleDep =>
          if (!shuffleIdToMapStage.contains(shuffleDep.shuffleId)) {
            ancestors.prepend(shuffleDep)//往前添加依赖,从后往前
            waitingForVisit.prepend(shuffleDep.rdd)
          } // Otherwise, the dependency and its ancestors have already been registered.
        }
      }
    }
    ancestors
  }

 

getShuffleMapStage就是获取shuffle所依赖的stafe

MapOutPutTrancker保存了map阶段结果的元数据信息,后面的task通过元数据获取数据

到这里DAGScheduler就完成了使命,等待这TaskScheduler对任务进行提交,过程中DAG还是会接收到任务执行的消息

 

主要参考SparkContext和DAGScheduler

举例WordCount的执行流程

Task有两类,

一种是不产生结果的,shuffleMapTask用来生成中间结果的任务,可以通过设置分区控制数量

还有一种是产生结果的,也叫做ResultTask,可以通过设置调整结果的处理数量

Spark运行原理及任务调度源码解析(基于Spark3.0)_第4张图片

前三个RDD都是一个stage,依赖类型都是一对一的窄依赖,不用shuffle可以并行执行,直到reduceByKey() 

这个方法,会将各个分区的同一个key的结果汇总(HashPartitioner)到一个同一个分区的同一个excutor的同一个task里面进行汇总

 

Spark运行原理及任务调度源码解析(基于Spark3.0)_第5张图片

 前面设置了5个task,所以会在shuffleMapTask阶段生成5个任务并行化执行,但是在ResultMap阶段会运行三个,这三个会去拉取block块,然后继续执行,将shuffleMapTask生成的结果,进行汇总,然后输出

 

Spark运行原理及任务调度源码解析(基于Spark3.0)_第6张图片

如何将用户逻辑转化为并行执行的任务?

如何实现移动运算优于移动数据?

如何实现shuffle?及数据传递到指定的任务?

如何分配计算资源?

 

 

 

留坑

 

你可能感兴趣的:(Spark,spark,大数据)