Job的初始化—EagerTaskInitializationListener

       客户端用户在向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节点将会自动停止一切服务。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章