Stage划分和Task最佳位置算法源码彻底解密(DT大数据梦工厂)

内容:

1、Job Stage划分算法解密;

2、Task最佳位置算法实现解密;

为什么要讲这两点:

1、Spark算子是链式的,计算首先Stage划分,划分好了之后才计算

2、Spark 追求最大化数据本地行,追求数据最大化的在内存中

==========Job Stage划分算法解密============

1、Spark Application中可以因为不同的Action触发众多的Job,也就是说一个Application中可以有很多Job,每个Job是由一个或者多个Stage构成的,后面的Stage依赖于前面的Stage,也就是说只有前面依赖的Stage计算完毕后,后面的Stage才会运行;

2、Stage划分的一句就是宽依赖,什么时候产生宽依赖?例如reduceByKey、groupByKey、saveAsTextFile等;

3、由Action(例如collect)导致了SparkContext的runJob的执行,最终导致了DAGScheduler中的submitJob执行,其核心是通过发送一个case class JobSubmitted给eventProcessLoop,其中JobSubmitted 源码如下;

/** A result-yielding job was submitted on a target RDD */
private[scheduler] case class JobSubmitted(
    jobId: Int,
    finalRDD: RDD[_],
    func: (TaskContextIterator[_]) => _,
    partitions: Array[Int],
    callSite: CallSite,
    listener: JobListener,
    properties: Properties = null)
  extends DAGSchedulerEvent

eventProcessLoop是DAGSchedulerEventProcesssLoop的具体实例,而DAGSchedulerEventProcesssLoop是EventLoop的子类,具体实现EventLoop的onReceive方法,onReceive方法转过来回调doOnReceive

private[scheduler] class DAGSchedulerEventProcessLoop(dagScheduler: DAGScheduler)
  extends EventLoop[DAGSchedulerEvent]("dag-scheduler-event-loop"with Logging {

  private[thisval timer = dagScheduler.metricsSource.messageProcessingTimer

  /**
   * The main event loop of the DAG scheduler.
   */
  override def onReceive(event: DAGSchedulerEvent): Unit = {
    val timerContext = timer.time()
    try {
      doOnReceive(event)
    } finally {
      timerContext.stop()
    }
  }


private def doOnReceive(event: DAGSchedulerEvent): Unit = event match {
  case JobSubmitted(jobIdrddfuncpartitionscallSitelistenerproperties) =>
    dagScheduler.handleJobSubmitted(jobIdrddfuncpartitionscallSitelistenerproperties)

  case MapStageSubmitted(jobIddependencycallSitelistenerproperties) =>
    dagScheduler.handleMapStageSubmitted(jobIddependencycallSitelistenerproperties)

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

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

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

  case AllJobsCancelled =>
    dagScheduler.doCancelAllJobs()

  case ExecutorAdded(execIdhost) =>
    dagScheduler.handleExecutorAdded(execIdhost)

  case ExecutorLost(execId) =>
    dagScheduler.handleExecutorLost(execIdfetchFailed = false)

  case BeginEvent(tasktaskInfo) =>
    dagScheduler.handleBeginEvent(tasktaskInfo)

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

  case completion @ CompletionEvent(taskreason__taskInfotaskMetrics) =>
    dagScheduler.handleTaskCompletion(completion)

  case TaskSetFailed(taskSetreasonexception) =>
    dagScheduler.handleTaskSetFailed(taskSetreasonexception)

  case ResubmitFailedStages =>
    dagScheduler.resubmitFailedStages()
}

为啥这里要给自己发一个消息呢?

主线程哪怕要调用自己的方法,那么也会给自己发一个消息,这样可以保证处理机制的一致,也容易扩展。

4、在doOnReceive中,通过模式匹配的方式把执行路由到

case JobSubmitted(jobIdrddfuncpartitionscallSitelistenerproperties) =>
  dagScheduler.handleJobSubmitted(jobIdrddfuncpartitionscallSitelistenerproperties)

5、在handleJobSubmitted中首先创建finalStage,创建finalStage时会建立父Stage的依赖链条;

private[scheduler] def handleJobSubmitted(jobId: Int,
    finalRDD: RDD[_],
    func: (TaskContextIterator[_]) => _,
    partitions: Array[Int],
    callSite: CallSite,
    listener: JobListener,
    properties: Properties) {
  var finalStage: ResultStage = 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 = newResultStage(finalRDDfuncpartitionsjobIdcallSite)
  } catch {
    case e: Exception =>
      logWarning("Creating new stage failed due to exception - job: " + jobIde)
      listener.jobFailed(e)
      return
  }

  val job = new ActiveJob(jobIdfinalStagecallSitelistenerproperties)
  clearCacheLocs()
  logInfo("Got job %s (%s) with %d output partitions".format(
    job.jobIdcallSite.shortFormpartitions.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.jobIdjobSubmissionTimestageInfosproperties))
  submitStage(finalStage)

  submitWaitingStages()
}

Missing其实就是没有父Stage的那些Tasks,一直调自己,从后往前回溯提交Stage

/** Submits stage, but first recursively submits any missing parents. */
private def submitStage(stage: Stage) {
  val jobId = activeJobForStage(stage)
  if (jobId.isDefined) {
    logDebug("submitStage(" + 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")
        submitMissingTasks(stagejobId.get)
      } else {
        for (parent <- missing) {
          submitStage(parent)
        }
        waitingStages += stage
      }
    }
  } else {
    abortStage(stage"No active job for stage " + stage.idNone)
  }
}

补充1:获得父Stage,广度优先算法(图论),每次碰到ShufDep就产生新的Stage,不是宽依赖的话,就和自己在同一个Stage,把自己当前依赖的rdd就push到waitingForVisit(栈)中

/**
 * Get or create the list of parent stages for a given RDD.  The new Stages will be created with
 * the provided firstJobId.
 */
private def getParentStages(rdd: RDD[_]firstJobId: Int): List[Stage] = {
  val parents = new HashSet[Stage]
  val visited = new HashSet[RDD[_]]
  // We are manually maintaining a stack here to prevent StackOverflowError
  // caused by recursively visiting
  val waitingForVisit = new Stack[RDD[_]]
  def visit(r: RDD[_]) {
    if (!visited(r)) {
      visited += r
      // Kind of ugly: need to register RDDs with the cache here since
      // we can't do it in its constructor because # of partitions is unknown
      for (dep <- r.dependencies) {
        dep match {
          case shufDep: ShuffleDependency[___] =>
            parents += getShuffleMapStage(shufDepfirstJobId)
          case _ =>
            waitingForVisit.push(dep.rdd)
        }
      }
    }
  }
  waitingForVisit.push(rdd)
  while (waitingForVisit.nonEmpty) {
    visit(waitingForVisit.pop())
  }
  parents.toList
}

spacer.gif

补充说明:所谓的Missing就是说要进行当前的计算!!!

==========Task最佳位置算法实现解密============

1、在submitMissingTasks中会通过调用一下代码来获得任务的本地性,

val taskIdToLocations: Map[Int, Seq[TaskLocation]] = try {
  stage match {
    case s: ShuffleMapStage =>
      partitionsToCompute.map { id => (idgetPreferredLocs(stage.rddid))}.toMap
    case s: ResultStage =>
      val job = s.activeJob.get
      partitionsToCompute.map { id =>
        val p = s.partitions(id)
        (idgetPreferredLocs(stage.rddp))
      }.toMap
  }
catch {
  case NonFatal(e) =>
    stage.makeNewStageAttempt(partitionsToCompute.size)
    listenerBus.post(SparkListenerStageSubmitted(stage.latestInfoproperties))
    abortStage(stages"Task creation failed: $e\n${e.getStackTraceString}"Some(e))
    runningStages -= stage
    return
}

// First figure out the indexes of partition ids to compute.
val partitionsToComputeSeq[Int] = stage.findMissingPartitions()

/** Returns the sequence of partition ids that are missing (i.e. needs to be computed). */
def findMissingPartitions(): Seq[Int]

2、具体一个partition中的数据本地性的算法实现位于下述代码:

private[spark]
def getPreferredLocs(rdd: RDD[_]partition: Int): Seq[TaskLocation] = {
  getPreferredLocsInternal(rddpartitionnew HashSet)
}

/**
 * Recursive implementation for getPreferredLocs.
 *
 * This method is thread-safe because it only accesses DAGScheduler state through thread-safe
 * methods (getCacheLocs()); please be careful when modifying this method, because any new
 * DAGScheduler state accessed by it may require additional synchronization.
 */
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((rddpartition))) {
    // 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.rddinPartvisited)
        if (locs != Nil) {
          return locs
        }
      }

    case _ =>
  }

  Nil
}

