JobTracker節點上的作業恢復RecoveryManager

       基於目前Hadoop的實現,在很多時候大家都會詬病於它的NameNode/JobTracker單點故障問題,特別是NameNode節點,一旦它發生了不可恢復的故障之後就意味着整個HDFS文件系統不在可用了。對於NameNode節點的單點故障問題,Hadoop目前採取的解決辦法是冷備份,就是在HDFS集羣中另外開啓一個SecondaryNameNode節點,這個節點會定期地對NameNode節點上的元數據進行備份(這一點請參看博文:HDFS中的SecondaryNameNode節點解析),缺點就是NameNode節點一旦崩潰或出錯,整個HDFS集羣將不得不停止運行,因爲它不會自動地切換到另一個備用的NameNode節點,當然HDFS集羣中目前也沒有這樣的節點。不過筆者在寫這篇博客的時候已經得知,Hadoop將會在下一個版本中針對這個問題引入Hadoop社區的HA解決方案來克服NameNode節點的SPOF問題,而且HDFS的集羣將可達到6000節點的規模。JobTracker節點的SPOF問題也類似於NameNode節點,唯一不同的是並不需要另外的一個單獨的節點來對JobTracker上的數據進行備份,因爲JobTracker節點上的重要數據都可以保存到一個分佈式文件系統上,如HDFS等。不過這裏有一個問題就是,如果JobTracker節點由於意外情況而宕機的話,那麼可能有一部分Job正在執行,也有一部分Job被用戶成功提交了可還沒有開始被調度執行,那麼當我們重啓JobTracker節點的時候就需要恢復或者重做這些還沒有完成的Job。所以,本文將重點講述JobTracker節點上的作業恢復管理器——RecoveryManager。

       對於Hadoop中的Map-Reduce集羣,配置有一個系統目錄,客戶端在向JobTracker節點提交一個Job之前會爲該Job在集羣的系統目錄下創建一個子目錄(子目錄的名字是Job的Id),然後會把該Job所需要的一切文件copy到它的目錄下(這一點我也在前面的博文Job的提交—客戶端中講過),這個系統目錄一般會在分佈式文件系統中,它可以通過JobTracker節點的配置文件來設置,對應的配置項爲:mapred.system.dir。如果一個Job被完成了,那麼這的Job在系統目錄下的數據將會被自動清除,而JobTracker節點在啓動的時候也會檢查系統目錄下有沒有子目錄(每一個子目錄對應一個沒有完成的Job),如果有沒完成的作業的話,這個Job會被提交到添加到RecoveryManager的作業緩存jobsToRecover中,以便被JobTracker節點恢復或重新執行。這裏要着重強調的是,RecoveryManager並不負責上一次未完成Job的調度執行,而只是恢復對這些作業的管理,即讓這些作業的任務狀態恢復到各自重啓之前的狀態。

    在RecoveryManager正式啓動恢復上一次沒完成的Job之前,會先幹一件事情,這件事就是往系統目錄下的一個文件中寫入一個數據,這個數據就是當前Map-Reduce集羣重啓的次數,而這個文件就是jobtracker.info。這裏要說的是RecoveryManager啓動對未完成Job的恢復是在JobTracker節點的主線程中完成的,而且是在JobTracker節點的所有後臺線程啓動之前,這個調用必須要在所有的未完成的Job被完成之後才返回。也就是說,JobTracker的作業恢復管理器在恢復作業的處理過程中,JobTracker節點不會接受客戶端的任何請求,也不接受TaskTracker的任何請求。

    RecoveryManager對作業的恢復(本質上是恢復作業的各個任務的執行狀態)依賴於作業對應的執行日誌文件,該日誌文件詳細的記錄了作業及其任務狀態變化的詳細信息。RecoveryManager通過對作業的日誌文件的解析即可恢復該作業的狀態到重啓之前的狀態了。每一個作業的日誌文件都作爲一個單獨的文件存儲在系統的作業歷史目錄下面,該目錄可以通過JobTracker節點的配置文件來設置,相應的配置項爲:hadoop.job.history.location。關於作業日誌文件內容的格式,我將在介紹作業日誌記錄器是詳細講述,而一個作業的日誌文件名的格式如下圖:


    在對作業的管理恢復過程中,RecoveryManager會記錄與這些恢復的作業任務相關聯的TaskTracker節點和Task實例。例如在回覆一個作業的某一個Task實例狀態的時候,它會記錄執行該Task實例的TaskTracker節點並將該節點放trackerExpiryQueue,同時也把該Task實例添加到JobTracker節點的任務監控對列expireLaunchingTasks中,如果該日誌文件中記錄了該Task實例已經完成了的話(無論成功還是失敗),就把該Task實例從expireLaunchingTasks中刪除。在RecoveryManager恢復了重啓之前未完成作業的管理之後,JobTracker節點就可以啓動其它的工作線程了,當然包括RPC服務組件。之後,JobTracker節點就可以接受用戶提交作業的請求、TaskTracker節點的心跳包,但是此時JobTracker節點並不會爲TaskTracker分配任何任務,除非被RecoveryManager標記過的TaskTracker節點在JobTracker節點重啓之後都向它註冊或發送過心跳包,總之JobTracker要明確地知道與本次恢復的作業相關的所有TaskTracker節點是活着還是掛了,對於還是活着的TaskTracker節點,JobTracker節點就可以確定在其上已執行完的Task是可用的,對於已經掛掉的TaskTracker節點(通過JobTracker節點的一個後臺工作線程來對trackerExpiryQueue進行監控),JobTracker就可以通知對應的JobInProgress和TaskInProgress該Task實例已經失效了。對於那些RecoveryManager認爲與本次恢復作業相關的正在TaskTracker節點上執行的Task實例,如果該Task實例確實正在對應的TaskTracker節點上執行,那麼該TaskTracker節點會不斷的向JobTracker節點報告該Task實例的執行狀態,而JobTracker節點在接收到該Task實例的狀態報告之後會將其從expireLaunchingTasks中刪除;如果該Task實例沒有在對應的TaskTracker節點上執行,那沒就沒有TaskTracker節點向JobTracker節點報告該Task實例的執行狀態,那麼該Task實例會被運行在JobTracker節點的一個後臺工作線程檢測出來,該後臺線程回認爲該Task實例已經執行失敗並將該情況通知給對應的JobInProgress來處理。

   其實,對於與本次恢復作業相關的已成功完成的Task實例的處理還有一個異常情況:如果一個被RecoveryManager認爲已經成功執行完某一個Task實例的TaskTracker節點在JobTracker節點重啓的時候自己也重啓了,那麼,已經在該TaskTracker節點上完成的Task實例失效了,而JobTracker節點是不會知道這個情況的而繼續認爲該Task實例是可用的。JobTracker節點雖然不會馬上知道這一情況,但它最終還是會知道的,因爲,如果該Task實例是Reduce型的,那麼已執行完的Reduce任務實例不受TaskTracker節點的影響;如果該該Task實例是Map型的,那麼當對應的Reduce任務是無法抓取該Task實例的輸出數據的,從而Reduce任務實例會向JobTracker節點報告而知道這一異常情況了。

   從JobTracker節點對作業的恢復處理來看,它是特別耗時間的(主要用在等待與恢復作業任務相關的TaskTracker節點的unmark上),所以對於短作業居多的應用場景,筆者並不贊同開啓RecoveryManager,而對於長作業居多的應用場景來說,開始RecoveryManager還是值得的。關於開啓/關閉RecoveryManager,可以通過JobTracker節點的配置文件來設置,對應的配置項爲:mapred.jobtracker.restart.recover啓動未完成Job的恢復工作其實也很簡單,不做詳細的討論,它的源碼如下:

