MapReduce作业运行流程

在Hadoop执行MapReduce任务之前,需要我们对作业进行提交.我们在通过开发工具提交之后,Hadoop做了哪些工作?这篇就来看看Hadoop在我们提交MapReduce作业之后都做了什么.

大体分为以下几个阶段:作业的提交,作业的初始化,作业的分配,作业的执行,以及作业的完成.中间穿插了对任务状态的更新过程,及最终的任务完成.下面就分别来介绍一下这几个阶段具体都做了什么.

作业的提交

此时我们已经编写好了MapReduce程序,准备将其提交至集群.

在驱动中,我们调用了Job.waitForCompletion()

 public boolean waitForCompletion(boolean verbose
                                   ) throws IOException, InterruptedException,
                                            ClassNotFoundException {
    if (state == JobState.DEFINE) { submit(); } //1
    if (verbose) {
      monitorAndPrintJob();
    } else {
      int completionPollIntervalMillis = 
        Job.getCompletionPollInterval(cluster.getConf()); 
      while (!isComplete()) { //2
        try {
          Thread.sleep(completionPollIntervalMillis); 
        } catch (InterruptedException ie) {
        }
      }
    }
    return isSuccessful();//3
  }

 1.如果作业的状态是DEFINE,就调用submit().

2.当作业还没有完成就执行里面的逻辑.

3.返回作业是否运行成功.

这就是作业提交的开始,这个方法会每秒轮询作业的进度,若就上一次所汇报的进度有改变,就输出至控制台,直到作业完成,就显示计数器;若失败,就输出引起失败的信息.在这个方法内部,会调用submit(),我们也可以在驱动中直接调用submit().

 public void submit() 
         throws IOException, InterruptedException, ClassNotFoundException {
    ensureState(JobState.DEFINE); //1
    setUseNewAPI();               //2
    connect();                    //3
    final JobSubmitter submitter = 
        getJobSubmitter(cluster.getFileSystem(), cluster.getClient());
    status = ugi.doAs(new PrivilegedExceptionAction<JobStatus>() {
      public JobStatus run() throws IOException, InterruptedException, 
      ClassNotFoundException {
        return submitter.submitJobInternal(Job.this, cluster); //4
      }
    });
    state = JobState.RUNNING;  //5
    LOG.info("The url to track the job: " + getTrackingURL());
   }

逐条解释如下:

1,确认作业有没有被重复提交.

2.根据获取的配置信息确定是否使用新API.

3.与集群建立连接.

4.调用JobSubmitter.submitJobInternal(),返回JobStatus对象.

5.将作业的状态设置为RUNNING.

JobSubmitter内部,会完成以下工作:

1.向资源管理器(ResourceManager)请求一个新的应用ID,用于该作业.

    JobID jobId = submitClient.getNewJobID();
    job.setJobID(jobId);

2.检查作业的输出说明.若输出目录已经存在或没有指定输出目录,就不提交作业,并抛出异常.

3.计算作业的输入分片,若无法计算,比如输入目录不存在,就不提交作业,并抛出异常.

      LOG.debug("Creating splits at " + jtFs.makeQualified(submitJobDir));
      int maps = writeSplits(job, submitJobDir); //1
      conf.setInt(MRJobConfig.NUM_MAPS, maps);   //2
      LOG.info("number of splits:" + maps);      //3

1.获取输入分片数量,也就相当于获得了map任务的数量.

2.将获取的map数传入配置信息. 

3.输出信息:分片数量为:map数.

4.将作业所需资源(程序Jar包,配置信息,输入分片)复制到一个以作业ID命名的目录下的共享文件系统.

    copyAndConfigureFiles(job, submitJobDir);

5.调用资源管理器的submitApplication()提交作业.

经过层层调用,最终会进入YARNRunner的submitJob(),在这里作业会被提交至资源管理器.

