Spark ShuffleMap任务的生成、执行及数据跟踪流程

RDD抽象类的定义

我们知道Spark中的一个可以用户计算的数据集,被抽象成了一个RDD,如下是RDD的类定义(这里只保留了类中关键的成员变量):

abstract class RDD[T: ClassTag](
    @transient private var _sc: SparkContext,
    @transient private var deps: Seq[Dependency[_]]
  ) extends Serializable with Logging {
    /** A unique ID for this RDD (within its SparkContext). */
  val id: Int = sc.newRddId()

  /** A friendly name for this RDD */
  @transient var name: String = _
    // Our dependencies and partitions will be gotten by calling subclass's methods below, and will
  // be overwritten when we're checkpointed
  private var dependencies_ : Seq[Dependency[_]] = _
  @transient private var partitions_ : Array[Partition] = _

  private var storageLevel: StorageLevel = StorageLevel.NONE

  private[spark] var checkpointData: Option[RDDCheckpointData[T]] = None

  private val checkpointAllMarkedAncestors =
 Option(sc.getLocalProperty(RDD.CHECKPOINT_ALL_MARKED_ANCESTORS)).exists(_.toBoolean)

  // From performance concern, cache the value to avoid repeatedly compute `isBarrier()` on a long
  // RDD chain.
  @transient protected lazy val isBarrier_ : Boolean =
    dependencies.filter(!_.isInstanceOf[ShuffleDependency[_, _, _]]).exists(_.rdd.isBarrier())

  private[spark] final lazy val outputDeterministicLevel: DeterministicLevel.Value = {
    if (isReliablyCheckpointed) {
      DeterministicLevel.DETERMINATE
    } else {
      getOutputDeterministicLevel
    }
  }
}

从上面的代码可以看到RDD在序列化时,仅仅会序列化id、dependencies_、storageLevel、checkpointAllMarkedAncestors、outputDeterministicLevel这些成员变量,当然还包括RD实现类的其它可以被序列化的成员变量。

ShuffledRDD创建

当我们通过RDD API调用带有Shuffle动作的方法时,就会生成这样的实例,partitionBy(…)方法的定义如下:

  /**
   * Return a copy of the RDD partitioned using the specified partitioner.
   */
  def partitionBy(partitioner: Partitioner): RDD[(K, V)] = self.withScope {
    if (keyClass.isArray && partitioner.isInstanceOf[HashPartitioner]) {
      throw new SparkException("HashPartitioner cannot partition array keys.")
    }
    if (self.partitioner == Some(partitioner)) {
      self
    } else {
      new ShuffledRDD[K, V, V](self, partitioner)
    }
  }

ShuffledRDD是整个RDD依赖图(一个有向图)中的一个结点,因此在下面的类定义中,可以看到ShuffledRDD会有一个成员变量prev,它就是前驱RDD的引用,同时注意这个变量是@transient类型的,因此它不能被序列化。
Spark会根据RDD依赖图,生成一系列的任务,对于ShuffledRDD来说,就会对应一个ShuffleMapTask(后面篇章详细说明),通常需要跨结点进行数据交换。
为了在创建任务时,方便解析RDD的依赖关系,RDD抽象类提供了getDenpendencies方法,用于生成RDD的依赖关系类ShuffleDependency,这个关系除了记录当前RDD以及前驱RDD,还记录了当前RDD的数据分区信息part、数据序列化器serializer、数据排序信息keyOrdering、聚合函数aggregator、合并函数mapSideCombine。

class ShuffledRDD[K: ClassTag, V: ClassTag, C: ClassTag](
    @transient var prev: RDD[_ <: Product2[K, V]],
    part: Partitioner)
  extends RDD[(K, C)](prev.context, Nil) {
  override def getDependencies: Seq[Dependency[_]] = {
    val serializer = userSpecifiedSerializer.getOrElse {
      val serializerManager = SparkEnv.get.serializerManager
      if (mapSideCombine) {
        serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[ClassTag[C]])
      } else {
        serializerManager.getSerializer(implicitly[ClassTag[K]], implicitly[ClassTag[V]])
      }
    }
    List(new ShuffleDependency(prev, part, serializer, keyOrdering, aggregator, mapSideCombine))
  }
}

ShuffleDependency创建

