(spark源码)job生成和提交

摘要

spark中, Job的提交可以分为四个阶段

  1. RDD生成相互间的依赖关系. 实际上依赖关系是RDD的固有属性, 也就是说, 每个RDD生成时, 依赖就已经被生成了
  2. DVG过程, 将Action算子对应的RDD作为finalStage, 由后往前递归的遍历所有RDD, 并根据宽窄依赖划分Stage
  3. 根据partition数量, 生成对应数量的Task
  4. Worker调用executor执行运算逻辑

此次整理了job提交相关的源码, 源码中完整体现了四个阶段的运行逻辑

runjob系列方法

spark是惰性计算的, Action算子提交时, 会生成Job, 并真正触发之前的Transformation算子
源码解析以foreach方法生成Job的流程为例

/***** 查看foreach方法 => RDD.scala, 916行 *****/
def foreach(f: T => Unit): Unit = withScope {
// clean方法用于将方法清理为闭包.
// 因为方法中可能包含全局变量, 或者调用了其它类的方法
// 为了分发函数, 需要将相关的类序列化. clean在spark中是很常使用的方法
  val cleanF = sc.clean(f)
// this就是调用foreach方法的rdd对象
// rdd对象调用方法, 方法的内部实现是sc提交本对象和相关参数. 这样的转换在spark中也十分常见
  sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}

/***** 查看runJob方法 => SparkContext.scala, 2086行 *****/
// 跳转到了sc. Job相关的任务, 实际执行都要依靠sc
// 继续调用runJob方法. 生成长度等于分区数的数组, 并作为参数传入
def runJob[T, U: ClassTag](rdd: RDD[T], func: Iterator[T] => U): Array[U] = {
  runJob(rdd, func, 0 until rdd.partitions.length)
}
  
/***** 查看runJob方法 => SparkContext.scala, 2057行 *****/
def runJob[T, U: ClassTag](
  rdd: RDD[T],
  func: Iterator[T] => U,
  partitions: Seq[Int]): Array[U] = {
// 其实此次示例的流程中, func已经被clean过了. 
val cleanedFunc = clean(func)
// func的参数中, 多了TaskContext, 方便之后的分发
runJob(rdd, (ctx: TaskContext, it: Iterator[T]) => cleanedFunc(it), partitions)
}

/***** 查看runJob方法 => SparkContext.scala, 2038行 *****/
// 在RDD的给定分区上运行函数, 并将结果作为数组返回
// 如果函数针对不同分区有不同的逻辑, 就需要在TaskContext中设定
def runJob[T, U: ClassTag](
    rdd: RDD[T],
    func: (TaskContext, Iterator[T]) => U,
    partitions: Seq[Int]): Array[U] = {
  val results = new Array[U](partitions.size)
  //### 通过一个匿名函数, 将数组results传了出去, 从而实现了数组的初始化 ###//
  runJob[T, U](rdd, func, partitions, (index, res) => results(index) = res)
  results
}

/***** 查看runJob方法 => SparkContext.scala, 2008行 *****/
// Spark所有行为的主要入口
def runJob[T, U: ClassTag](
    rdd: RDD[T],
    func: (TaskContext, Iterator[T]) => U,
    partitions: Seq[Int],
    // 这个函数内部有一个数组, 该函数运行的结果就是数组完成了初始化, 所以返回的是Unit
    resultHandler: (Int, U) => Unit): Unit = {
  // 如果任务已经停止, 就抛出异常
  if (stopped.get()) {
    throw new IllegalStateException("SparkContext has been shutdown")
  }
  // getCallSite方法会返回当前用户的callSite, callSite记录着请求发起的位置, 也用于记录日志
  val callSite = getCallSite
  // 日常clean
  val cleanedFunc = clean(func)
  // 记录日志
  logInfo("Starting job: " + callSite.shortForm)
  if (conf.getBoolean("spark.logLineage", false)) {
    logInfo("RDD's recursive dependencies:\n" + rdd.toDebugString)
  }
  //### 该方法的核心是以下代码 ###//
  // dagScheduler就是DAG调度器, 我们继续追runJob方法
  dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get)
  // 将所有stage标记为已结束, 清空UI界面的进度条
  progressBar.foreach(_.finishAll())
  // 进行CheckPoint
  rdd.doCheckpoint()
}