public class YARNRunner implements ClientProtocol{}

 public JobStatus submitJob(JobID jobId, String jobSubmitDir, Credentials ts)
  throws IOException, InterruptedException {
   
    addHistoryToken(ts);//添加至历史记录

    ApplicationSubmissionContext appContext =
      createApplicationSubmissionContext(conf, jobSubmitDir, ts);
    // Submit to ResourceManager 此时将作业提交至资源管理器(ResourceManager)
    try {
      ApplicationId applicationId =
          resMgrDelegate.submitApplication(appContext);
      ApplicationReport appMaster = resMgrDelegate
          .getApplicationReport(applicationId);
      String diagnostics =
          (appMaster == null ?
              "application report is null" : appMaster.getDiagnostics());
      if (appMaster == null
          || appMaster.getYarnApplicationState() == YarnApplicationState.FAILED
          || appMaster.getYarnApplicationState() == YarnApplicationState.KILLED) {
                //失败状态或被杀掉
        throw new IOException("Failed to run job : " + //作业运行失败
            diagnostics);
      }
      return clientCache.getClient(jobId).getJobStatus(jobId);
    } catch (YarnException e) {//捕获YarnException
      throw new IOException(e);
    }
  }

随后就进入了作业的初始化阶段.

作业的初始化

资源管理器收到submitApplication()之后,就将请求传输至YARN调度器.调度器分配一个容器,资源管理器在节点管理器(NodeManager)的管理下,在容器中启动application master进程.

MapReduce作业的application master以MRAppMaster为主类.其中有一个main(),可以想到,这个类的一切工作都从这个main()开始.

public class MRAppMaster extends CompositeService {
    ......
    public static void main(String[] args) {......}
    .....
}

它将接受作业的进度与完成报告,之后接受来自文件系统且已经在客户端计算完毕的输入分片.然后对每一个输入分片创建1个map任务,以及我们设定的reduce任务数(setReduceNum(int 数量),或配置文件中的mapreduce.job.reduce).

  public class Job extends JobContextImpl implements JobContext {}
    public void setNumReduceTasks(int tasks) throws IllegalStateException {
        ensureState(JobState.DEFINE);
        conf.setNumReduceTasks(tasks);
      }

此时会分配任务ID.

然后application master会进行判断是否可以在自己的JVM上运行任务.条件是,application master判断在新的容器中分配和运行任务的开销大于在与自己同一JVM运行的开销时,就会与自己在同一个JVM运行.再具体一点,默认情况下,少于10个map任务且只有1个reduce任务且输入大小小于1个HDFS块就是这类任务.这种任务叫做Uber任务.

public class Job extends JobContextImpl implements JobContext {} 
    public boolean isUber() throws IOException, InterruptedException {
        ensureState(JobState.RUNNING);
        updateStatus();
        return status.isUber();
      }

当然我们需要在配置文件中启动这个选项,具体来说就是mapreduce.job.ubertask.enable设置为true.

在运行任何任务之前,application master会调用setJob()设置OutputComitter.

public abstract class OutputCommitter {

  public abstract void setupJob(JobContext jobContext) throws IOException;

  @Deprecated
  public void cleanupJob(JobContext jobContext) throws IOException { }

  public void commitJob(JobContext jobContext) throws IOException {
    cleanupJob(jobContext);
  }

  public void abortJob(JobContext jobContext, JobStatus.State state) 
  throws IOException {
    cleanupJob(jobContext);
  }
  
  public abstract void setupTask(TaskAttemptContext taskContext)
  throws IOException;

  public abstract boolean needsTaskCommit(TaskAttemptContext taskContext)
  throws IOException;

  public abstract void commitTask(TaskAttemptContext taskContext)
  throws IOException;

  public abstract void abortTask(TaskAttemptContext taskContext)
  throws IOException;

  @Deprecated
  public boolean isRecoverySupported() {
    return false;
  }

  public boolean isCommitJobRepeatable(JobContext jobContext)
      throws IOException {
    return false;
  }

  public boolean isRecoverySupported(JobContext jobContext) throws IOException {
    return isRecoverySupported();
  }

  public void recoverTask(TaskAttemptContext taskContext)
  throws IOException{}
}

默认为FileOutputComitter,表示将建立作业的最终输出目录以及任务临时空间.

任务的分配

