在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);
}
}
}
}
此时作业的相关信息会被放入历史服务器进行存档,以便以后查看.
到这里一个作业就完成了.