public void recover() {
    if (!shouldRecover()) {
      // clean up jobs structure
      jobsToRecover.clear();
      return;
    }

    LOG.debug("Restart count of the jobtracker : " + restartCount);

    // I. Init the jobs and cache the recovered job history filenames
    Map<JobID, Path> jobHistoryFilenameMap = new HashMap<JobID, Path>();
    Iterator<JobID> idIter = jobsToRecover.iterator();
    while (idIter.hasNext()) {
      JobID id = idIter.next();
      LOG.info("Trying to recover Job[" + id + "]...");
      try {
        // 1. Create the job object
      	LOG.debug("creating a JobInProgress for Job["+id+"]..");
        JobInProgress job = new JobInProgress(id, JobTracker.this, conf, restartCount);

        // 2. Check if the user has appropriate access Get the user group info for the job's owner
        UserGroupInformation ugi = UserGroupInformation.readFrom(job.getJobConf());
        LOG.debug("User["+ugi.getUserName()+":"+StringUtils.arrayToString(ugi.getGroupNames())+"] submit the Job["+id+"].");
        
        // check the access
        try {
      	  LOG.debug("checking whether User["+ugi.getUserName()+":"+StringUtils.arrayToString(ugi.getGroupNames())+"] is able to submit the Job["+id+"]");
          checkAccess(job, QueueManager.QueueOperation.SUBMIT_JOB, ugi);
        } catch (Throwable t) {
          LOG.warn("Access denied for user " + ugi.getUserName() + " in groups : [" + StringUtils.arrayToString(ugi.getGroupNames()) + "]");
          throw t;
        }

        // 3. Get the log file and the file path
        String logFileName = JobHistory.JobInfo.getJobHistoryFileName(job.getJobConf(), id);
        if (logFileName != null) {
          Path jobHistoryFilePath = JobHistory.JobInfo.getJobHistoryLogLocation(logFileName);

          // 4. Recover the history file. This involved
          //     - deleting file.recover if file exists
          //     - renaming file.recover to file if file doesnt exist
          // This makes sure that the (master) file exists
          JobHistory.JobInfo.recoverJobHistoryFile(job.getJobConf(), jobHistoryFilePath);
        
          // 5. Cache the history file name as it costs one dfs access
          jobHistoryFilenameMap.put(job.getJobID(), jobHistoryFilePath);
        } else {
          LOG.info("No history file found for job " + id);
          idIter.remove(); // remove from recovery list
        }

        // 6. Sumbit the job to the jobtracker
        addJob(id, job);
      } catch (Throwable t) {
        LOG.warn("Failed to recover job " + id + " Ignoring the job.", t);
        idIter.remove();
        continue;
      }
    }

    long recoveryStartTime = System.currentTimeMillis();

    // II. Recover each job
    idIter = jobsToRecover.iterator();
    while (idIter.hasNext()) {
      JobID id = idIter.next();
      JobInProgress pJob = getJob(id);

      // 1. Get the required info
      // Get the recovered history file
      Path jobHistoryFilePath = jobHistoryFilenameMap.get(pJob.getJobID());
      String logFileName = jobHistoryFilePath.getName();

      FileSystem fs;
      try {
        fs = jobHistoryFilePath.getFileSystem(conf);
      } catch (IOException ioe) {
        LOG.warn("Failed to get the filesystem for job " + id + ". Ignoring.", ioe);
        continue;
      }

      // 2. Parse the history file
      // Note that this also involves job update
      JobRecoveryListener listener = new JobRecoveryListener(pJob);
      try {
        JobHistory.parseHistoryFromFS(jobHistoryFilePath.toString(), listener, fs);
      } catch (Throwable t) {
        LOG.info("Error reading history file of job " + pJob.getJobID() + ". Ignoring the error and continuing.", t);
      }

      // 3. Close the listener
      listener.close();
      
      // 4. Update the recovery metric
      totalEventsRecovered += listener.getNumEventsRecovered();

      // 5. Cleanup history
      // Delete the master log file as an indication that the new file
      // should be used in future
      try {
        synchronized (pJob) {
          JobHistory.JobInfo.checkpointRecovery(logFileName,pJob.getJobConf());
        }
      } catch (Throwable t) {
        LOG.warn("Failed to delete log file (" + logFileName + ") for job " + id + ". Continuing.", t);
      }

      if (pJob.isComplete()) {
        idIter.remove(); // no need to keep this job info as its successful
      }
    }

    recoveryDuration = System.currentTimeMillis() - recoveryStartTime;
    hasRecovered = true;

    // III. Finalize the recovery
    synchronized (trackerExpiryQueue) {
      // Make sure that the tracker statuses in the expiry-tracker queue
      // are updated
      long now = System.currentTimeMillis();
      int size = trackerExpiryQueue.size();
      for (int i = 0; i < size ; ++i) {
        // Get the first status
        TaskTrackerStatus status = trackerExpiryQueue.first();

        // Remove it
        trackerExpiryQueue.remove(status);

        // Set the new time
        status.setLastSeen(now);

        // Add back to get the sorted list
        trackerExpiryQueue.add(status);
      }
    }

    LOG.info("Restoration complete");
  }



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