ShuffleDependency是对一个RDD及其直接父继RDD的关系的描述,下面的代码展示了它的定义。这里主要关心的是ShuffleHandle的生成以及shuffle数据清理逻辑的注册,细节见代码中的注释。

class ShuffleDependency[K: ClassTag, V: ClassTag, C: ClassTag](
    @transient private val _rdd: RDD[_ <: Product2[K, V]],
    val partitioner: Partitioner,
    val serializer: Serializer = SparkEnv.get.serializer,
    val keyOrdering: Option[Ordering[K]] = None,
    val aggregator: Option[Aggregator[K, V, C]] = None,
    val mapSideCombine: Boolean = false)
  extends Dependency[Product2[K, V]] {

  if (mapSideCombine) {
    require(aggregator.isDefined, "Map-side combine without Aggregator specified!")
  }
  override def rdd: RDD[Product2[K, V]] = _rdd.asInstanceOf[RDD[Product2[K, V]]]

  private[spark] val keyClassName: String = reflect.classTag[K].runtimeClass.getName
  private[spark] val valueClassName: String = reflect.classTag[V].runtimeClass.getName
  // Note: It's possible that the combiner class tag is null, if the combineByKey
  // methods in PairRDDFunctions are used instead of combineByKeyWithClassTag.
  private[spark] val combinerClassName: Option[String] =
    Option(reflect.classTag[C]).map(_.runtimeClass.getName)
  // 在创建ShuffleDependency实例时,会生成一个在当前用户环境下的唯一ID
  // 以便能够在其它地方通过这个id映射到此实例
  val shuffleId: Int = _rdd.context.newShuffleId()
  // 同时在实例化时,会向当前执行环境中的ShuffleManager中注册自己
  // 实际是返回一个适合的ShuffleHandle,以便在Map端完成数据处理工作时,决定如何
  // 缓存这些数据。
  // BypassMergeSortShuffleHandle:序列化但不排序数据,写出到文件
  // SerializedShuffleHandle:对应于UnsafeShuffleWriter,数据会被序列化,先缓存,如果走过内存则溢出到文件
  // BaseShuffleHandle:对应于SortShuffleWriter写出器,不序列化,先缓存,如果超过内存则溢出到文件
  val shuffleHandle: ShuffleHandle = _rdd.context.env.shuffleManager.registerShuffle(
    shuffleId, _rdd.partitions.length, this)
  // 在这里会注册清理方法,当这个ShuffleDependency对应的数据不再使用时,清理掉
  _rdd.sparkContext.cleaner.foreach(_.registerShuffleForCleanup(this))
}

ShuffleMapTask创建

当调用RDD的action行为的方法时,会触发DAGScheduler::handleJobSubmitted(…)方法的调用,然后遍历前面讲到的ShuffledRDD依赖图,创建任务ShuffleMapTask和FinalTask两种类型的任务,最后提交集群运行。提交任务方法的核心代码如下:

  private[scheduler] def handleJobSubmitted(jobId: Int,
      finalRDD: RDD[_],
      func: (TaskContext, Iterator[_]) => _,
      partitions: Array[Int],
      callSite: CallSite,
      listener: JobListener,
      properties: Properties) {
    var finalStage: ResultStage = null
    // New stage creation may throw an exception if, for example, jobs are run on a
    // HadoopRDD whose underlying HDFS files have been deleted.
    // 从最后一个RDD,递归生成Stage图,本人其它Spark文章已经有介绍,这里仅简单说明:
    // 这个方法会根据RDD的依赖图,即ShuffleDependency依赖图,生成一个
    // ShuffleMapStage的依赖图,ShuffleDependency依赖图中的每一个结点就是一个
    // ShuffleMapStage
    finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
    ...
    // 创建一个ActiveJob实例,也就是我们在WEBUI页面上看到的JOB,每一个
    // 依赖图的都会产生一个新的JOB。
    val job = new ActiveJob(jobId, finalStage, callSite, listener, properties)
    ...
    val jobSubmissionTime = clock.getTimeMillis()
    // 缓存ActiveJob信息
    jobIdToActiveJob(jobId) = job
    activeJobs += job
    // 设置finalStage关联的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))
    // 提交sage
    submitStage(finalStage)
  }
ShuffleMapStage的创建