/***** 查看runJob方法 => DAGScheduler.scala, 612行 *****/
// 在给定的RDD上运行Actor算子,并在结果到达时将所有结果传递给resultHandler函数。
// (我们知道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
  //##### 核心方法, 提交Job到任务队列(后面的代码用于记录日志, 跳转即可) =>
  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))
      // 日志中会包括堆栈跟踪.
      val callerStackTrace = Thread.currentThread().getStackTrace.tail
      exception.setStackTrace(exception.getStackTrace ++ callerStackTrace)
      throw exception
  }
}

job提交(概览)

/***** 查看submitJob => DAGScheduler.scala, 568行 *****/
// 规范化各参数, 准备提交任务
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] = {
  // partitions是用于记录分区角标的数组, 下述代码检查了partitions是否合法
  // 此次代码流程中, partitions的创建逻辑是`0 until rdd.partitions.length`
  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)
  }

  // 生成jobId
  val jobId = nextJobId.getAndIncrement()
  if (partitions.size == 0) {
    // 如果分区数为0, 以为着没有运算进行, 所以立刻返回
    return new JobWaiter[U](this, jobId, 0, resultHandler)
  }

  // 断言, 分区数必须大于0 
  assert(partitions.size > 0)
  // 强制类型转换, 清除泛型以匹配post方法的参数
  val func2 = func.asInstanceOf[(TaskContext, Iterator[_]) => _]
  // JobWaiter类通过内部通讯机制, 提交运算请求, 并等待接收运算结果
  //### partitions.size对应的参数是totalTasks, 也就是分区数决定Task数 ###//
  val waiter = new JobWaiter(this, jobId, partitions.size, resultHandler)
  // JobSubmitted用于描述将要运行的job(包含所有Job信息)
  // eventProcessLoop内部开启了一个循环进行的线程
  // 该线程的run方法会不断读取任务队列中的event(事件), 并调用onReceive方法处理该event.
  // post方法就是用于将event事件放入了任务队列, 跳转=>
  eventProcessLoop.post(JobSubmitted(
    jobId, rdd, func2, partitions.toArray, callSite, waiter,
    SerializationUtils.clone(properties)))
  waiter
}

job提交(细节)

代码到现在, waiter, 也就是Listener, 已经开启并在等待结果
所以job才能真正被提交, 并标记为active
这个过程的核心就是eventProcessLoop对象

/***** 查看eventProcessLoop => DAGScheduler.scala, 200行 *****/
// DAGScheduler类生成对象的时候, eventProcessLoop就被new出来了
private[scheduler] val eventProcessLoop = new DAGSchedulerEventProcessLoop(this)

/***** 查看DAGSchedulerEventProcessLoop => DAGScheduler.scala, 1658行 *****/
private[scheduler] class DAGSchedulerEventProcessLoop(dagScheduler: DAGScheduler)
  // post方法是继承自EventLoop类, 线程的具体实现也在父类
  // 简而言之, 调用post方法就会延迟调用onReceive方法, 我们关注后者即可
  extends EventLoop[DAGSchedulerEvent]("dag-scheduler-event-loop") with Logging {

//### DAG调度程序的主事件循环 ###//
  override def onReceive(event: DAGSchedulerEvent): Unit = {
    val timerContext = timer.time()
    try {
      // 调用了doOnReceive方法
      doOnReceive(event)
    } finally {
      timerContext.stop()
    }
  }

  private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
    // 模式匹配, 匹配的到JobSubmitted
    case JobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties) =>
      // 终于真正处理Job了. 跳转=>
      dagScheduler.handleJobSubmitted(jobId, rdd, func, partitions, callSite, listener, properties)
      
//### 这里省略了其余case代码, 还省略了该类中非关键的方法和属性 ###//
  }
}

