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節點將會自動停止一切服務。

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