createShuffleMapStage是在DAGScheduler中定义的方法用于从当前的ShuffleDenpendency实例递归地构建每一个ShuffleMapStage,同时会将它们注册到各个缓存数据结构中,以便在任务运行时交换状态及数据,其定义如下:

  // 可以看到每一个ShuffleDependency实例就对应一个ShuffleMapStage实例
  def createShuffleMapStage(shuffleDep: ShuffleDependency[_, _, _], jobId: Int): ShuffleMapStage = {
    val rdd = shuffleDep.rdd
    checkBarrierStageWithDynamicAllocation(rdd)
    checkBarrierStageWithNumSlots(rdd)
    checkBarrierStageWithRDDChainPattern(rdd, rdd.getNumPartitions)
    val numTasks = rdd.partitions.length
    // 递归创建父依赖(ShuffleMapStage)的实例
    val parents = getOrCreateParentStages(rdd, jobId)
    val id = nextStageId.getAndIncrement()
    val stage = new ShuffleMapStage(
      id, rdd, numTasks, parents, jobId, rdd.creationSite, shuffleDep, mapOutputTracker)
	// 缓存当前stage的映射信息
    stageIdToStage(id) = stage
    // 缓存父stage到当前stage的映射信息
    shuffleIdToMapStage(shuffleDep.shuffleId) = stage
    // 将当前stage添加到jobId对应的集合
    updateJobIdStageIdMaps(jobId, stage)

    if (!mapOutputTracker.containsShuffle(shuffleDep.shuffleId)) {
      logInfo("Registering RDD " + rdd.id + " (" + rdd.getCreationSite + ")")
      // 已经处理完当前stage所有父依赖,向MapOutputTracker中注册当前
      // shuffle stage,实际上就是缓存当前stage与ShuffleStatus实例的映射,
      // 而ShuffleStatus用来记录上游rdd即将输出的每一个partion的状态
      mapOutputTracker.registerShuffle(shuffleDep.shuffleId, rdd.partitions.length)
    }
    stage
  }
提交Stage图

在前面的过程中,DAGScheduler根据RDD依赖图,最终生成了Stage依赖,如果想要Stage依次执行,就需要将整个依赖图,转换成一个个有依赖关系的Task。而submitStage(stage: Stage)方法,就是从Stage依赖图的最后一个Stage,即FinalStage开始构建不同类型的任务,具体的方法定义如下:

  /** Submits stage, but first recursively submits any missing parents. */
  private def submitStage(stage: Stage) {
    val jobId = activeJobForStage(stage)
    if (jobId.isDefined) {
      logDebug("submitStage(" + stage + ")")
      // 如果当前stage还没有被执行,同时如果有父Stage且还没准备好,
      // 就递归地、尝试执行构建祖先任务,将提交执行。
      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")
          // 如果发现所有依赖的stage都已经完成了,那么就可以从当前stage开始跑任务啦
          submitMissingTasks(stage, jobId.get)
        } else {
          for (parent <- missing) {
            submitStage(parent)
          }
          waitingStages += stage
        }
      }
    } else {
      abortStage(stage, "No active job for stage " + stage.id, None)
    }
  }
从当前ShuffleStage构建任务

