O motivo pelo qual o estágio foi ignorado é exibido no SparkUI [análise do código-fonte]

Muitas vezes, mostra que tarefas e estágios são ignorados na IU da página inicial do Spark, conforme mostrado na seguinte captura de tela:
Insira a descrição da imagem aqui

Este artigo explicará em quais circunstâncias o estágio ou tarefa será exibido como ignorado e se a execução do aplicativo Spark causará problemas quando o estágio e a tarefa forem exibidos como ignorados?

Depois que a última tarefa do ResultStage do Spark Job for executada com sucesso, o método DAGScheduler.handleTaskCompletion enviará o evento SparkListenerJobEnd. O código-fonte é o seguinte:

private[scheduler] def handleTaskCompletion(event: CompletionEvent) {
    
      
    val task = event.task  
    val stageId = task.stageId  
    val taskType = Utils.getFormattedClassName(task)  
  
    outputCommitCoordinator.taskCompleted(stageId, task.partitionId,  
      event.taskInfo.attempt, event.reason)  
  
    // The success case is dealt with separately below, since we need to compute accumulator  
    // updates before posting.  
    if (event.reason != Success) {
    
      
      val attemptId = stageIdToStage.get(task.stageId).map(_.latestInfo.attemptId).getOrElse(-1)  
      listenerBus.post(SparkListenerTaskEnd(stageId, attemptId, taskType, event.reason,  
        event.taskInfo, event.taskMetrics))  
    }  
  
    if (!stageIdToStage.contains(task.stageId)) {
    
      
      // Skip all the actions if the stage has been cancelled.  
      return  
    }  
  
    val stage = stageIdToStage(task.stageId)  
    event.reason match {
    
      
      case Success =>  
        listenerBus.post(SparkListenerTaskEnd(stageId, stage.latestInfo.attemptId, taskType,  
          event.reason, event.taskInfo, event.taskMetrics))  
        stage.pendingTasks -= task  
        task match {
    
      
          case rt: ResultTask[_, _] =>  
            // Cast to ResultStage here because it's part of the ResultTask  
            // TODO Refactor this out to a function that accepts a ResultStage  
            val resultStage = stage.asInstanceOf[ResultStage]  
            resultStage.resultOfJob match {
    
      
              case Some(job) =>  
                if (!job.finished(rt.outputId)) {
    
      
                  updateAccumulators(event)  
                  job.finished(rt.outputId) = true  
                  job.numFinished += 1  
                  // If the whole job has finished, remove it  
                  if (job.numFinished == job.numPartitions) {
    
    //ResultStage所有任务都执行完毕,发送SparkListenerJobEnd事件  
                    markStageAsFinished(resultStage)  
                    cleanupStateForJobAndIndependentStages(job)  
                    listenerBus.post(  
                      SparkListenerJobEnd(job.jobId, clock.getTimeMillis(), JobSucceeded))  
                  }  
  
                  // taskSucceeded runs some user code that might throw an exception. Make sure  
                  // we are resilient against that.  
                  try {
    
      
                    job.listener.taskSucceeded(rt.outputId, event.result)  
                  } catch {
    
      
                    case e: Exception =>  
                      // TODO: Perhaps we want to mark the resultStage as failed?  
                      job.listener.jobFailed(new SparkDriverExecutionException(e))  
                  }  
                }  
              case None =>  
                logInfo("Ignoring result from " + rt + " because its job has finished")  
            }

O método JobProgressListener.onJobEnd é responsável por manipular o evento SparkListenerJobEnd, o código é o seguinte:

override def onJobEnd(jobEnd: SparkListenerJobEnd): Unit = synchronized {
    
      
   val jobData = activeJobs.remove(jobEnd.jobId).getOrElse {
    
      
     logWarning(s"Job completed for unknown job ${jobEnd.jobId}")  
     new JobUIData(jobId = jobEnd.jobId)  
   }  
   jobData.completionTime = Option(jobEnd.time).filter(_ >= 0)  
  
   jobData.stageIds.foreach(pendingStages.remove)  
   jobEnd.jobResult match {
    
      
     case JobSucceeded =>  
       completedJobs += jobData  
       trimJobsIfNecessary(completedJobs)  
       jobData.status = JobExecutionStatus.SUCCEEDED  
       numCompletedJobs += 1  
     case JobFailed(exception) =>  
       failedJobs += jobData  
       trimJobsIfNecessary(failedJobs)  
       jobData.status = JobExecutionStatus.FAILED  
       numFailedJobs += 1  
   }  
   for (stageId <- jobData.stageIds) {
    
      
     stageIdToActiveJobIds.get(stageId).foreach {
    
     jobsUsingStage =>  
       jobsUsingStage.remove(jobEnd.jobId)  
       if (jobsUsingStage.isEmpty) {
    
      
         stageIdToActiveJobIds.remove(stageId)  
       }  
       stageIdToInfo.get(stageId).foreach {
    
     stageInfo =>  
         if (stageInfo.submissionTime.isEmpty) {
    
    //Job的Stage没有提交执行,则这个Stage和它对应的Task会标记为skipped stage和skipped task进行统计  
           // if this stage is pending, it won't complete, so mark it as "skipped":  
           skippedStages += stageInfo  
           trimStagesIfNecessary(skippedStages)  
           jobData.numSkippedStages += 1  
           jobData.numSkippedTasks += stageInfo.numTasks  
         }  
       }  
     }  
   }  
 }

StageInfo.submissionTime é definido antes que o Stage seja decomposto em TaskSet e o TaskSet seja enviado ao TaskSetManager. O código-fonte é o seguinte:

private def submitMissingTasks(stage: Stage, jobId: Int) {
    
      
   logDebug("submitMissingTasks(" + stage + ")")  
   // Get our pending tasks and remember them in our pendingTasks entry  
   stage.pendingTasks.clear()  
  
  
   // First figure out the indexes of partition ids to compute.  
   //parititionsToCompute是一个List, 表示一个stage需要compute的所有分区的index  
   val partitionsToCompute: Seq[Int] = {
    
      
     stage match {
    
      
       case stage: ShuffleMapStage =>  
         (0 until stage.numPartitions).filter(id => stage.outputLocs(id).isEmpty)  
       case stage: ResultStage =>  
         val job = stage.resultOfJob.get  
         (0 until job.numPartitions).filter(id => !job.finished(id))  
     }  
   }  
  
   val properties = jobIdToActiveJob.get(stage.firstJobId).map(_.properties).orNull  
  
   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.latestInfo = StageInfo.fromStage(stage, Some(partitionsToCompute.size))  
   outputCommitCoordinator.stageStart(stage.id)  
   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  
   try {
    
      
     // For ShuffleMapTask, serialize and broadcast (rdd, shuffleDep).  
     // For ResultTask, serialize and broadcast (rdd, func).  
     val taskBinaryBytes: Array[Byte] = stage match {
    
      
       case stage: ShuffleMapStage =>  
         closureSerializer.serialize((stage.rdd, stage.shuffleDep): AnyRef).array()  
       case stage: ResultStage =>  
         closureSerializer.serialize((stage.rdd, stage.resultOfJob.get.func): AnyRef).array()  
     }  
  
     taskBinary = sc.broadcast(taskBinaryBytes)//将任务信息构造成广播变量,广播到每个Executor  
   } catch {
    
      
     // In the case of a failure during serialization, abort the stage.  
     case e: NotSerializableException =>  
       abortStage(stage, "Task not serializable: " + e.toString)  
       runningStages -= stage  
  
       // Abort execution  
       return  
     case NonFatal(e) =>  
       abortStage(stage, s"Task serialization failed: $e\n${e.getStackTraceString}")  
       runningStages -= stage  
       return  
   }  
   //tasks是一个List,它表示一个stage每个task的描述,描述信息为:task所在stage id、task处理的partition、partition所在的主机地址和Executor id  
   val tasks: Seq[Task[_]] = try {
    
      
     stage match {
    
      
       case stage: ShuffleMapStage =>  
         partitionsToCompute.map {
    
     id =>  
           /* 
           * 获取task所在的节点,数据所在的节点优先启动任务处理这些数据,在这里用到ShuffleMapStage. 
           * */  
           val locs = getPreferredLocs(stage.rdd, id)  
           val part = stage.rdd.partitions(id)  
           new ShuffleMapTask(stage.id, taskBinary, part, locs)//taskBinary是广播变量  
         }  
  
       case stage: ResultStage =>  
         val job = stage.resultOfJob.get  
         partitionsToCompute.map {
    
     id =>  
           val p: Int = job.partitions(id)  
           val part = stage.rdd.partitions(p)  
           val locs = getPreferredLocs(stage.rdd, p)  
           new ResultTask(stage.id, taskBinary, part, locs, id)  
         }  
     }  
   } catch {
    
      
     case NonFatal(e) =>  
       abortStage(stage, s"Task creation failed: $e\n${e.getStackTraceString}")  
       runningStages -= stage  
       return  
   }  
  
   if (tasks.size > 0) {
    
      
     logInfo("Submitting " + tasks.size + " missing tasks from " + stage + " (" + stage.rdd + ")")  
     stage.pendingTasks ++= tasks  
     logDebug("New pending tasks: " + stage.pendingTasks)  
     taskScheduler.submitTasks(  
       new TaskSet(tasks.toArray, stage.id, stage.newAttemptId(), stage.firstJobId, properties))  
     stage.latestInfo.submissionTime = Some(clock.getTimeMillis())//设置StageInfo的submissionTime成员,表示这个TaskSet会被执行,不会被skipped  
   } else

O estágio do trabalho não é decomposto em TaskSet e enviado para execução. O estágio e sua tarefa correspondente serão marcados como estágio ignorado e tarefa ignorada para exibição estatística.

Qual estágio não será decomposto em TaskSet e executado?

Quando o Spark envia um trabalho, ele envia um evento JobSubmitted. Depois que DAGScheduler.doOnReceive recebe o evento JobSubmitted, ele chama o método DAGScheduler.handleJobSubmitted para processar o envio da tarefa.

DAGScheduler.handleJobSubmitted primeiro chama o método DAGScheduler.newResultStage para criar o último estágio. DAGScheduler.newResultStage eventualmente será chamado para DAGScheduler.registerShuffleDependencies por meio da seguinte série de chamadas de função. Este método adiciona todos os estágios ancestrais deste RDulD ao Hashched .jobIdToStageIds. Em seguida, obtenha o StageInfo correspondente a cada estágio deste trabalho, converta-o em um Seq e envie o evento SparkListenerJobStart.
DAGScheduler.newResultStage->
DAGScheduler.getParentStagesAndId->
DAGScheduler.getParentStagesAndId-> getParentStages
DAGScheduler.getParentStagesAndId-> getShuffleMapStage
DAGScheduler.registerShuffleDependencies

DAGScheduler.registerShuffleDependencies primeiro chama DAGScheduler.getAncestorShuffleDependencies para encontrar as dependências rdd de todos os ancestrais do rdd atual, incluindo pais, avôs e dependências de rdd de nível ainda mais alto e, em seguida, chama DAGScheduler.newOrUsedMaporShuffleStage para criar o correspondente rdd ,

private def registerShuffleDependencies(shuffleDep: ShuffleDependency[_, _, _], firstJobId: Int) {
    
      
    val parentsWithNoMapStage = getAncestorShuffleDependencies(shuffleDep.rdd)//获取所有祖宗rdd依赖,包括父辈、爷爷辈等  
    while (parentsWithNoMapStage.nonEmpty) {
    
      
      val currentShufDep = parentsWithNoMapStage.pop()  
      //根据ShuffleDependency和jobid生成Stage,由于是从栈里面弹出,所以最先添加的是Root stage,依次类推,最先添加的Stage shuffleId越小  
      val stage = newOrUsedShuffleStage(currentShufDep, firstJobId)  
      shuffleToMapStage(currentShufDep.shuffleId) = stage  
    }  
  }

DAGScheduler.newOrUsedShuffleStage chamará DAGScheduler.newShuffleMapStage para criar um palco.
Depois que o método DAGScheduler.newShuffleMapStage cria o estágio, chame o método DAGScheduler.updateJobIdStageIdMaps para adicionar o
stage.id recém-criado a DAGScheduler.jobIdToStageIds. O código-fonte é o seguinte:

private def updateJobIdStageIdMaps(jobId: Int, stage: Stage): Unit = {
    
      
   def updateJobIdStageIdMapsList(stages: List[Stage]) {
    
      
     if (stages.nonEmpty) {
    
      
       val s = stages.head  
       s.jobIds += jobId  
       jobIdToStageIds.getOrElseUpdate(jobId, new HashSet[Int]()) += s.id//将stage id加入到jobIdToStageIds中  
       val parents: List[Stage] = getParentStages(s.rdd, jobId)  
       val parentsWithoutThisJobId = parents.filter {
    
     ! _.jobIds.contains(jobId) }  
       updateJobIdStageIdMapsList(parentsWithoutThisJobId ++ stages.tail)  
     }  
   }  
   updateJobIdStageIdMapsList(List(stage))  
 }

O código-fonte de DAGScheduler.handleJobSubmitted é o seguinte:

private[scheduler] def handleJobSubmitted(jobId: Int,  
      finalRDD: RDD[_],  
      func: (TaskContext, Iterator[_]) => _,  
      partitions: Array[Int],  
      allowLocal: Boolean,  
      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(finalRDD, partitions.size, jobId, callSite)//创建ResultStage,在这个方法里面会将这个Job执行过程中,需要可能经历的Stage全部放入到  
    } catch {
    
      
      case e: Exception =>  
        logWarning("Creating new stage failed due to exception - job: " + jobId, e)  
        listener.jobFailed(e)  
        return  
    }  
    if (finalStage != null) {
    
      
      val job = new ActiveJob(jobId, finalStage, func, partitions, callSite, listener, properties)  
      clearCacheLocs()  
      logInfo("Got job %s (%s) with %d output partitions (allowLocal=%s)".format(  
        job.jobId, callSite.shortForm, partitions.length, allowLocal))  
      logInfo("Final stage: " + finalStage + "(" + finalStage.name + ")")  
      logInfo("Parents of final stage: " + finalStage.parents)  
      logInfo("Missing parents: " + getMissingParentStages(finalStage))  
      val shouldRunLocally =  
        localExecutionEnabled && allowLocal && finalStage.parents.isEmpty && partitions.length == 1  
      val jobSubmissionTime = clock.getTimeMillis()  
      if (shouldRunLocally) {
    
      
        // Compute very short actions like first() or take() with no parent stages locally.  
        listenerBus.post(  
          SparkListenerJobStart(job.jobId, jobSubmissionTime, Seq.empty, properties))  
        runLocally(job)  
      } else {
    
      
        jobIdToActiveJob(jobId) = job  
        activeJobs += job  
        finalStage.resultOfJob = Some(job)  
        val stageIds = jobIdToStageIds(jobId).toArray//获取一个Job对应的所有的Stage id,Job的所有Stage在执行newResultStage的时候会创建,所以在这里能获取成功  
        val stageInfos = stageIds.flatMap(id => stageIdToStage.get(id).map(_.latestInfo))//获取每个Stage对应的StageInfo  
        listenerBus.post(  
          SparkListenerJobStart(job.jobId, jobSubmissionTime, stageInfos, properties))//发送Job启动事件SparkListenerJobStart  
        submitStage(finalStage)  
      }  
    }  
    submitWaitingStages()  
  }

para concluir:

JobProgressListener.onJobStart é responsável por receber e processar eventos SparkListenerJobStart. Ele colocará todas as informações de StageInfo criadas pelo método DAGScheduler.handleJobSubmitted no HashMap de JobProgressListener.stageIdToInfo.

Até agora, pode-se concluir: No método JobProgressListener.onJobEnd, a informação obProgressListener.stageIdToInfo processada é
gerada executando DAGScheduler.handleJobSubmitted. Ele foi gerado antes de todos os estágios correspondentes ao trabalho serem decompostos em tarefas
.

O artigo pode saber que quando o Palco é decomposto em TaskSet, se um RDD foi armazenado em cache no BlockManager, todos os estágios ancestrais correspondentes a este RDD não serão decompostos em TaskSet para execução, portanto, o StageInfo.submissionTime.isEmpty correspondente a esses ancestrais Stages retornará true, portanto, esses estágios ancestrais e suas
tarefas correspondentes serão exibidos no Spark ui como o estágio ignorado . Depois que a execução for concluída, JobProgressListener.onStageCompleted será executado para salvar as informações do estágio em JobProgressListener.stageIdToInfo. O código-fonte é o seguinte:

override def onStageCompleted(stageCompleted: SparkListenerStageCompleted): Unit = synchronized {
    
      
    val stage = stageCompleted.stageInfo  
    stageIdToInfo(stage.stageId) = stage//保存Stage的信息,便于跟踪显示  
    val stageData = stageIdToData.getOrElseUpdate((stage.stageId, stage.attemptId), {
    
      
      logWarning("Stage completed for unknown stage " + stage.stageId)  
      new StageUIData  
    })

Depois que todas as tarefas no TaskSet correspondente ao Palco forem executadas com êxito, o StageInfo correspondente ao Palco será realimentado para JobProgressListener.stageIdToInfo, para que essas tarefas não sejam exibidas como ignoradas

É normal que a tarefa apareça como ignorada.A razão para a omissão é que os dados a serem calculados foram armazenados em cache na memória e não há necessidade de repetir o cálculo. A aparência de ignorado não tem efeito no resultado

Acho que você gosta

Origin blog.csdn.net/qq_32727095/article/details/113740277
Recomendado
Clasificación