Client提交作業與作業在JT中的初始化流程分析

作業提交與初始化主要是爲後續的MR程序運行做準備工作.

將其分爲四個步驟即1配置MR作業環境, 2上傳作業信息,  3提交作業, 4作業初始化.

下面將分別介紹以上四個步驟

1.1設置作業環境

這一步就是我們在MR中經常要用到的那些配置. 配置保證MR基本運行的參數.

以wordcount爲例配置如下:

      Configuration conf = new Configuration();
 
      Job job = new Job(conf);
      // 當我們的應用打包成jar,在hadoop中執行時,必須有該行代碼
      job.setJarByClass(WordCountApp.class);
 
      // 告訴job執行我們自定義的Mapper類
      job.setMapperClass(WordCountMapper.class);
     
      job.setCombinerClass(IntSumReducer.class);
      //job.setPartitionerClass(HashPartitioner.class);
      // 告訴job執行我們自定義的Reducer類
      job.setReducerClass(WordCountReducer.class);
 
      // 告訴job,k3是什麼類型
      job.setOutputKeyClass(Text.class);
      // 告訴job,v3是什麼類型
      job.setOutputValueClass(IntWritable.class);
     
      // 告訴job, 將要執行的Reduce數量.
      job.setNumReduceTasks(4);
     
      // 告訴job輸入源在哪裏;輸入可以是多個文件
      FileInputFormat.addInputPath(job, new Path(BaseConf.URI_HDFS_PREFIX
            + "/test/wd/input"));
     
      // 告訴job輸出路徑在哪裏;
      FileOutputFormat.setOutputPath(job, new Path(BaseConf.URI_HDFS_PREFIX
            + "/test/wd/output"));
 
      job.waitForCompletion(true);

當然針對一個MR還得很多可用的配置, 在此採用默認配置, 可以關注JobConf.java中所提供的set接口

這裏重點關注於整個架構的執行過程來去理解運行原理.

上面的設置都是通過job的成員jobConf去完成的.

1.1.2提交前的準備工作

 public void submit() throws IOException, InterruptedException,
                              ClassNotFoundException {
    ensureState(JobState.DEFINE); //確認作業未運行.
    setUseNewAPI();  //根據配置"mapred.mapper.new-api"與"mapred.reducer.new-api"設置是否使用了新的MR API
   
    // Connect to the JobTracker and submit the job
    connect();
    info = jobClient.submitJobInternal(conf);
    super.setJobID(info.getID());
    state = JobState.RUNNING;
   }