下面的方法,首先查找当前stage要计算的所有的Partitions信息,然后根据自己的类型分别创建ShuffleMapTask或是ResultTask,并填充任务实例所需要的各种信息,最后在有任务还没跑时,调用TaskScheduler::submitTasks(…)方法提交任务。

  /** Called when stage's parents are available and we can now do its task. */
  private def submitMissingTasks(stage: Stage, jobId: Int) {
    logDebug("submitMissingTasks(" + stage + ")")

    // First figure out the indexes of partition ids to compute.
    // 找到所有要计算的partition信息
    val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()

    // Use the scheduling pool, job group, description, etc. from an ActiveJob associated
    // with this Stage
    val properties = jobIdToActiveJob(jobId).properties

    runningStages += stage
    // SparkListenerStageSubmitted should be posted before testing whether tasks are
    // serializable. If tasks are not serializable, a SparkListenerStageCompleted event
    // will be posted, which should always come after a corresponding SparkListenerStageSubmitted
    // event.
    stage match {
      case s: ShuffleMapStage =>
        outputCommitCoordinator.stageStart(stage = s.id, maxPartitionId = s.numPartitions - 1)
      case s: ResultStage =>
        // 如果是
        outputCommitCoordinator.stageStart(
          stage = s.id, maxPartitionId = s.rdd.partitions.length - 1)
    }
    val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {
      stage match {
        case s: ShuffleMapStage =>
          partitionsToCompute.map { id => (id, getPreferredLocs(stage.rdd, id))}.toMap
        case s: ResultStage =>
          partitionsToCompute.map { id =>
            val p = s.partitions(id)
            (id, getPreferredLocs(stage.rdd, p))
          }.toMap
      }
    } catch {
      case NonFatal(e) =>
        stage.makeNewStageAttempt(partitionsToCompute.size)
        listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))
        abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage
        return
    }

    stage.makeNewStageAttempt(partitionsToCompute.size, taskIdToLocations.values.toSeq)

    // If there are tasks to execute, record the submission time of the stage. Otherwise,
    // post the even without the submission time, which indicates that this stage was
    // skipped.
    if (partitionsToCompute.nonEmpty) {
      stage.latestInfo.submissionTime = Some(clock.getTimeMillis())
    }
    listenerBus.post(SparkListenerStageSubmitted(stage.latestInfo, properties))

    // TODO: Maybe we can keep the taskBinary in Stage to avoid serializing it multiple times.
    // Broadcasted binary for the task, used to dispatch tasks to executors. Note that we broadcast
    // the serialized copy of the RDD and for each task we will deserialize it, which means each
    // task gets a different copy of the RDD. This provides stronger isolation between tasks that
    // might modify state of objects referenced in their closures. This is necessary in Hadoop
    // where the JobConf/Configuration object is not thread-safe.
    var taskBinary: Broadcast[Array[Byte]] = null
    var partitions: Array[Partition] = null
    try {
      // For ShuffleMapTask, serialize and broadcast (rdd, shuffleDep).
      // For ResultTask, serialize and broadcast (rdd, func).
      var taskBinaryBytes: Array[Byte] = null
      // taskBinaryBytes and partitions are both effected by the checkpoint status. We need
      // this synchronization in case another concurrent job is checkpointing this RDD, so we get a
      // consistent view of both variables.
      RDDCheckpointData.synchronized {
        taskBinaryBytes = stage match {
          case stage: ShuffleMapStage =>
            JavaUtils.bufferToArray(
              closureSerializer.serialize((stage.rdd, stage.shuffleDep): AnyRef))
          case stage: ResultStage =>
            JavaUtils.bufferToArray(closureSerializer.serialize((stage.rdd, stage.func): AnyRef))
        }

        partitions = stage.rdd.partitions
      }
	  // 序列化任务图,包含当前stage.rdd信息以及依赖关系信息stage.shuffleDep
      taskBinary = sc.broadcast(taskBinaryBytes)
    } catch {
      // In the case of a failure during serialization, abort the stage.
      case e: NotSerializableException =>
        abortStage(stage, "Task not serializable: " + e.toString, Some(e))
        runningStages -= stage

        // Abort execution
        return
      case e: Throwable =>
        abortStage(stage, s"Task serialization failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage

        // Abort execution
        return
    }
    // 创建任务
    val tasks: Seq[Task[_]] = try {
      val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()
      stage match {
        // 如果是ShuffleMapStage类型的任务,则创建ShuffleMapTask
        // 对每一个partition,都创建一个任务,因此可以看到ShuffleMapTask会带有
        // stage.id:当前任务属于哪个stage
        // stage.lastInfo.attemptNumber:任务的重试次数
        // taskBinary:任务执行时,需要执行在parition数据集上需要进行的操作
        // part:当前任务要在哪个Partition数据集上的运行
        // locs:任务要读取的parition数据的位置,可以在本地,可能在远端
        // ...
        case stage: ShuffleMapStage =>
          stage.pendingPartitions.clear()
          partitionsToCompute.map { id =>
            val locs = taskIdToLocations(id)
            val part = partitions(id)
            stage.pendingPartitions += id
            new ShuffleMapTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, properties, serializedTaskMetrics, Option(jobId),
              Option(sc.applicationId), sc.applicationAttemptId, stage.rdd.isBarrier())
          }
	    // 创建ResultTask类型的任务
        case stage: ResultStage =>
          partitionsToCompute.map { id =>
            val p: Int = stage.partitions(id)
            val part = partitions(p)
            val locs = taskIdToLocations(id)
            new ResultTask(stage.id, stage.latestInfo.attemptNumber,
              taskBinary, part, locs, id, properties, serializedTaskMetrics,
              Option(jobId), Option(sc.applicationId), sc.applicationAttemptId,
              stage.rdd.isBarrier())
          }
      }
    } catch {
      case NonFatal(e) =>
        abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
        runningStages -= stage
        return
    }

    if (tasks.size > 0) {
      logInfo(s"Submitting ${tasks.size} missing tasks from $stage (${stage.rdd}) (first 15 " +
        s"tasks are for partitions ${tasks.take(15).map(_.partitionId)})")
      // 当前Stage所依赖的所有Paritions,都会生成对应的ShuffleMapTask或是ResultTask,
      // 这些任务又被组织成了TaskSet类,它表示了一组可以并行 计算的任务集合,至于如何分配这些任务
      //到哪些Worker上执行,以及如何管理状态等,都会交给TaskSetManager去完成。
      taskScheduler.submitTasks(new TaskSet(
        tasks.toArray, stage.id, stage.latestInfo.attemptNumber, jobId, properties))
    } else {
      // Because we posted SparkListenerStageSubmitted earlier, we should mark
      // the stage as completed here in case there are no tasks to run
      markStageAsFinished(stage, None)

      stage match {
        case stage: ShuffleMapStage =>
          logDebug(s"Stage ${stage} is actually done; " +
              s"(available: ${stage.isAvailable}," +
              s"available outputs: ${stage.numAvailableOutputs}," +
              s"partitions: ${stage.numPartitions})")
          markMapStageJobsAsFinished(stage)
        case stage : ResultStage =>
          logDebug(s"Stage ${stage} is actually done; (partitions: ${stage.numPartitions})")
      }
      submitWaitingChildStages(stage)
    }
  }

