作业提交与初始化主要是为后续的MR程序运行做准备工作.
将其分为四个步骤即1配置MR作业环境, 2上传作业信息, 3提交作业, 4作业初始化.
下面将分别介绍以上四个步骤
1.1设置作业环境
这一步就是我们在MR中经常要用到的那些配置. 配置保证MR基本运行的参数.
以wordcount为例配置如下:
Configuration conf = new Configuration();
Job job = new Job(conf);
// 当我们的应用打包成jar,在hadoop中执行时,必须有该行代码
job.setJarByClass(WordCountApp.class);
// 告诉job执行我们自定义的Mapper类
job.setMapperClass(WordCountMapper.class);
job.setCombinerClass(IntSumReducer.class);
//job.setPartitionerClass(HashPartitioner.class);
// 告诉job执行我们自定义的Reducer类
job.setReducerClass(WordCountReducer.class);
// 告诉job,k3是什么类型
job.setOutputKeyClass(Text.class);
// 告诉job,v3是什么类型
job.setOutputValueClass(IntWritable.class);
// 告诉job, 将要执行的Reduce数量.
job.setNumReduceTasks(4);
// 告诉job输入源在哪里;输入可以是多个文件
FileInputFormat.addInputPath(job, new Path(BaseConf.URI_HDFS_PREFIX
+ "/test/wd/input"));
// 告诉job输出路径在哪里;
FileOutputFormat.setOutputPath(job, new Path(BaseConf.URI_HDFS_PREFIX
+ "/test/wd/output"));
job.waitForCompletion(true);
当然针对一个MR还得很多可用的配置, 在此采用默认配置, 可以关注JobConf.java中所提供的set接口
这里重点关注于整个架构的执行过程来去理解运行原理.
上面的设置都是通过job的成员jobConf去完成的.
1.1.2提交前的准备工作
public void submit() throws IOException, InterruptedException,
ClassNotFoundException {
ensureState(JobState.DEFINE); //确认作业未运行.
setUseNewAPI(); //根据配置"mapred.mapper.new-api"与"mapred.reducer.new-api"设置是否使用了新的MR API
// Connect to the JobTracker and submit the job
connect();
info = jobClient.submitJobInternal(conf);
super.setJobID(info.getID());
state = JobState.RUNNING;
}
1.1.3连接到JT
利用java安全的存取控制器去检查安全策略,是否允许连接到JT.
这会通过创建JobClient实例, 并通过RPC连接到JT.
JobSubmissionProtocolrpcJobSubmitClient = (JobSubmissionProtocol)RPC.getProxy(JobSubmissionProtocol.class,…
1.2 上传作业信息
1> 从JT获取HDFS上保存作业的目录
Path jobStagingArea =JobSubmissionFiles.getStagingDir(JobClient.this, jobCopy);
默认为${"mapreduce.jobtracker.staging.root.dir"}/tmp/hadoop/mapred/staging/${user}
2>分配JobId.
JobIDjobId = jobSubmitClient.getNewJobId();
3>指定提交目录
PathsubmitJobDir = new Path(jobStagingArea, jobId.toString()); //权限默认为700
4>保存作业信息到submitJobDir中
copyAndConfigureFiles(jobCopy,submitJobDir);
在其中会在submitJobDir中创建并拷贝指定文件到指定目录
submitJobDir/archives/对应启动参数-archives指定的文件
submitJobDir/files/ 对应启动参数-files指定文件.
submitJobDir/libjars/ 保存依赖的第三方jar文件.
submitJobDir/job.jar 对应将要运行的MR程序jar文件.
5>计算InputSplit信息并上传
intmaps = writeSplits(context, submitJobDir);
->InputFormat<?,?> input = ReflectionUtils.newInstance(job.getInputFormatClass(), conf);
->List<InputSplit>splits = input.getSplits(job);
-> JobSplitWriter.createSplitFiles(jobSubmitDir,conf, jobSubmitDir.getFileSystem(conf), array);
通过配置中设置的InputFormat类的getSplits计算输入文件的InputSplit信息.并将其上传到如下两个文件中:
submitJobDir/job.split
submitJobDir/job.splitmetainfo
然后返回map数.
splitmetainfo信息中包含了
1. job.split文件的位置
2. InputSplit在job.split文件中的位置
3. InputSplit的数据长度
4. InputSplit所在的host列表.
6>将jobConf中的配置信息导出到HDFS上的submitJobDir/job.xml中.
jobCopy.writeXml(out);
1.3提交作业
1.3.1提交作业
通过JT在客户端的代理对象提交作业到JT.
status= jobSubmitClient.submitJob( jobId, submitJobDir.toString(),jobCopy.getCredentials());
当JT收到客户端的提交作业submitJob执行如下图:
1.3.1.1创建JobInProgress监控作业
jobInfo = new JobInfo(jobId, newText(ugi.getShortUserName()), newPath(jobSubmitDir));
job = new JobInProgress(this, this.conf,jobInfo, 0, ts);
在JobInprogress初始时会根据HDFS中将作业信息job.xml创建对应的conf对象, 并根据其设置JobInprogress相关的信息.
同时还会创建几个重要的集合用于去保存作业运行时的一些作业信息.
//不需要考虑数据本地性的mapTask,
//如果MapTask的InputSplit.location[]为空, 则在任务调度时在此集合中不会考虑数据本地性
this.nonLocalMaps = new LinkedList<TaskInProgress>();
//这是一个按照失败次数进行排序TIP集合, 并指定了排序规则failComparator.
this.failedMaps = new TreeSet<TaskInProgress>(failComparator);
//还没有运行的Task集合.
this.nonLocalRunningMaps = new LinkedHashSet<TaskInProgress>();
//正在运行中的MapTask集合,
this.runningMapCache = new IdentityHashMap<Node,Set<TaskInProgress>>();
//未运行的Reduce集合, 按照failComparator排序, (这难道是说失败的reduce会重新加入到未运行的reduce集合中么?)
this.nonRunningReduces = newTreeSet<TaskInProgress>(failComparator);
//正在运行的reduceTask集合
this.runningReduces = new LinkedHashSet<TaskInProgress>();
1.3.1.2检查内存需求量
checkMemoryRequirements(job);
判断MR程序的配置"mapred.task.maxvmem"(默认为-1L)内存量是否超过集群允许的的配置"mapred.task.limit.maxvmem"量.
1.3.1.3 加入作业到队列中.
status = addJob(jobId, job);
jobs.put(job.getProfile().getJobID(), job);
for (JobInProgressListener listener : jobInProgressListeners) {
listener.jobAdded(job);
}
将jobInProgress加入到JT的jobs映射中. 然后通知任务调度器
在调度器启动时会将自己的监听器加入到JT的监听器队列中. 当有job加入时, 就会通知队列中的所有监听器加入了一个job作业.这样来告知调度器有一个作业到来.
1.4 作业初始化
1.4.1 通知调度器
这里我们以CapacityTaskScheduler高度器为例, 当CapacityTaskScheduler的JobQueuesManager监听器收到jobAdded事件后,
// add job to waiting queue. It will end up in the right place,
// based on priority.
queue.addWaitingJob(job);
// let scheduler know.
scheduler.jobAdded(job);
1. 将job加入到调度器的指定队列CapacitySchedulerQueue的waitingJobs映射中, 队列是由MR程序的配置"mapred.job.queue.name"决定的. 默认为default.
2. 告诉调度器调度队列中来了一个job
// Inform the queue
queue.jobAdded(job); //将作业提交用户在队列中的执行作业数加1.
// setup scheduler specific job information
preInitializeJob(job);
作job初始化前的准备工作.这主要是根据集群的配置"mapred.cluster.map.memory.mb"(每个map槽的内存量slotSizePerMap)和MR程序的配置"mapred.task.maxvmem"(每个task最大内存量getMemoryForMapTask())来决定每个mapTask将要占用集群的槽数.
公式为:(int)(Math.ceil((float)getMemoryForMapTask() / (float)slotSizePerMap))
ReduceTask同上.使用集群的"mapred.cluster.reduce.memory.mb"配置
1.4.2 调度器调度作业初始化
在调度器启动的同时也会启动几个后台服务线程. 其中有一个线程为JobInitializationPoller线程, 以下是其run方法执行情况:
JobInitializationPoller.run()代码:
while (running) {
cleanUpInitializedJobsList(); //从初始化集合中清理掉那些处于RUNNING状态的且被调度过的job, 或者已经完成的job
selectJobsToInitialize();
if (!this.isInterrupted()) {
Thread.sleep(sleepInterval); //配置"mapred.capacity-scheduler.init-poll-interval"默认为3000.
}
}
1.4.2.1 选择job去初始化.
selectJobsToInitialize()代码:
for(String queue : jobQueueManager.getAllQueues()) {
ArrayList<JobInProgress> jobsToInitialize =getJobsToInitialize(queue);
JobInitializationThread t = threadsToQueueMap.get(queue);
for (JobInProgress job : jobsToInitialize) {
t.addJobsToQueue(queue, job);
}
}
首先遍历所有集群中queue队列中的的所有jobInProgress实例, 并找出所有状态为JobStatus.PREP 的集合.
然后找到用于初始化Job的线程JobInitializationThread(这个线程是threadsToQueueMap映射中根据queue指定的队列所对应的启动线程. 这是启动时根据capacity-scheduler.xml的配置"mapred.capacity-scheduler.init-worker-threads"来决定的, 默认会启5个线程去为各个队列检查要初始化的job, 并初始化它.)
注: 当集群环境中job过多时, 可以增加此配置用适量的线程去执行作业初始化工作.
最后将队列queue所对应的未初始化的job都加入到JobInitializationThread线程中进行一一的初始化.
JobInitializationThread.run()代码:
public void run() {
while (startIniting) {
initializeJobs();
Thread.sleep(sleepInterval); //配置"mapred.capacity-scheduler.init-poll-interval"默认为3000.
}
}
在initializeJobs()中会遍历所有未初始化的job调用JT的initJob去初始化这个job.
setInitializingJob(job);
ttm.initJob(job); //TaskTrackerManager即为JobTracker.
1.4.2.2 初始化作业
TaskInProgress
TaskInProgress类维护了一个Task运行过程中的全部信息. 在hadoop中, 由于一个任务可能会推测执行或者重新执行.所以会存在多个TaskAttempt, 同一时刻可能有多个处理相同的任务尝试同时执行. 这些任务被同一个TaskInprogress对象管理和跟踪.
初始化它时主要的属性:
privatefinal TaskSplitMetaInfo splitInfo; //maptask要处理的split信息
privateJobInProgress job; //TaskInProgress所在的jobInprogress.
//正在运行的TaskID与TaskTrackerID之间的映射关系.
privateTreeMap<TaskAttemptID, String> activeTasks = newTreeMap<TaskAttemptID, String>();
//已运行的所有TaskAttemptID, 包括已运行完成的和正在运行的.
privateTreeSet<TaskAttemptID> tasks = new TreeSet<TaskAttemptID>();
//TaskID与TaskStatus的映射关系.
privateTreeMap<TaskAttemptID,TaskStatus> taskStatuses = newTreeMap<TaskAttemptID,TaskStatus>();
//cleanupTaskId与TaskTracker的对应关系
privateTreeMap<TaskAttemptID, String> cleanupTasks = newTreeMap<TaskAttemptID, String>();
//已经运行失败的task节点列表
privateTreeSet<String> machinesWhereFailed = new TreeSet<String>();
//待杀死的Task列表.
privateTreeMap<TaskAttemptID, Boolean> tasksToKill = newTreeMap<TaskAttemptID, Boolean>();
//等待被提交的TaskAttemp,
privateTaskAttemptID taskToCommit;
在线程JobInitializationThread运行环境中调用JT的initJob(job). 主要做了两件事:
1初始化job相关的Task
TaskSplitMetaInfo[] splits = createSplits(jobId);
numMapTasks = splits.length;
maps = new TaskInProgress[numMapTasks];
//"mapreduce.job.locality.wait.factor"
localityWaitFactor = conf.getFloat(LOCALITY_WAIT_FACTOR, DEFAULT_LOCALITY_WAIT_FACTOR);
this.reduces = new TaskInProgress[numReduceTasks];
//根据配置计算当mapTask完成多少个时开始调度reduceTask启动. 默认为5%.
completedMapsForReduceSlowstart = conf.getFloat("mapred.reduce.slowstart.completed.maps") * numMapTasks)
cleanup = new TaskInProgress[2];
setup = new TaskInProgress[2];
1.1MapTaskInprogress
从job的作业提交目录中读取splitMetainfo信息, 并将其解析成相关的TaskSplitMetaInfo[]数组, 并创建相应多的mapTaskInProgress实例分发这些split去准备执行. 即为每个MapTaskInProgress实例中都指定它将要处理的源数据.
1.2 ReduceTaskInprogress
然后根据MR的配置"mapred.reduce.tasks"创建reduceTaskInProgress实例.
1.3cleanupTaskInprogress和setupTaskInprogress.
会为map和reduce分别创建cleanup和setup类型的Task.
2通知调度器更新job.
判断初始化job相关Task之前与之后若状态不一致时, 通知调度器去更新这个job.
JobStatusChangeEvent event = newJobStatusChangeEvent(job, EventType.RUN_STATE_CHANGED, prevStatus,
newStatus);
synchronized (JobTracker.this) {
for(JobInProgressListener listener : jobInProgressListeners) {
listener.jobUpdated(event);
}
}
对于刚初始化完还没有得被调度器调度的job此时仍然牌PREP状态.