Job的初始化—EagerTaskInitializationListener

来源:互联网 发布:网络编程教程 编辑:程序博客网 时间:2024/06/08 09:09

       客户端用户在向JobTracker节点成功地提交了一个Job之后,JobTracker节点并不会马上对这个作业进行调度,因为任何作业在被任务调度器TaskSchedule调度之前,都必须先被初始化,这一点笔者曾经在讲解Hadoop的默认调度器——JobQueueTaskSchedule时已经详细地谈到过了。而本文将要集中讨论的是Job是如何被初始化的,同时,Job的初始化又干了哪些事情。

       对于Job的初始化,JobTracker采用了Observer的设计模式,也就是说JobTracker并没有在用户的响应线程中对该用户提交的Job进行初始化处理,而是交给了一个Listener来做,JobTracker节点之所以会这样设计,是因为Job的初始化处理会影响用户的响应时间(Job在初始化是会从DFS上本地化与该Job相关的文件)。这个过程其实很简单,就是当一个用户提交了一个Job之后,JobTracker会把这个Job的初始化工作结果一个后台线程来做,而主线程可以直接返回了,即可结束对用户的提交Job请求处理。这个后台线程就是EagerTaskInitializationListener。

    先来看看与EagerTaskInitializationListener相关的类吧!

     JobTracker节点把所有成功提交的Job都交给了EagerTaskInitializationListener来初始化,所以为了提高自身初始化Job的吞吐量,EagerTaskInitializationListener在其内部设计了多线程的方式,每一个Job都会用一个线程来为它初始化,但是考虑到JobTracker节点系统资源的限制,它又折中的选择了线程池。EagerTaskInitializationListener内部默认的线程池大下小为4,不过也可以通过JobTracker的配置文件来设置,对应的配置项为:mapred.jobinit.threads。对于频繁提交小型作业的应用系统来说,适当地增大线程池的容量可以有效的提供高系统的工作效率。这个简单的过程如下图:

    那么,Job的初始化工作主要是对该Job做哪些初始化工作呢?这个初始化工作主要是加载Job对应的split文件,因为这个split文件包含了作业的输入数据的切片,而每一个数据切片又对应的一个Map任务,请注意,这个split文件是存储在Map-Reduce所以来的分布式文件系统上的,所以前面说Job的初始化会耗费一些时间。根据split文件中的数据切片数量以及每一个切片所在的存储节点的位置,就可以创建该Job的所有Map任务了,创建了Map任务之后顺带把Reduce任务也给创建了,不过除了Job真正的Map/Reduce任务之外,一个Job还包括额外的四个任务:Setup/Cleanup Map/Reduce Task。Setup Map/Reduce Task任务分别用来启动Map/Reduce任务,即为真正的Map/Reduce任务的执行做准备,如为这些任务创建数据存放目录;Cleanup Map/Reduce Task任务被分别用来清除与Map/Reduce任务相关的数据和目录。这个Job初始化的源代码如下:

