MapReduce調度與執行原理之作業初始化

前言:本文旨在理清在Hadoop中一個MapReduce作業(Job)在提交到框架後的整個生命週期過程,權作總結和日後參考,如有問題,請不吝賜教。本文不涉及Hadoop的架構設計,如有興趣請參考相關書籍和文獻。在梳理過程中,我對一些感興趣的源碼也會逐行研究學習,以期強化基礎。
作者:Jaytalent
開始日期:2013年9月9日
參考資料:【1】《Hadoop技術內幕--深入解析MapReduce架構設計與實現原理》董西成
                  【2】Hadoop 1.0.0 源碼
                            【3】《Hadoop技術內幕--深入解析Hadoop Common和HDFS架構設計與實現原理》蔡斌 陳湘萍
上一篇文章中,作業準備提交到JobTracker了。本文關注作業在提交到JobTracker後且在執行前經歷了哪些事情。
一個MapReduce作業的生命週期大體分爲5個階段【1】
1. 作業提交與初始化
2. 任務調度與監控
3. 任務運行環境準備
4. 任務執行
5. 作業完成
一、作業提交與初始化
 JobClient.submitJobInternal方法中,最後一步就是將作業提交到JobTracker:
 status = jobSubmitClient.submitJob(jobId, submitJobDir.toString(), jobCopy.getCredentials());
這個方法所屬的接口在上一篇文章中有所提及,其實現有兩個:JobTracker和LocalRunner。LocalRunner是用於執行本地作業的,當Hadoop配置爲本地模式時採用該類處理作業。我們關注JobTracker.submitJob方法。這裏多說一句,在JobClient對象初始化時有如下代碼:
 /**
   * Connect to the default {@link JobTracker}.
   * @param conf the job configuration.
   * @throws IOException
   */
  public void init(JobConf conf) throws IOException {
    String tracker = conf.get("mapred.job.tracker", "local");
    tasklogtimeout = conf.getInt(
      TASKLOG_PULL_TIMEOUT_KEY, DEFAULT_TASKLOG_TIMEOUT);
    this.ugi = UserGroupInformation.getCurrentUser();
    if ("local".equals(tracker)) {
      conf.setNumMapTasks(1);
      this.jobSubmitClient = new LocalJobRunner(conf);
    } else {
      this.jobSubmitClient = createRPCProxy(JobTracker.getAddress(conf), conf);
    }        
  }
可以看到,tracker變量是從mapred.job.tracker配置獲取值的,默認值爲字符串“local”。因此,如果沒有在配置文件配置這個值或者JobConf對象中沒有添加配置文件(通常爲mapred-site.xml)資源,jobSubmitClient就會使用LocalJobRunner進行初始化。
回到正題,JobTracker.submitJob方法會做如下工作:
a. 首先創建JobInProgress對象。這個是個非常重要的對象,它維護了作業的生命週期,可以跟蹤作業的運行狀態和進度。
    // Create the JobInProgress, do not lock the JobTracker since
    // we are about to copy job.xml from HDFS
    JobInProgress job = null;
    try {
      job = new JobInProgress(this, this.conf, jobInfo, 0, ts);
    } catch (Exception e) {
      throw new IOException(e);
    }
我們可以進入構造函數內部看看JobInProgress都有什麼內容。
this.jobtracker = jobtracker;
this.status = new JobStatus(jobId, 0.0f, 0.0f, JobStatus.PREP);
JobStatus對象也比較關鍵,它維護了作業的一些狀態信息如作業優先級,開始時間,map和reduce任務的進度等。
其他信息在後面分析JobTracker實現的時候再詳述。
b. 檢查用戶的作業提交權限。Hadoop以隊列爲單位管理作業和資源。一個用戶可以屬於一個或多個隊列,管理員可以配置用戶在某個隊列中的作業提交權限。
      // check if queue is RUNNING
      String queue = job.getProfile().getQueueName();
      if (!queueManager.isRunning(queue)) {
        throw new IOException("Queue \"" + queue + "\" is not running");
      }
      try {
        aclsManager.checkAccess(job, ugi, Operation.SUBMIT_JOB);
      } catch (IOException ioe) {
        LOG.warn("Access denied for user " + job.getJobConf().getUser()
            + ". Ignoring job " + jobId, ioe);
        job.fail();
        throw ioe;
      }
c. 檢查作業的內存使用量。用戶在配置文件中可以配置Map任務和Reduce任務的內存使用量,而這些值不能超過管理員所配置的最大使用量,否則作業提交就會失敗。
      // Check the job if it cannot run in the cluster because of invalid memory
      // requirements.
      try {
        checkMemoryRequirements(job);
      } catch (IOException ioe) {
        throw ioe;
      }
d. 調用調度器模塊,對作業進行初始化。具體過程如下
     // Submit the job
      JobStatus status;
      status = addJob(jobId, job);
在addJob方法中,作業首先被加入到已經提交的作業列表中,然後通知JobTracker所有的監聽器對象,當前作業被提交,並採取相應的行動。
    synchronized (jobs) {
      synchronized (taskScheduler) {
        jobs.put(job.getProfile().getJobID(), job);
        for (JobInProgressListener listener : jobInProgressListeners) {
          listener.jobAdded(job);
        }
      }
    }