在具体算法实现的时候,首先查询DAGScheduler的内存数据结构中是否存在当前partition的数据本地性的信息,如果有的话直接返回,如果没有首先会调用rdd.prefferedLocations。

例如想让Spark运行在hbase上或者一种现在还没有直接的数据库上面,此时开发者需要自定义RDD,为了保证Task计算的数据本地性,最为关键的方式就是必须实现RDD的getPreferredLocations。意思就是hbase部署在那里,Spark就部署在哪里

[scheduler]
(rdd: RDD[_]): [[TaskLocation]] = .synchronized {
  (!.contains(rdd.)) {
    locs: [[TaskLocation]] = (rdd.getStorageLevel == StorageLevel.) {
      .fill(rdd.partitions.length)()
    } {
      blockIds =
        rdd.partitions.indices.map(index => (rdd.index)).toArray[BlockId]
      blockManagerMaster.getLocations(blockIds).map { bms =>
        bms.map(bm => (bm.hostbm.executorId))
      }
    }
    (rdd.) = locs
  }
  (rdd.)
}

3、DAGScheduler计算数据本地性的时候,巧妙的借助了RDD自身的getPreferredLocations中的数据最大化的优化了效率,因为getPreferredLocations中表名了每个partition的数据本地性,虽然当前partition可能被pesrsist或者checkpoint,但是partition或者pesrsist默认情况下肯定是和getPreferredLocations中的partition的数据本地性是一致的,这就极大的简化了Task数据本地性算法的实现和效率的优化;

王家林老师名片:

中国Spark第一人

新浪微博:http://weibo.com/ilovepains

微信公众号:DT_Spark

博客:http://blog.sina.com.cn/ilovepains

手机:18610086859

QQ:1740415547

邮箱:[email protected]


本文出自 “一枝花傲寒” 博客,谢绝转载!

你可能感兴趣的:(Stage划分和Task最佳位置算法源码彻底解密(DT大数据梦工厂))