**Jobclient 准备运行环境
Jobtracker 接收作业
Taskscheduler 初始化作业**
作业提交:
总体来言,作业提交还是比较简单的,主要涉及创建目录、上传文件等操作;一旦用户提交了作业以后,Jobtracker端便会对作业进行初始化,初始化的作业主要是根据输入数据量和作业的配置参数将作业分解成若干个map task 和reduce task
整个过程:
用户使用hadoop提供的shell命令提交作业
Jobclient 按照作业配置的信息将作业运行需要的全部文件上传到Jobtracker文件系统(一般是HDFS)中
Jobclient 调用RPC接口向Jobtracker提交作业
Jobtracker接收到作业以后,将其告知TashScheduler,有TaskScheduler对作业进行初始化。
作业提交过程
1、执行shell命令
当我们提交hadoop jar xxx.jar 参数,命令以后,通过命令文件可以看到底层调用的RunJar类处理,
elif [ "$COMMAND" = "jar" ] ; then
CLASS=org.apache.hadoop.util.RunJar
RunJar类调用这个main方法以后 直接运行了run(args)方法,这个方法解压jar包和设置环境变量,将运行的参数传递给mapreduce程序,并运行。
RunJar方法的部分代码:取到运行的类和需要执行的main方法。通过invoke 方法提交给mapreduce运行框架
ClassLoader loader = createClassLoader(file, workDir);
Thread.currentThread().setContextClassLoader(loader);
Class<?> mainClass = Class.forName(mainClassName, true, loader);
Method main = mainClass.getMethod("main", new Class[] {
Array.newInstance(String.class, 0).getClass()
});
String[] newArgs = Arrays.asList(args)
.subList(firstArg, args.length).toArray(new String[0]);
try {
main.invoke(null, new Object[] { newArgs });
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
RunJar的main方法
publicstaticvoid main(String[] args) throws Throwable {
new RunJar().run(args);
}
用户的mapreduce程序已经配置了作业运行时需要的各种信息(如Mapper类,Reducer类,Reducer task数量等),它最终在main函数中调用job.waitForCompletion(true)函数(老的API使用JobClient.runJob函数)提交作业,这之后会依次经过以下的过程才到Jobtracker中
2、 作业文件上传
Jobclient将作业提交到JobTracker端之前,需要进行一些初始化的工作,包括:获取作业的ID,创建HDFS目录,上传作业文件以及生成split文件等,这些工作由函数JobClient.submitJobInternet(Job)实现,
MapReduce作业文件的上传与下载是由DistributedCache工具完成的,它是hadoop为方便用户进行应用程序开发而设计的数据分发工具,其整个工作流程对用户而言是透明的,也就是说,用户只需在提交作业时指定文件位置,至于这些文件的分发,完全由DistributedCache工具完成,不需要用户参与。
通常情况下,一个java Mapreduce作业,可能需要的资源有:
2、1: 程序jar包:用户编写的Mapreduce应用程序jar包
2、2: 作业配置文件:描述Mapreduce应用程序的配置信息(根据JobConf对象生成的xml文件) JobConf就是给作业配置信息赋值的类
2、3: 依赖的第三方的jar包:应用程序依赖的第三方的包,提交作业时用参数”-libjars”指定
2、4: 依赖的归档文件:应用程序中用到多个文件,可直接打包成归档文件,提交作业用参数”-archives”指定
2、5: 依赖的普通文件:应用程序中可能用到普通文件,比如文本格式的字典文件,提交作业时用参数”-files”指定
注意:应用程序依赖的文件可以存放到本地磁盘中,也可以存放到hdfs中,默认情况下面是存放在本地磁盘上的,-libjars=third-party.jar这个指的是本地磁盘上,如果用-libjars=hdfs:///data/third-party.jar这个是指定在hdfs上的DistributedCache将文件分为两种可见级别,分别是private和public级别。其中private级别文件只会被当前用户使用,不能与其他用户共享;而public级别文件则不同,它们在每个节点上保存一份,可被本节点上的所有作业和用户共享,这样可以大大降低文件复制代价,提高了作业运行的效率。一个文件/目录要成为public级别文件/目录,需同时满足以下2个条件:
1、1:该文件/目录对所有用户/用户组均有可读权限
1、2:该文件/目录的父目录、父目录的父目录……对所有用户/用户组有可执行权限
作业文件上传到hdfs后,可能会有大量节点同时从hdfs下载这些文件,从而产生文件访问热点现象,造成性能瓶颈。为此,JobClient上传这些文件时会调高它们的副本数(由参数 mapred.submit.replication指定,默认是10)以通过分摊负载方式避免产生访问热点
3、 产生InputSplit文件
用户提交Mapreduce作业后,JobClient会调用对应输入流(我们这里以FileInputFormat为例)的getSplit()方法生成InputSplit相关信息,其实FileInputFormat底层是实现了InputFormat的getSplits方法,该信息包括了2个部分:InputSplit元数据信息和原始InputSplit信息。其中的第一部分将被JobTracker使用,用以生成Task本地性(Task Locality)相关的数据结构;而第二部分则将被Map Task初始化时使用,用以获取自己需要处理的数据。这2部分信息分别被保存到目录mapreduce.jobtracker.staging.root.dir/{user}/.staging/${JobId}下面的文件job.splitmetainfo和job.split中InputSplit相关的操作放在包org.apache.hadoop.mapreduce.split中,主要包含3个类JobSplit、JobSplitWriter、SplitMetaInfoReader
JobSplit封装了读写InputSplit相关的基础类,主要包括以下三个:
3、1:JobSplit.SplitMetaInfo:描述一个InputSplit的元数据信息,包括以下三项内容:
privatelong startOffset;//该InputSplit元信息在Job.split文件中的偏移量
privatelong inputDataLength;//该InputSplit的数据长度
private String[] locations;//该InputSplit所在的host列表
所有InputSplit对应的SplitMetaInfo将被保存到文件job.SplitMetaInfo中,该文件内容组织方式如图,依次是:一个用于标识InputSplit元数据文件头部的字符串”META-SPL”,文件版本号splitVersion(当前默认值是1),作业对应的InputSplit数目length,最后是length个InputSplit对应的SplitMetaInfo信息。
3、2:JobSplit.TaskSplitMetaInfo:用于保存InputSplit元信息的数据结构,包括以下三项内容::
private TaskSplitIndex splitIndex;//Split元信息在Job.split文件中的位置
privatelong inputDataLength;//InputSplit的数据长度
private String[] locations;//InputSplit所在的host列表
这些信息是在作业初始化时,JobTracker从文件job.splitmetainfo中获取,其中host列表信息是任务调度器判断任务是否具有本地性的最重要因素,而splitIndex信息保存了新任务需处理的数据位置信息在文件job.split中的索引,TaskTracker(从JobTracker端)收到该信息后,便可以从job.split文件中读取InputSplit信息,进而运行一个新任务。
3、3:JobSplit.TaskSplitIndex:JobTracker向TaskTracker分配新任务时,TaskSplitIndex用于指定新任务待处理数据位置信息在文件Job.split中的索引,主要包括两项内容:
private String splitLocation;//job.split文件的位置
privatelong startOffset;//InputSplit信息在job.split文件中的位置
举例:
Job.Split文件内容
FileSplit={file=hdfs://server1:9000/user/admin/in/yellow.txt,hosts= [server3, server2], length=67108864,start=0}
FileSplit={file=hdfs://server1:9000/user/admin/in/yellow.txt,hosts= [server3, server2], length=67108864,start= 67108864}
FileSplit={file=hdfs://server1:9000/user/admin/in/yellow.txt,hosts= [server3, server2], length= 66782272,start= 134217728}
Job.SplitMetaInfo文件
JobSplitSplitMetaInfo=data−size:67108864,start−offset:7,locations:[server3,server2]JobSplitSplitMetaInfo ={data-size : 67108864,start-offset : 116,locations :[server3, server2]}
JobSplit$SplitMetaInfo ={data-size : 66782272,start-offset : 225,locations :[server3, server2]}JobTracker转换SplitMetaInfo 为TaskSplitMetaInfo 信息
TaskSplitMetaInfo[0]={ inputDataLength=67108864, locations=[server3, server2], splitIndex=JobSplitTaskSplitIndex{splitLocation=”hdfs://server1:9000/tmp/hadoop-admin/mapred/staging/admin/.staging/job_201404200521_0001/job.split” , startOffset=7}}
TaskSplitMetaInfo[1]={ inputDataLength=67108864, locations=[server3, server2], splitIndex=JobSplitTaskSplitIndex{splitLocation=”hdfs://server1:9000/tmp/hadoop-admin/mapred/staging/admin/.staging/job_201404200521_0001/job.split” , startOffset= 116}}
TaskSplitMetaInfo[2]={ inputDataLength= 66782272, locations=[server3, server2], splitIndex=JobSplit$TaskSplitIndex{splitLocation=”hdfs://server1:9000/tmp/hadoop-admin/mapred/staging/admin/.staging/job_201404200521_0001/job.split” , startOffset=225}}
4、 作业提交到JobTracker
JobClient最终调用RPC方法submitJob将作业提交到JobTracker端,在JobTracker.submitJob中,会做如下的操作
4、1: 为作业创建一个JobInProgress对象JobTracker会为每一个作业创建一个JobInProgress对象,该对象维护了作业的运行时信息,它在作业运行过程中一直存在,主要 用于跟踪正在运行作业的运行状态和进度。
4、2: 检查用户是否具有指定队列的作业提交权限,Hadoop以队列为单位管理作业和资源,每个队列分配有一定量的资源,每个用户属于一个或者多个队列且只能使用所属队列中的资源。
4、3: 用户提交作业时,可分别用参数Mapred.job.map.memory.mb和mapred.job.reduce.memory.mb指定map task 和Reduce task占用内存量,而管理员可通过参数mapred.cluster.max.map.memory.mb和mapred.cluster.max.reduce.memory.mb限制用户配置的任务 最大的内存使用量,一旦用户配置的内存使用量超过系统限制,则提交作业失败。
4、4: 通知TaskScheduler初始化作业,JobTracker接收到作业后,并不会马上对其初始化,而是交给调度器,由它按照一定的策略对作业进行初始化,之所以不选择JobTracker而让调度器初始化,主要有2个因素:
4、4、1:作业一旦初始化后就需要占用一定量的内存资源,为了防止大量初始化的作业排队等待调度而占用大量不必要的内存资源,hadoop按照一定的策略选择性的初始化作业以节省内存资源
4、4、2:任务调度器的职责是根据每个节点的资源使用情况对其分配最合适的任务,而只有经过初始化的作业才有可能得到调度,因而将作业初始化策略嵌到调度器中是一种比较合理的设计
Hadoop的调度器是一个可插拨模块,用户可通过实现TaskScheduler接口设计自己的调度器,当前Hadoop默认的调度器是JobQueueTaskScheduler,它采用的调度策略是先来先服务(First In First Out FIFO)。另外还有2个常用的调度器,Fair Scheduler 和Capacity Scheduler。
JobTracker采用了观察者设计模式(发布-订阅模式)将“提交新作业”这一事件告诉TaskScheduler。
JobTracker采用观察者设计模式将作业变化(添加/删除/更新作业)通知TaskScheduler。
JobTracker启动时会 根据配置参数mapred.jobtracker.taskscheduler构造相应的任务调度器,并调用它的start()方法进行初始化。在该方法中,调度器会向JobTracker注册JobInProgressListener对象以监听作业的添加/删除/更新等事件。以默认调度JobQueueTaskScheduler为例,它的start方法是:
public synchronized void start() throws IOException {
super.start();
// taskTrackerManager就是JobTracker的对象,向JobTracker注册一个//jobQueueJobInProgressListener
taskTrackerManager.addJobInProgressListener(jobQueueJobInProgressListener); eagerTaskInitializationListener.setTaskTrackerManager(taskTrackerManager);
eagerTaskInitializationListener.start(); //向JobTracker注册eagerTaskInitializationListener
taskTrackerManager.addJobInProgressListener(eagerTaskInitializationListener);}
JobQueueTaskScheduler向JobTracker注册了2个JobInProgressListener: jobQueueJobInProgressListener和eagerTaskInitializationListener它们分别是用于作业排序和作业初始化的,JobTracker创建实例时候加载调度器。
public static JobTracker startTracker(JobConf conf, String identifier, boolean initialize)
throws IOException, InterruptedException {
......
result = new JobTracker(conf, identifier);//创建唯一JobTracker实例
result.taskScheduler.setTaskTrackerManager(result);//将JobTracker实例传递给TaskScheduler
......
}