public synchronized void initTasks() throws IOException, KillInterruptedException {    //作业已经被初始化了或者已经被完成了    if (tasksInited.get() || isComplete()) {       LOG.debug("Job["+jobId+"] has been inited or compelted, so return");      return;    }        synchronized(jobInitKillStatus){      if(jobInitKillStatus.killed || jobInitKillStatus.initStarted) {        return;      }      jobInitKillStatus.initStarted = true;    }    LOG.info("Initializing " + jobId);    // log job info    JobHistory.JobInfo.logSubmitted(getJobID(), conf, jobFile.toString(), this.startTime, hasRestarted());    // log the job priority    setPriority(this.priority);        // 加载作业的split文件    String jobFile = profile.getJobFile();    Path sysDir = new Path(this.jobtracker.getSystemDir());    FileSystem fs = sysDir.getFileSystem(conf);    Path _splitFile= new Path(conf.get("mapred.job.split.file"));    DataInputStream splitFile = fs.open(_splitFile);    JobClient.RawSplit[] splits;    try {      splits = JobClient.readSplitFile(splitFile);    } finally {      splitFile.close();     }    numMapTasks = splits.length;//map任务的数量    // if the number of splits is larger than a configured value then fail the job.    int maxTasks = jobtracker.getMaxTasksPerJob();    if (maxTasks > 0 && numMapTasks + numReduceTasks > maxTasks) {      throw new IOException("The number of tasks for this job " + (numMapTasks + numReduceTasks) + " exceeds the configured limit " + maxTasks);    }    jobtracker.getInstrumentation().addWaiting(getJobID(), numMapTasks + numReduceTasks);    //创建作业的Map任务    maps = new TaskInProgress[numMapTasks];    for(int i=0; i < numMapTasks; ++i) {      inputLength += splits[i].getDataLength();      maps[i] = new TaskInProgress(jobId, jobFile, splits[i],jobtracker, conf, this, i);    }    LOG.info("Input size for job " + jobId + " = " + inputLength + ". Number of splits = " + splits.length);    if (numMapTasks > 0) {       nonRunningMapCache = createCache(splits, maxLevel);    }            //设置作业运行的开始时间    this.launchTime = System.currentTimeMillis();    // 创建作业的Reduce任务    this.reduces = new TaskInProgress[numReduceTasks];    for (int i = 0; i < numReduceTasks; i++) {      reduces[i] = new TaskInProgress(jobId, jobFile, numMapTasks, i, jobtracker, conf, this);      LOG.debug("creae a Reduce Task["+reduces[i].getTIPId()+"] for Job["+jobId+"].");      nonRunningReduces.add(reduces[i]);    }    // Calculate the minimum number of maps to be complete before     // we should start scheduling reduces    completedMapsForReduceSlowstart = (int)Math.ceil((conf.getFloat("mapred.reduce.slowstart.completed.maps", DEFAULT_COMPLETED_MAPS_PERCENT_FOR_REDUCE_SLOWSTART) * numMapTasks));    // create cleanup two cleanup tips, one map and one reduce.    cleanup = new TaskInProgress[2];    // cleanup map tip. This map doesn't use any splits. Just assign an empty    // split.    JobClient.RawSplit emptySplit = new JobClient.RawSplit();    cleanup[0] = new TaskInProgress(jobId, jobFile, emptySplit, jobtracker, conf, this, numMapTasks);    cleanup[0].setJobCleanupTask();    LOG.debug("create a Cleanup Map Task["+cleanup[0].getTIPId()+"] for Job["+jobId+"]");    // cleanup reduce tip.    cleanup[1] = new TaskInProgress(jobId, jobFile, numMapTasks,numReduceTasks, jobtracker, conf, this);    cleanup[1].setJobCleanupTask();    LOG.debug("create a Cleanup Reduce Task["+cleanup[1].getTIPId()+"] for Job["+jobId+"]");    // create two setup tips, one map and one reduce.    setup = new TaskInProgress[2];    // setup map tip. This map doesn't use any split. Just assign an empty    // split.    setup[0] = new TaskInProgress(jobId, jobFile, emptySplit, jobtracker, conf, this, numMapTasks + 1 );    setup[0].setJobSetupTask();    LOG.debug("create a Setup Map Task["+setup[0].getTIPId()+"] for Job["+jobId+"]");    // setup reduce tip.    setup[1] = new TaskInProgress(jobId, jobFile, numMapTasks, numReduceTasks + 1, jobtracker, conf, this);    setup[1].setJobSetupTask();    LOG.debug("create a Setup Reduce Task["+setup[1].getTIPId()+"] for Job["+jobId+"]");        synchronized(jobInitKillStatus){      jobInitKillStatus.initDone = true;      if(jobInitKillStatus.killed) {        throw new KillInterruptedException("Job " + jobId + " killed in init");      }    }        tasksInited.set(true);//标记该作业已经初始化了    JobHistory.JobInfo.logInited(profile.getJobID(), this.launchTime, numMapTasks, numReduceTasks);  }
      从上面的源码可以看出,作业的Map任务的数量是由输入数据的切片数量决定的,而Reduce任务的数量是由用户在提交Job之前决定的。但是,一个Job的作业总数量(这里指真正Map/Reduce的任务和)不能超过JobTracker节点规定的上限,这个上限值可以由配置文件来设置,对应的配置项为:mapred.jobtracker.maxtasks.per.job,负数值表示无穷大。不过,在Job的初始化过程中还要注意的一个问题是,如果作业有Map的任务的话,在创建创建Reduce任务之前还会调用createCache()方法,那么这个方法是用来干嘛的呢?这个方法用来预分配作业Map任务的本地调度范围,这个本地范围与JobTracker的numTaskCacheLevels配置有关。该操作之所以放到Job的初始化中完成,主要是为了提高系统不过效率,因为集群中的绝大多数Job中的Map任务都是在等待被调度给TaskTracker节点执行的,不可能说在Job被第一次调度时再执行这个操作。但这里的关键问题是如何对作业中的每一个Map任务进行预分配的。举个例子,JobTracker的numTaskCacheLevels配置为2,假设一个Map任务对应的输入数据片有三个副本,分别存储在在数据节点/network1/rack1/datanode1、/network1/rack1/datanode2、/network1/rack2/datanode4上,那么把这个Map任务分配给在网络拓扑路径在/network1/rack1/datanode1、/network1/rack1/datanode2、/network1/rack2/datanode4/network1/rack1//network1/rack2/上的TaskTracker节点时就称作是给这个TaskTracker节点分配了一个本地任务;反之,如果这个Map任务被分配给了不在上面拓扑路径下的TaskTracker节点的话,就说给这个TaskTracker节点分配了一个非本地任务。另外,numTaskCacheLevels的值可以通过配置文件的mapred.task.cache.levels项来设置,而且,当TaskTracker节点向JobTracker节点注册时,如果这个在TaskTracker节点在解析其拓扑路径后,其层次数小于numTaskCacheLevels的话,JobTracker节点将会自动停止一切服务。