ShuffleMapTask的执行

前面已经讲到,在Driver端,DAGScheduler会遍历RDD依赖图,最终生成一组组的TaskSet,并将由TaskSetManager进行管理,最终每一个ShuffleMapTask会给分配到空闲的Worker上运行,而任务的运行过程定义在ShuffleMapTask中,代码如下:

class ShuffleMapTask {
  override def runTask(context: TaskContext): MapStatus = {
    // Deserialize the RDD using the broadcast variable.
    val threadMXBean = ManagementFactory.getThreadMXBean
    val deserializeStartTime = System.currentTimeMillis()
    val deserializeStartCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
      threadMXBean.getCurrentThreadCpuTime
    } else 0L
    val ser = SparkEnv.get.closureSerializer.newInstance()
    // 从任务携带的taskBinary广播变量中,反序列化出当前任务对应的RDD,以及依赖信息
    val (rdd, dep) = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](
      ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)

    _executorDeserializeTime = System.currentTimeMillis() - deserializeStartTime
    _executorDeserializeCpuTime = if (threadMXBean.isCurrentThreadCpuTimeSupported) {
      threadMXBean.getCurrentThreadCpuTime - deserializeStartCpuTime
    } else 0L

    var writer: ShuffleWriter[Any, Any] = null
    try {
      val manager = SparkEnv.get.shuffleManager
      // 根据当前任务指定的shuffle处理器类型,返回一个可用的、绑定到partitionId的writer,
      writer = manager.getWriter[Any, Any](dep.shuffleHandle, partitionId, context)
      // 通过rdd的迭代iterator操作,处理绑定的parition数据,此RDD可能包含了多个流水线操作,
      // 例如有依赖如下(其中<--表示右边依赖左边):
      // ShuffledRDD <-- MapParitionsRDD <-- MapParitionsRDD <-- current_rdd
      // 因此rdd.iterator在迭代时,会以递归的形式,从最左边的ShuffledRDD开始,通过
      // ShuffleManager远程读取数据,然后再今次执行后面的两个MapParitionsRDD上绑定的处理函数,
      // 最终由当前RDD,即ShuffledRDD类型,根据dep.shuffleHandle类型,将处理后的Parition
      // 数据输出到指定位置。
      writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]])
      writer.stop(success = true).get
    } catch {
      case e: Exception =>
        try {
          if (writer != null) {
            writer.stop(success = false)
          }
        } catch {
          case e: Exception =>
            log.debug("Could not stop writer", e)
        }
        throw e
    }
  }
}