其中,任務調度器taskScheduler對象是在JobTracker構造時創建出來的。調度器對象和JobTracker對象是互相包含的關係。
    // Create the scheduler
    Class<? extends TaskScheduler> schedulerClass
      = conf.getClass("mapred.jobtracker.taskScheduler",
          JobQueueTaskScheduler.class, TaskScheduler.class);
    taskScheduler = (TaskScheduler) ReflectionUtils.newInstance(schedulerClass, conf);
可以看出,Hadoop任務調度器時可插拔的模塊,調度器的類型通過配置文件獲得,並使用反射機制實例化。用戶可以通過繼承TaskScheduler類實現自己的調度器(由於做研究需要,本人實現了一個簡單的調度器,實現過程日後分享)。Hadoop默認的調度器爲JobQueueTaskScheduler,調度策略爲先進先出(FIFO)。
注意:JobTracker採用了典型的觀察者模式。TaskScheduler爲訂閱者,JobTracker爲發佈者,二者通過作業監聽器JobInProgressListener對象發生關係。當用戶自定義了一個調度器後,同時要自定義監聽器類。一個調度器對象可以包含若干個監聽器,調度器在初始化時,會將其所有的監聽器對象註冊到JobTracker,訂閱其發佈的消息。以JobQueueTaskScheduler爲例:
  public synchronized void start() throws IOException {
    super.start();
    taskTrackerManager.addJobInProgressListener(jobQueueJobInProgressListener);
    eagerTaskInitializationListener.setTaskTrackerManager(taskTrackerManager);
    eagerTaskInitializationListener.start();
    taskTrackerManager.addJobInProgressListener(
        eagerTaskInitializationListener);
  }
其中taskTrackerManager對象就是JobTracker,通過addJobInProgressListener方法註冊監聽器。這樣,當有JobTracker發現有作業被提交、更新或刪除時,就會通知訂閱者TaskScheduler,並調用相應的回調函數,如上面提到的listener.jobAdded方法。更多調度器的細節請關注後續文章。
作業初始化的工作就是由上述的EagerTaskInitializationListener監聽器對象實現的。在該監聽器內部有一個作業初始化管理線程在運行,該線程訪問一個初始化作業隊列,取出一個作業,並新開一個作業初始化線程執行JobTracker.initJob方法。
初始化的過程主要是根據作業信息創建該作業的任務(Task)。作業的任務包括四種:
1. Setup Task。該任務進行一些簡單工作,運行狀態設置爲setup。它在運行時會佔用slot。Map和Reduce Setup任務各有一個。
    // 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, 1);
    setup[0].setJobSetupTask();
    // setup reduce tip.
    setup[1] = new TaskInProgress(jobId, jobFile, numMapTasks,
                       numReduceTasks + 1, jobtracker, conf, this, 1);
    setup[1].setJobSetupTask();
2. Map Task。Map階段處理數據的任務。其創建過程如下:
    maps = new TaskInProgress[numMapTasks];
    for(int i=0; i < numMapTasks; ++i) {
      inputLength += splits[i].getInputDataLength();
      maps[i] = new TaskInProgress(jobId, jobFile, 
                                   splits[i], 
                                   jobtracker, conf, this, i, numSlotsPerMap);
    }
TaskInProgrees維護任務運行時信息,與JobInProgress類似。
3. Reduce Task。Reduce階段處理數據的任務。其創建過程如下:
    this.reduces = new TaskInProgress[numReduceTasks];
    for (int i = 0; i < numReduceTasks; i++) {
      reduces[i] = new TaskInProgress(jobId, jobFile, 
                                      numMapTasks, i, 
                                      jobtracker, conf, this, numSlotsPerReduce);
      nonRunningReduces.add(reduces[i]);
    }
用戶可以在配置文件中指定Reduce任務的個數。
4. Cleanup Task。 作業完成後完成一些清理工作的任務。清理包括刪除臨時目錄,設置狀態等操作。
    // 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.
    TaskSplitMetaInfo emptySplit = JobSplit.EMPTY_TASK_SPLIT;
    cleanup[0] = new TaskInProgress(jobId, jobFile, emptySplit, 
            jobtracker, conf, this, numMapTasks, 1);
    cleanup[0].setJobCleanupTask();
    // cleanup reduce tip.
    cleanup[1] = new TaskInProgress(jobId, jobFile, numMapTasks,
                       numReduceTasks, jobtracker, conf, this, 1);
    cleanup[1].setJobCleanupTask();
至此,作業初始化工作完成。接下來,調度器會根據當前可用的slot資源,從隊列中選擇一個作業,進而選擇該作業的一個任務,放到空閒slot上執行。四種任務執行的順序爲Setup、Map、Reduce和Cleanup。由於Reduce任務的輸入依賴於Map任務的輸出,因此Reduce任務通常延後開始,否則將閒置reduce slot。可以配置檔Map任務進度大於mapred.reduce.slowstart.completed.maps時,Reduce任務纔開始,該值默認爲5%。Map和Reduce任務這種依賴性在任務調度器設計中時常考慮。例如Facebook在提出FairScheduler的論文中就試圖解決這個問題。
另外,關於爲什麼JobTracker將初始化工作交給調度器處理,文獻【1】給出的理由是:
  • 作業初始化後會佔用內存資源,如果有大量初始化作業在JobTracker等待調度就會佔用不必要的資源。在交給調度器後,Hadoop按照一定策略選擇性地初始化以節省內存資源。
  • 只有經過初始化的作業才能得到調度,因此將初始化工作嵌入調度器中比較合理。
有關任務調度器內容詳見下一篇文章:MapReduce調度與執行原理之任務調度




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