前言 :本文旨在理清在Hadoop中一个MapReduce作业(Job)在提交到框架后的整个生命周期过程,权作总结和日后参考,如有问题,请不吝赐教。本文不涉及Hadoop的架构设计,如有兴趣请参考相关书籍和文献。在梳 理过程中,我对一些感兴趣的源码也会逐行研究学习,以期强化基础。
作者 :Jaytalent
开始日期 :2013年9月9日参考资料:【1】《Hadoop技术内幕--深入解析MapReduce架构设计与实现原理》董西成【2】 Hadoop 1.0.0 源码
【3】《Hadoop技术内幕--深入解析Hadoop Common和HDFS架构设计与实现原理》蔡斌 陈湘萍
一个MapReduce作业的生命周期大体分为5个阶段
【1】:
1. 作业提交与初始化
2. 任务调度与监控
3.
任务运行环境准备
4.
任务执行
5.
作业完成
这篇文章将任务的准备、执行到整个作业完成的过程进行研究。
一、任务启动
任务启动过程由TaskTracker完成。启动过程如下:首先判断是否是第一次收到某个作业的任务,若是就进行作业本地化,然后创建任务目录,否则直接创建任务目录。接下来启动JVM,并从TaskTracker获取任务,进行任务本地化并执行任务。任务执行后,若该JVM没有到重用次数上限,则再次从TaskTracker获取任务,重复上述过程。总体上讲,主要包括作业本地化、任务本地化和启动任务三步。本地化的目的是为任务运行创建一个环境,包括工作目录、下载运行所需文件和设置环境变量等。作业本地化由该作业第一个任务启动时执行。下面依次来看上述步骤。
1. 作业本地化
我们先从TaskTracker(以下简称TT)收到心跳响应说起。在TT的offerService方法中,通过transmitHeartbeat方法收到心跳响应,并从中提取出需要执行的命令:
HeartbeatResponse heartbeatResponse = transmitHeartBeat(now);
TaskTrackerAction[] actions = heartbeatResponse.getActions();
if (actions != null){
for(TaskTrackerAction action: actions) {
if (action instanceof LaunchTaskAction) {
addToTaskQueue((LaunchTaskAction)action);
} else if (action instanceof CommitTaskAction) {
CommitTaskAction commitAction = (CommitTaskAction)action;
}
}
}
addToTaskQueue方法根据任务类型选择指定的启动器:
private void addToTaskQueue(LaunchTaskAction action) {
if (action.getTask().isMapTask()) {
mapLauncher.addToTaskQueue(action);
} else {
reduceLauncher.addToTaskQueue(action);
}
}
其中mapLauncher和reduceLauncher是两个TaskLauncher对象,该对象继承于Thread类,即启动每个map或reduce任务各由一个单独的线程完成。
addToTaskQueue方法将任务注册后加入待启动任务列表,并通知等待线程有新任务加入。
public void addToTaskQueue(LaunchTaskAction action) {
synchronized (tasksToLaunch) {
TaskInProgress tip = registerTask(action, this);
tasksToLaunch.add(tip);
tasksToLaunch.notifyAll();
}
}
在启动线程的run方法中调用startNewTask方法启动一个任务:
void startNewTask(final TaskInProgress tip) throws InterruptedException {
Thread launchThread = new Thread(new Runnable() {
@Override
public void run() {
try {
RunningJob rjob = localizeJob(tip);
tip.getTask().setJobFile(rjob.getLocalizedJobConf().toString());
// Localization is done. Neither rjob.jobConf nor rjob.ugi can be null
launchTaskForJob(tip, new JobConf(rjob.getJobConf()), rjob);
} catch (Throwable e) {
String msg = ("Error initializing " + tip.getTask().getTaskID() +
":\n" + StringUtils.stringifyException(e));
LOG.warn(msg);
tip.reportDiagnosticInfo(msg);
}
}
});
launchThread.start();
}
从上面实现看出,每个一个任务的具体启动过程是由一个单独的线程完成的。这种设计的考虑是:用户应用程序依赖的文件很大,从HDFS上下载的时间会很长,如果串行启动任务必然导致时间过长。
synchronized (rjob) {
if (!rjob.localized) {
while (rjob.localizing) {
rjob.wait();
}
if (!rjob.localized) {
//this thread is localizing the job
rjob.localizing = true;
}
}
}
if (!rjob.localized) {
Path localJobConfPath = initializeJob(t, rjob, ttAddr);
}
注意这段代码,每个作业有两个状态:localizing和localized,分别表示作业正在本地化和作业本地化已经完成。当第一个任务启动时,作业处于正在本地化的状态,作业不会等待,直接将localizing设置为true,并执行initializeJob执行作业