/***** handleJobSubmitted方法 => DAGScheduler.scala, 839行 *****/
private[scheduler] def handleJobSubmitted(jobId: Int,
    // 这个rdd就是Action算子对应的rdd
    finalRDD: RDD[_],
    // 运算逻辑, 借助TaskContext可以传递更为复杂的逻辑
    func: (TaskContext, Iterator[_]) => _,
    // 分区
    partitions: Array[Int],
    // 发起请求的位置
    callSite: CallSite,
    // 就是JobWaiter, 等待接收运算结果
    listener: JobListener,
    // 配置文件
    properties: Properties) {
  var finalStage: ResultStage = null
  try {
    // 生成新stage时可能会抛出异常, 比如RDD对应的hdfs上的文件被删除了
    // 这段代码, 将Actor算子对应的RDD封装为finalStage
    finalStage = createResultStage(finalRDD, func, partitions, jobId, callSite)
  } catch {
    case e: Exception =>
      logWarning("Creating new stage failed due to exception - job: " + jobId, e)
      listener.jobFailed(e)
      return
  }
// 终于真正生成了ActiveJob
  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是个HashMap, 这句代码将id存储为active键, 并记录对应的job
  jobIdToActiveJob(jobId) = job
  // 将job记录为active
  activeJobs += job
  finalStage.setActiveJob(job)
  // 完善下stage相关信息
  val stageIds = jobIdToStageIds(jobId).toArray
  val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))
  // 提前开启Listener, 等待接收结果
  listenerBus.post(
    SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))
  //##### 核心方法, 跳转 => 
  submitStage(finalStage)
}

Stage生成

通过handleJobSubmittedgetMissingParentStages两个方法, 来将此次job分割为更细粒度的stage, 也就是DAG过程的核心
handleJobSubmittedgetMissingParentStages两个方法都有递归, getMissingParentStages的递归会在回溯到宽依赖时停止, handleJobSubmitted从宽依赖处切分job, 从而生成stage, 然后继续递归回溯, 从而完成对整个job的处理
简而言之, stage的划分是从action算子对应的rdd开始的, 然后通过递归, 不断回溯, 直至完成job中rdd的遍历处理

/***** handleJobSubmitted方法 => DAGScheduler.scala, 921行 *****/
// 该方法用于划分stage, 参数中传入的是finalStage
private def submitStage(stage: Stage) {
  val jobId = activeJobForStage(stage)
  // if确认jobId存在
  if (jobId.isDefined) {
    // 先把finalStage记录到日志上
    logDebug("submitStage(" + stage + ")")
    // waitingStages等方法实际上是去对应的HashSet上查看stage是否存在, 以此来确认stage是未经处理的
    if (!waitingStages(stage) && !runningStages(stage) && !failedStages(stage)) {
      // getMissingParentStages方法生成Stage集合. 跳转 =>
      // <= 对宽依赖的父rdd的依赖分析, 由此处的递归来实现
      val missing = getMissingParentStages(stage).sortBy(_.id)
      logDebug("missing: " + missing)
      // 没有宽依赖时, 终止递归
      if (missing.isEmpty) {
        logInfo("Submitting " + stage + " (" + stage.rdd + "), which has no missing parents")
        //### 作为递归的终止条件, 该方法实际上会提交所有stage ###//
        submitMissingTasks(stage, jobId.get)
      } else {
        for (parent <- missing) {
          // 递归
          submitStage(parent)
        }
        // 将stage标记为等待(放入waitingStages集合, 对应上面的if条件判断语句)
        waitingStages += stage
      }
    } 
  } else {
    abortStage(stage, "No active job for stage " + stage.id, None)
  }
}
// 至此, stage划分完成, 且全部进入waitingStages等待运行
// 运行的结果会被Listener收集