1.1.3連接到JT

     利用java安全的存取控制器去檢查安全策略,是否允許連接到JT.

     這會通過創建JobClient實例, 並通過RPC連接到JT.

     JobSubmissionProtocolrpcJobSubmitClient =  (JobSubmissionProtocol)RPC.getProxy(JobSubmissionProtocol.class,…

1.2 上傳作業信息

1> 從JT獲取HDFS上保存作業的目錄

     Path jobStagingArea =JobSubmissionFiles.getStagingDir(JobClient.this, jobCopy);

     默認爲${"mapreduce.jobtracker.staging.root.dir"}/tmp/hadoop/mapred/staging/${user}

     2>分配JobId.

         JobIDjobId = jobSubmitClient.getNewJobId();

     3>指定提交目錄

         PathsubmitJobDir = new Path(jobStagingArea, jobId.toString()); //權限默認爲700

     4>保存作業信息到submitJobDir

         copyAndConfigureFiles(jobCopy,submitJobDir);

         在其中會在submitJobDir中創建並拷貝指定文件到指定目錄

         submitJobDir/archives/對應啓動參數-archives指定的文件

         submitJobDir/files/    對應啓動參數-files指定文件.

         submitJobDir/libjars/  保存依賴的第三方jar文件.

         submitJobDir/job.jar   對應將要運行的MR程序jar文件.

     5>計算InputSplit信息並上傳

         intmaps = writeSplits(context, submitJobDir);

              ->InputFormat<?,?> input = ReflectionUtils.newInstance(job.getInputFormatClass(), conf);

              ->List<InputSplit>splits = input.getSplits(job);

              -> JobSplitWriter.createSplitFiles(jobSubmitDir,conf, jobSubmitDir.getFileSystem(conf), array);

         通過配置中設置的InputFormat類的getSplits計算輸入文件的InputSplit信息.並將其上傳到如下兩個文件中:

         submitJobDir/job.split

         submitJobDir/job.splitmetainfo

         然後返回map數.

         splitmetainfo信息中包含了

1.   job.split文件的位置

2.   InputSplit在job.split文件中的位置

3.   InputSplit的數據長度

4.   InputSplit所在的host列表.

     6>將jobConf中的配置信息導出到HDFS上的submitJobDir/job.xml中.

          jobCopy.writeXml(out);

1.3提交作業

1.3.1提交作業

       通過JT在客戶端的代理對象提交作業到JT.

       status= jobSubmitClient.submitJob( jobId, submitJobDir.toString(),jobCopy.getCredentials());

當JT收到客戶端的提交作業submitJob執行如下圖:


1.3.1.1創建JobInProgress監控作業

jobInfo = new JobInfo(jobId, newText(ugi.getShortUserName()),  newPath(jobSubmitDir));

job = new JobInProgress(this, this.conf,jobInfo, 0, ts);

在JobInprogress初始時會根據HDFS中將作業信息job.xml創建對應的conf對象, 並根據其設置JobInprogress相關的信息.

同時還會創建幾個重要的集合用於去保存作業運行時的一些作業信息.

      //不需要考慮數據本地性的mapTask,
    //如果MapTask的InputSplit.location[]爲空, 則在任務調度時在此集合中不會考慮數據本地性
     this.nonLocalMaps = new LinkedList<TaskInProgress>();
     //這是一個按照失敗次數進行排序TIP集合, 並指定了排序規則failComparator.
     this.failedMaps = new TreeSet<TaskInProgress>(failComparator);
     //還沒有運行的Task集合.
     this.nonLocalRunningMaps = new LinkedHashSet<TaskInProgress>();
     //正在運行中的MapTask集合,
     this.runningMapCache = new IdentityHashMap<Node,Set<TaskInProgress>>();
     //未運行的Reduce集合, 按照failComparator排序, (這難道是說失敗的reduce會重新加入到未運行的reduce集合中麼?)
     this.nonRunningReduces = newTreeSet<TaskInProgress>(failComparator);
     //正在運行的reduceTask集合
     this.runningReduces = new LinkedHashSet<TaskInProgress>();
1.3.1.2檢查內存需求量

checkMemoryRequirements(job);

判斷MR程序的配置"mapred.task.maxvmem"(默認爲-1L)內存量是否超過集羣允許的的配置"mapred.task.limit.maxvmem"量.

1.3.1.3 加入作業到隊列中.
       status = addJob(jobId, job);
       jobs.put(job.getProfile().getJobID(), job);
       for (JobInProgressListener listener : jobInProgressListeners) {
         listener.jobAdded(job);
       }

將jobInProgress加入到JT的jobs映射中. 然後通知任務調度器

在調度器啓動時會將自己的監聽器加入到JT的監聽器隊列中. 當有job加入時, 就會通知隊列中的所有監聽器加入了一個job作業.這樣來告知調度器有一個作業到來.

1.4 作業初始化

1.4.1 通知調度器

這裏我們以CapacityTaskScheduler高度器爲例, 當CapacityTaskScheduler的JobQueuesManager監聽器收到jobAdded事件後,

   // add job to waiting queue. It will end up in the right place,
   // based on priority.
   queue.addWaitingJob(job);
   // let scheduler know.
   scheduler.jobAdded(job);

1.   將job加入到調度器的指定隊列CapacitySchedulerQueue的waitingJobs映射中, 隊列是由MR程序的配置"mapred.job.queue.name"決定的. 默認爲default.

2.   告訴調度器調度隊列中來了一個job

   // Inform the queue
     queue.jobAdded(job);  //將作業提交用戶在隊列中的執行作業數加1.
     // setup scheduler specific job information
     preInitializeJob(job);

作job初始化前的準備工作.這主要是根據集羣的配置"mapred.cluster.map.memory.mb"(每個map槽的內存量slotSizePerMap)和MR程序的配置"mapred.task.maxvmem"(每個task最大內存量getMemoryForMapTask())來決定每個mapTask將要佔用集羣的槽數.

公式爲:(int)(Math.ceil((float)getMemoryForMapTask() / (float)slotSizePerMap))

 ReduceTask同上.使用集羣的"mapred.cluster.reduce.memory.mb"配置

1.4.2 調度器調度作業初始化

在調度器啓動的同時也會啓動幾個後臺服務線程. 其中有一個線程爲JobInitializationPoller線程, 以下是其run方法執行情況:

JobInitializationPoller.run()代碼:

while (running) {
       cleanUpInitializedJobsList(); //從初始化集合中清理掉那些處於RUNNING狀態的且被調度過的job, 或者已經完成的job
       selectJobsToInitialize();
       if (!this.isInterrupted()) {
         Thread.sleep(sleepInterval); //配置"mapred.capacity-scheduler.init-poll-interval"默認爲3000.
       }
    }
1.4.2.1 選擇job去初始化.

selectJobsToInitialize()代碼:

for(String queue : jobQueueManager.getAllQueues()) {
     ArrayList<JobInProgress> jobsToInitialize =getJobsToInitialize(queue);
     JobInitializationThread t = threadsToQueueMap.get(queue);
     for (JobInProgress job : jobsToInitialize) {
       t.addJobsToQueue(queue, job);
     }
}

首先遍歷所有集羣中queue隊列中的的所有jobInProgress實例, 並找出所有狀態爲JobStatus.PREP 的集合.

然後找到用於初始化Job的線程JobInitializationThread(這個線程是threadsToQueueMap映射中根據queue指定的隊列所對應的啓動線程. 這是啓動時根據capacity-scheduler.xml的配置"mapred.capacity-scheduler.init-worker-threads"來決定的, 默認會啓5個線程去爲各個隊列檢查要初始化的job, 並初始化它.)

注: 當集羣環境中job過多時, 可以增加此配置用適量的線程去執行作業初始化工作.

最後將隊列queue所對應的未初始化的job都加入到JobInitializationThread線程中進行一一的初始化.

JobInitializationThread.run()代碼:

public void run() {
     while (startIniting) {
       initializeJobs(); 
           Thread.sleep(sleepInterval); //配置"mapred.capacity-scheduler.init-poll-interval"默認爲3000.
     }
    }

在initializeJobs()中會遍歷所有未初始化的job調用JT的initJob去初始化這個job.

   

        setInitializingJob(job);
           ttm.initJob(job); //TaskTrackerManager即爲JobTracker.

1.4.2.2 初始化作業

TaskInProgress

     TaskInProgress類維護了一個Task運行過程中的全部信息. 在hadoop中, 由於一個任務可能會推測執行或者重新執行.所以會存在多個TaskAttempt, 同一時刻可能有多個處理相同的任務嘗試同時執行. 這些任務被同一個TaskInprogress對象管理和跟蹤.

初始化它時主要的屬性:

     privatefinal TaskSplitMetaInfo splitInfo; //maptask要處理的split信息
     privateJobInProgress job; //TaskInProgress所在的jobInprogress.
     //正在運行的TaskID與TaskTrackerID之間的映射關係.
     privateTreeMap<TaskAttemptID, String> activeTasks = newTreeMap<TaskAttemptID, String>();
     //已運行的所有TaskAttemptID, 包括已運行完成的和正在運行的.
     privateTreeSet<TaskAttemptID> tasks = new TreeSet<TaskAttemptID>();
     //TaskID與TaskStatus的映射關係.
     privateTreeMap<TaskAttemptID,TaskStatus> taskStatuses = newTreeMap<TaskAttemptID,TaskStatus>();
     //cleanupTaskId與TaskTracker的對應關係
     privateTreeMap<TaskAttemptID, String> cleanupTasks = newTreeMap<TaskAttemptID, String>();
     //已經運行失敗的task節點列表
     privateTreeSet<String> machinesWhereFailed = new TreeSet<String>();
     //待殺死的Task列表.
     privateTreeMap<TaskAttemptID, Boolean> tasksToKill = newTreeMap<TaskAttemptID, Boolean>();
     //等待被提交的TaskAttemp,
     privateTaskAttemptID taskToCommit;
在線程JobInitializationThread運行環境中調用JT的initJob(job). 主要做了兩件事:

1初始化job相關的Task

TaskSplitMetaInfo[] splits = createSplits(jobId);
numMapTasks = splits.length;
maps = new TaskInProgress[numMapTasks];
//"mapreduce.job.locality.wait.factor"
localityWaitFactor = conf.getFloat(LOCALITY_WAIT_FACTOR, DEFAULT_LOCALITY_WAIT_FACTOR);
this.reduces = new TaskInProgress[numReduceTasks];
//根據配置計算當mapTask完成多少個時開始調度reduceTask啓動. 默認爲5%.
completedMapsForReduceSlowstart = conf.getFloat("mapred.reduce.slowstart.completed.maps") * numMapTasks)
cleanup = new TaskInProgress[2];
setup = new TaskInProgress[2];
 

1.1MapTaskInprogress

從job的作業提交目錄中讀取splitMetainfo信息, 並將其解析成相關的TaskSplitMetaInfo[]數組, 並創建相應多的mapTaskInProgress實例分發這些split去準備執行. 即爲每個MapTaskInProgress實例中都指定它將要處理的源數據.

1.2 ReduceTaskInprogress

 然後根據MR的配置"mapred.reduce.tasks"創建reduceTaskInProgress實例.

1.3cleanupTaskInprogress和setupTaskInprogress.

會爲map和reduce分別創建cleanup和setup類型的Task.

2通知調度器更新job.

判斷初始化job相關Task之前與之後若狀態不一致時, 通知調度器去更新這個job.

JobStatusChangeEvent event = newJobStatusChangeEvent(job, EventType.RUN_STATE_CHANGED, prevStatus,
              newStatus);
       synchronized (JobTracker.this) {
          for(JobInProgressListener listener : jobInProgressListeners) {
               listener.jobUpdated(event);
          }
       }
對於剛初始化完還沒有得被調度器調度的job此時仍然牌PREP狀態.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章