Shuffle数据合并及写出

BypassMergeSortShuffleWriter

ShuffleMapTask数据处理完成后,如果它的数据处理方法是BypassMergeSortShuffleHandle类型的,那么就会使用此Writer写出Shuffle数据,并将写出位置注册到当前环境的BlockManager中。

此Writer写出数据时,会首先为每一个Partition都创建一个单独的文件,并开启一个文件流;然后顺序将每一个Parition数据直接写出到文件;最后将所有单独的parition文件拼接(concatenate)成一个文件,并写出索引文件。

可以看到这个写出器并不会Map端进行数据的合并、排序等,同时不会缓存数据在内存中,因此性能比较底,会在下面的场景中使用:

  1. 没有指定排序键
  2. 没有指定聚合函数
  3. partitions的数量小于spark.shuffle.sort.bypassMergeThreshold指定的值
UnsafeShuffleWriter

ShuffleMapTask数据处理完成后,如果它的数据处理方法是SerializedShuffleHandle类型的,那么就会使用此Writer先尝试在内存中保存所有的记录,(实际上内部通过ShuffleExternalSorter维护数据的缓存与写出),如果当前任务完成或是内存要溢出时,会触发写数据到文件的动作。
此Writer会在触发写文件时,先在内存中将所有的记录按ParitionID排序;然后将数据序列化后写出到一个单独的文件中(如果有内存溢出发生时,可能会写出多个溢出文件,即是将内存中的数据序列化后写出到另外一个溢出文件);最后所有的数据序列化后的溢出文件会被合并为一个文件。

SortShuffleWriter

ShuffleMapTask数据处理完成后,如果它的数据处理方法是BaseShuffleHandle类型的,那么就会使用此Writer,其基本的数据溢出逻辑与UnsafeShuffleWriter相似,不同的是它内部会创建ExternalSorter实例,并绑定Combine方法,在必要的情况下先在内存中对数据进行聚合。

We repeatedly fill up buffers of in-memory data, using either a PartitionedAppendOnlyMap if we want to combine by key, or a PartitionedPairBuffer if we don’t.
Map中的数据是PartitionID作为key。

同样在处理完成所有的数据后,会将所有可能的溢出文件与内存中的数据按ParitionID进行合并,并执行聚合操作,最终写出到一个shuffle文件;当然也会产生一个索引文件。

ShuffleMapTask任务的更新

当一个ShuffleMapTask任务完成后,会生成一个状态实例MapStatus,记录了这个任务的产生的数据信息。
随后 Executor就会map status转发给driver端,driver收到executor发来的任务完成消息时后,再会将这个消息转发给真正的任务调度器TaskSchedulerImpl,同时回收executor的上分配的资源,但并不会立即移除它。
TaskSchedulerImpl收到任务完成的消息后,会更新内部的状态器(就是TaskSetManager),同时判断是不是这个任务所属的TaskSet中的任务都执行完成了,如果都完成了,那么就触发调度后置的TaskSet执行。