application master会为map任务和reduce任务向资源管理器请求容器.首先会为map任务请求容器,其次是reduce任务,很好理解,因为map任务要先于reduce任务执行,具体地说,就是所有的map任务必须在reduce的排序阶段能够启动前完成.何时请求reduce任务所需容器?在5%的map任务完成后,就会发出请求.

此时就要决定任务的执行位置.reduce任务可以在集群任意位置执行,但是map任务就不一样了,因为最优的选择是在拥有该数据分片的节点上运行.但是如果该节点现在比较繁忙,就会在同机架内寻找相对空闲的节点运行,再不能满足才会在其他机架请求可用的节点.

任务运行需要资源,默认情况下,会为每个map任务和reduce任务请求1GB内存1个CPU核心.

任务的执行

分配完容器并准备好资源之后,就可以开始运行任务了.

资源管理器的调度器为任务分配了一个特定节点上的容器,application master就通过与节点管理器通信来启动容器.该任务由主类为YarnChild的一个Java应用程序执行.

class YarnChild {

  private static final Log LOG = LogFactory.getLog(YarnChild.class);
  static volatile TaskAttemptID taskid = null;

  public static void main(String[] args) throws Throwable {}

  public static void setEncryptedSpillKeyIfRequired(Task task) 
         throws Exception {}

  private static void configureLocalDirs(Task task, JobConf job) 
          throws IOException {}

  private static void configureTask(JobConf job, Task task,
      Credentials credentials, Token<JobTokenIdentifier> jt) 
          throws IOException {}

  private static final FsPermission urw_gr =
    FsPermission.createImmutable((short) 0640);

  private static void writeLocalJobFile(Path jobFile, JobConf conf)
      throws IOException {}
}

运行之前,会将任务所需资源本地化,之后就开始执行map任务与reduce任务.由于YarnChild在单独的JVM中运行,所以map任务和reduce任务出错也不会影响它.

每个任务都能执行搭建(setup)和提交(commit)动作,和任务在同一个JVM运行,并由作业的OutputComitter确定.对于基于文件的作业,提交动作将任务输出由临时位置移动至最终位置.提交协议确保党启动了推测执行时,只有一个任务副本被提交,其余的都被取消.

任务状态的更新

每个作业和任务都有一个状态,包括作业或任务的状态(运行中,成功完成,失败等),map任务进度与reduce任务进度,作业计数器的值,状态信息或描述.在作业执行期间,会不断的更新.

对于map任务,进度就是已处理输入所占得比例.对reduce来说有些复杂,大体上也是估计输入比例.整个过程分为3部分,与shuffle的3个阶段对应.

作业的完成

经历了长时间的运行之后,这个作业终于运行完毕了,此时需要一些善后工作.

application master收到了最后一个任务已完成的通知,就会把作业的状态设置为成功.Job此时就会知道作业已经成功完成,就输出一条信息通知用户,然后从waitForCompletion()返回,并输出该作业的统计信息.

最后,application master和任务容器开始善后工作:将中间的输出结果删除,OutputCommitter的commitJob()被调用.这里展示的是FileOutputComitter中的代码.

 public void commitJob(JobContext context) throws IOException {
    int maxAttemptsOnFailure = isCommitJobRepeatable(context) ?
        context.getConfiguration().getInt(FILEOUTPUTCOMMITTER_FAILURE_ATTEMPTS,
            FILEOUTPUTCOMMITTER_FAILURE_ATTEMPTS_DEFAULT) : 1;
    int attempt = 0;
    boolean jobCommitNotFinished = true;
    while (jobCommitNotFinished) { 
      try {
        commitJobInternal(context); 
        jobCommitNotFinished = false;
      } catch (Exception e) {
        if (++attempt >= maxAttemptsOnFailure) { 
          throw e;
        } else {
          LOG.warn("Exception get thrown in job commit, retry (" + attempt +
              ") time.", e);
        }
      }
    }
  }

此时作业的相关信息会被放入历史服务器进行存档,以便以后查看.

到这里一个作业就完成了.

猜你喜欢

转载自blog.csdn.net/bujiujie8/article/details/86673963