/***** getMissingParentStages方法 => DAGScheduler.scala, 442行 *****/
private def getMissingParentStages(stage: Stage): List[Stage] = {
  val missing = new HashSet[Stage]
  val visited = new HashSet[RDD[_]]
  // 使用Stack, 以避免递归可能导致的栈溢出
  val waitingForVisit = new Stack[RDD[_]]
  def visit(rdd: RDD[_]) {
    // !visited(rdd)就是visited集合中不存在rdd
    if (!visited(rdd)) {
      // 如果不存在, 就像先添加进来
      visited += rdd
      // 查看rdd的高速缓存集合是否为空
      val rddHasUncachedPartitions = getCacheLocs(rdd).contains(Nil)
      // 如果没缓存
      if (rddHasUncachedPartitions) {
        // 遍历该rdd的依赖
        for (dep <- rdd.dependencies) {
          dep match {
            // 如果是宽依赖(可能发生shuffle的依赖)
            case shufDep: ShuffleDependency[_, _, _] =>
              // 生成Stage
              val mapStage = getOrCreateShuffleMapStage(shufDep, stage.firstJobId)
              if (!mapStage.isAvailable) {
                // 将生成的stage存入missing集合
                missing += mapStage
              }
            // 如果是窄依赖
            case narrowDep: NarrowDependency[_] =>
              // 等会儿追溯(递归)
              // 注意到rdd和其父rdd, 如果是窄依赖, 就会递归的找父rdd的父rdd
              // 但是如果是宽依赖, 就不递归了
              // 对宽依赖的父rdd当然也需要前溯, 这个递归是由上一个方法(handleJobSubmitted)来完成的
              waitingForVisit.push(narrowDep.rdd)
          }
        }
      }
    }
  }
  waitingForVisit.push(stage.rdd)
  // 如果还有RDD没被访问过的, 就递归
  while (waitingForVisit.nonEmpty) {
    visit(waitingForVisit.pop())
  }
  // 返回MissStage的集合, 我们也返回去继续看代码 =>
  missing.toList
}

Task生成

在这个方法中, 每个Stage生成了相应数量的Task
至此, job流程中主要的代码逻辑列举完毕

/***** submitMissingTasks方法 => DAGScheduler.scala, 944行 *****/
private def submitMissingTasks(stage: Stage, jobId: Int) {
  // 将要操作的stage记录到日志
  logDebug("submitMissingTasks(" + stage + ")")

  // 计算出要计算的分区id的索引
  val partitionsToCompute: Seq[Int] = stage.findMissingPartitions()

  // 加载配置文件
  val properties = jobIdToActiveJob(jobId).properties

  // 将stage标记为running
  runningStages += stage

  // case语句判断stage类型
  stage match {
    // 先判断ShuffleMapStage, 如果不是再进行后续判断
    case s: ShuffleMapStage =>
      outputCommitCoordinator.stageStart(stage = s.id, maxPartitionId = s.numPartitions - 1)
    // 实际上只有finalStage是ResultStage
    case s: ResultStage =>
      outputCommitCoordinator.stageStart(
        stage = s.id, maxPartitionId = s.rdd.partitions.length - 1)
  }
  
  // 中间代码省略, 跳转 => 1021行
  val tasks: Seq[Task[_]] = try {
    val serializedTaskMetrics = closureSerializer.serialize(stage.latestInfo.taskMetrics).array()
    stage match {
      case stage: ShuffleMapStage =>
        stage.pendingPartitions.clear()
        partitionsToCompute.map { id =>
          val locs = taskIdToLocations(id)
          // part就是task的数量, 由rdd的分区数决定
          val part = stage.rdd.partitions(id)
          stage.pendingPartitions += id
          // 根据最佳位置算法, 生成Task
          // 最佳位置算法其实说白了,就是从stage的最后一个RDD开始,
          // 由后向前, 寻找已完成缓存的分区(cache或checkpoint). 
          // task最佳位置, 就是已完成缓存的分区位置. 
          // 因为这样的话, task就可以在那个节点上执行, 而不需要计算之前的RDD了。
          new ShuffleMapTask(stage.id, stage.latestInfo.attemptId,
            taskBinary, part, locs, properties, serializedTaskMetrics, Option(jobId),
            Option(sc.applicationId), sc.applicationAttemptId)
        }

      case stage: ResultStage =>
        partitionsToCompute.map { id =>
          val p: Int = stage.partitions(id)
          // 两个case都是相同的part逻辑
          val part = stage.rdd.partitions(p)
          val locs = taskIdToLocations(id)
          // 和上面的case类似, 找最佳位置
          new ResultTask(stage.id, stage.latestInfo.attemptId,
            taskBinary, part, locs, id, properties, serializedTaskMetrics,
            Option(jobId), Option(sc.applicationId), sc.applicationAttemptId)
        }
    }
  } catch {
    case NonFatal(e) =>
      abortStage(stage, s"Task creation failed: $e\n${Utils.exceptionString(e)}", Some(e))
      runningStages -= stage
      return
  }
  
// 之后还有一段健壮性判断, 也省略不写
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值