private[spark] class TaskSetManager(
    sched: TaskSchedulerImpl,
    val taskSet: TaskSet,
    val maxTaskFailures: Int,
    blacklistTracker: Option[BlacklistTracker] = None,
    clock: Clock = new SystemClock()) extends Schedulable with Logging {

  /**
   * Marks a task as successful and notifies the DAGScheduler that the task has ended.
   */
  def handleSuccessfulTask(tid: Long, result: DirectTaskResult[_]): Unit = {
    val info = taskInfos(tid)
    val index = info.index
    // Check if any other attempt succeeded before this and this attempt has not been handled
    if (successful(index) && killedByOtherAttempt.contains(tid)) {
      // Undo the effect on calculatedTasks and totalResultSize made earlier when
      // checking if can fetch more results
      calculatedTasks -= 1
      val resultSizeAcc = result.accumUpdates.find(a =>
        a.name == Some(InternalAccumulator.RESULT_SIZE))
      if (resultSizeAcc.isDefined) {
        totalResultSize -= resultSizeAcc.get.asInstanceOf[LongAccumulator].value
      }

      // Handle this task as a killed task
      handleFailedTask(tid, TaskState.KILLED,
        TaskKilled("Finish but did not commit due to another attempt succeeded"))
      return
    }

    info.markFinished(TaskState.FINISHED, clock.getTimeMillis())
    if (speculationEnabled) {
      successfulTaskDurations.insert(info.duration)
    }
    // 从运行队列中移除这个任务ID
    removeRunningTask(tid)

    // Kill any other attempts for the same task (since those are unnecessary now that one
    // attempt completed successfully).
    for (attemptInfo <- taskAttempts(index) if attemptInfo.running) {
      logInfo(s"Killing attempt ${attemptInfo.attemptNumber} for task ${attemptInfo.id} " +
        s"in stage ${taskSet.id} (TID ${attemptInfo.taskId}) on ${attemptInfo.host} " +
        s"as the attempt ${info.attemptNumber} succeeded on ${info.host}")
      killedByOtherAttempt += attemptInfo.taskId
      sched.backend.killTask(
        attemptInfo.taskId,
        attemptInfo.executorId,
        interruptThread = true,
        reason = "another attempt succeeded")
    }
    if (!successful(index)) {
      tasksSuccessful += 1
      logInfo(s"Finished task ${info.id} in stage ${taskSet.id} (TID ${info.taskId}) in" +
        s" ${info.duration} ms on ${info.host} (executor ${info.executorId})" +
        s" ($tasksSuccessful/$numTasks)")
      // Mark successful and stop if all the tasks have succeeded.
      successful(index) = true
      if (tasksSuccessful == numTasks) {
        isZombie = true
      }
    } else {
      logInfo("Ignoring task-finished event for " + info.id + " in stage " + taskSet.id +
        " because task " + index + " has already completed successfully")
    }
    // There may be multiple tasksets for this stage -- we let all of them know that the partition
    // was completed.  This may result in some of the tasksets getting completed.
    // 如果当前任务的完成,意味着当前的Partition数据被成功处理了,但由于有失败重试机制,同一个
    // TaskSet集合可能存在多个历史实例。如果当前TaskSet中的某任务失败了,而其它任务在之前的失败
    // 的TaskSet中已经被标记成功过了,那么当前的TaskSet只需要重跑之前已经失败或还没有跑过的任务
    // 即可,避免不必要的重复工作。因此这里需要根据这个完成任务所属的Stage,更新这个Stage所有
    // 可能的TaskSets。
    sched.markPartitionCompletedInAllTaskSets(stageId, tasks(index).partitionId, info)
    // This method is called by "TaskSchedulerImpl.handleSuccessfulTask" which holds the
    // "TaskSchedulerImpl" lock until exiting. To avoid the SPARK-7655 issue, we should not
    // "deserialize" the value when holding a lock to avoid blocking other threads. So we call
    // "result.value()" in "TaskResultGetter.enqueueSuccessfulTask" before reaching here.
    // Note: "result.value()" only deserializes the value when it's called at the first time, so
    // here "result.value()" just returns the value and won't block other threads.
    // 通知DAGScheduler,这个任务已经结束了,有可能成功,也有可能失败
    sched.dagScheduler.taskEnded(tasks(index), Success, result.value(), result.accumUpdates, info)
    // 如果当前的TaskSetManager实例,被标记为Zombie,同时属于这个Stage或是TaskSet的任务都已经完成了,
    // 则清理这个manager,并将其从调度池中移除
    maybeFinishTaskSet()
  }
}

TaskSet完成自己管理 的任务状态更新后,就将完成的消息转发给DAGScheduler,以便scheduler根据当前任务所属的Stage的状态,尝试调度后继Stage的运行或是重跑当前Stage。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())
  extends Logging {

  private[scheduler] def handleTaskCompletion(event: CompletionEvent) {
  	...
  	// 获取到完成任务对应的Stage
  	val stage = stageIdToStage(task.stageId)
	...
	stage match {
		// 如果任务是一个ShuffleMapTask任务
		case smt: ShuffleMapTask =>
			// 则将对应的stage转换为ShuffleMapStage
            val shuffleStage = stage.asInstanceOf[ShuffleMapStage]
            if (!shuffleStage.isAvailable) {
                // Some tasks had failed; let's resubmit this shuffleStage.
                // TODO: Lower-level scheduler should also deal with this
                logInfo("Resubmitting " + shuffleStage + " (" + shuffleStage.name +
                  ") because some of its tasks had failed: " +
                  shuffleStage.findMissingPartitions().mkString(", "))
                submitStage(shuffleStage)
            } else {
                // 标记这个Stage的状态的执行成功
                markMapStageJobsAsFinished(shuffleStage)
                // 提交后置Stage执行
                submitWaitingChildStages(shuffleStage)
            }
      ...
    }
  }
}

Shuffle数据的读取

SortShuffleManager

一个ShuffleMapTask任务在依赖图时,即ShuffleDependency时,就会将自己注册到ShuffleManager上,是便DAGScheduler在调度时,能够跟踪一个JOB所有Stage的状态信息,进而选择合适的时机、合适的Stage进行调度执行。

Shuffle数据读取

从ShuffleManager构建上游Stage包含的某个Partition所使用的Shuffle数据写出器,通过Spark中实现的Iterator实例,从当前任务RDD上绑定的函数开始执行,递归调用可能的上游RDD上的处理函数执行,直到ShuffledRDD。
而ShuffledRDD的工作并不是直接完成计算了,而是从本地或是远程的BlockManager上拉取当前任务所需的Partition的数据。
当整个数据都被处理完成后,当前的ShuffleMapTask又会将新产生的数据通过自己的ShuffleWriter写出到合适的位置,最终完成整个任务的执行。

      var writer: ShuffleWriter[Any, Any] = null
      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]]])
      writer.stop(success = true).get

总结

从RDD依赖图到任务执行的流程简化如下(其中<-- 表示右边依赖左边的完成):

调用RDD或DataFrame API,生成一个RDD依赖图,例如df.read().filter(…).partitionBy(…).write(),
将生成ShuffledRDD <-- MapPartitionsRDD <-- ShuffledRDD <-- ShuffledRDD

通过DAGScheduler,生成依赖图:
ShuffleDependency <-- ShuffleDependency <-- ShuffleDependency

通过DAGScheduler,生成Stage依赖图
ShuffleMapStage <-- ShuffleMapStage <-- ResultStage

通过DAGScheduler,生成任务集TaskSet
ShuffleMapTask_1 <-- ShuffleMapTask_2 <-- ResultTask

通过TaskSetManager管理任务,将任务发送到Worker端执行

在Worker上执行ShuffleMapTask_1

在Worker上执行ShuffleMapTask_2

在Driver上执行ResultTask

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spark任务的详细执行流程如下: 1. 创建SparkContext:首先,Spark应用程序需要创建一个SparkContext对象,它是与集群通信的主要入口点。 2. 创建RDD:在Spark中,数据被组织成弹性分布式数据集(RDD)。RDD可以从外部存储系统(如HDFS)中读取数据,也可以通过对已有RDD进行转换操作来创建。 3. 转换操作:Spark提供了一系列转换操作,如map、filter、reduce等。这些操作可以对RDD进行转换,生成新的RDD。转换操作是惰性求值的,即不会立即执行,而是记录下来以便后续执行。 4. 行动操作:当需要从RDD中获取结果时,需要执行行动操作。行动操作会触发Spark作业的执行,并将结果返回给驱动程序。 5. 任务划分:Spark将作业划分为一系列任务,每个任务处理RDD的一个分区。任务划分是根据数据的分区情况和可用的计算资源进行的。 6. 任务调度:Spark任务调度到集群中的可用计算节点上执行任务调度器负责将任务分配给可用的Executor,并监控任务执行情况。 7. 任务执行:每个Executor会为分配给它的任务创建一个或多个线程,并在这些线程上执行任务任务执行过程中,Executor会将数据从内存或磁盘中读取到计算节点上,并进行计算操作。 8. 数据传输:在任务执行过程中,Spark会根据需要将数据从一个节点传输到另一个节点。这种数据传输可以是节点内的数据传输,也可以是跨节点的数据传输。 9. 结果返回:当任务执行完成后,结果会返回给驱动程序。驱动程序可以将结果保存到外部存储系统,或者进行进一步的处理和分析。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值