mapreduce job提交流程原始碼級分析(二)(原創)

玖瘋發表於2014-04-10

上一小節(http://www.cnblogs.com/lxf20061900/p/3643581.html)講到Job. submit()方法中的:

info = jobClient.submitJobInternal(conf)方法用來上傳資源提交Job的,這一節就講講這個方法。

一、首先jobClient在建構函式中會構造了和JobTracker通訊的物件jobSubmitClient,jobSubmitClient是JobSubmissionProtocol型別的動態代理類。JobSubmissionProtocol協議是JobClient與JobTracker通訊專用協議。程式碼如下:

 private static JobSubmissionProtocol createRPCProxy(InetSocketAddress addr,
      Configuration conf) throws IOException {
    return (JobSubmissionProtocol) RPC.getProxy(JobSubmissionProtocol.class,
        JobSubmissionProtocol.versionID, addr, 
        UserGroupInformation.getCurrentUser(), conf,
        NetUtils.getSocketFactory(conf, JobSubmissionProtocol.class));
  }

   getProxy方法的關鍵是Invoker類,Invoker類實現了 InvocationHandler介面,主要有兩個成員變數,remoteId是Client.ConnectionId型別,儲存連線地址和使用者的 ticket,客戶端連線伺服器由<remoteAddress,protocol,ticket>唯一標識。

Invoker類的invoke方法最重要的操作是:ObjectWritable value = (ObjectWritable) client.call(new Invocation(method, args), remoteId)。Invocation實現了Writable介面,並封裝了method和args,使得可以通過RPC傳輸;Client.call方法將Writable引數封裝到一個Call中,並且連線JobTracker後將封裝後call傳送過去,同步等待call執行完畢,返回value。

 public Writable call(Writable param, ConnectionId remoteId)  
                       throws InterruptedException, IOException {
    Call call = new Call(param);
    Connection connection = getConnection(remoteId, call);
    connection.sendParam(call);                 // send the parameter
    boolean interrupted = false;
    synchronized (call) {
      while (!call.done) {
        try {
          call.wait();                           // wait for the result
        } catch (InterruptedException ie) {
          // save the fact that we were interrupted
          interrupted = true;
        }
      }

      if (interrupted) {
        // set the interrupt flag now that we are done waiting
        Thread.currentThread().interrupt();
      }

      if (call.error != null) {
        if (call.error instanceof RemoteException) {
          call.error.fillInStackTrace();
          throw call.error;
        } else { // local exception
          // use the connection because it will reflect an ip change, unlike
          // the remoteId
          throw wrapException(connection.getRemoteAddress(), call.error);
        }
      } else {
        return call.value;
      }
    }
  }

   上面的第四行程式碼用於建立同JobTracker的連線。而Client.getConnection方法中connection.setupIOstreams()才是真正建立連線的地方,其中的socket是通過預設的SocketFactory .createSocket(),而這個預設的SocketFactory是org.apache.hadoop.net. StandardSocketFactory。

二、jobClient.submitJobInternal(conf)初始化staging目錄(這是job提交的根目錄):Path jobStagingArea=JobSubmissionFiles.getStagingDir(JobClient.this, jobCopy),這個方法最終會呼叫jobTracker.getStagingAreaDirInternal()方法,程式碼如下:

  private String getStagingAreaDirInternal(String user) throws IOException {
    final Path stagingRootDir =
      new Path(conf.get("mapreduce.jobtracker.staging.root.dir",
            "/tmp/hadoop/mapred/staging"));
    final FileSystem fs = stagingRootDir.getFileSystem(conf);
    return fs.makeQualified(new Path(stagingRootDir,
                              user+"/.staging")).toString();
  }

三、從JobTracker獲取JobID。JobID jobId = jobSubmitClient.getNewJobId()。最終呼叫的是JobTracker.getNewJobId()方法。然後執行Path submitJobDir = new Path(jobStagingArea, jobId.toString());獲得該job提交的路徑,也就是在stagingDir目錄下建一個以jobId為檔名的目錄,可以檢視配置檔案中的"mapreduce.job.dir"來檢視此完整目錄。有了 submitJobDir之後就可以將job執行所需的全部檔案上傳到對應的目錄下了,具體是呼叫 jobClient.copyAndConfigureFiles(jobCopy, submitJobDir)這個方法。

四、copyAndConfigureFiles(jobCopy, submitJobDir)實現上傳檔案,包括-tmpfiles(外部檔案)、tmpjars(第三方jar包)、tmparchives(一些歸檔檔案)以及job.jar拷貝到HDFS中,這個方法最終呼叫jobClient.copyAndConfigureFiles(job, jobSubmitDir, replication);這個方法實現檔案上傳。而前三種檔案(tmpfiles(外部檔案)、tmpjars(第三方jar包)、tmparchives(一些歸檔檔案))的實際上傳過程在copyRemoteFiles方法中,通過FileUtil.copy完成拷貝,這三種檔案都是先分割檔案列表後分別上傳(每一類檔案可以有多個)。然後是:

   // First we check whether the cached archives and files are legal.
    TrackerDistributedCacheManager.validate(job);
    //  set the timestamps of the archives and files
    TrackerDistributedCacheManager.determineTimestamps(job);
    //  set the public/private visibility of the archives and files
    TrackerDistributedCacheManager.determineCacheVisibilities(job);
    // get DelegationTokens for cache files
    TrackerDistributedCacheManager.getDelegationTokens(job,job.getCredentials());

上面的程式碼是進行一些cached archives and files的校驗和儲存其時間戳和許可權內容

  Job.jar通過fs.copyFromLocalFile方法拷貝到HDFS中。而job.jar(這是打包後的作業)檔案則是直接通過fs.copyFromLocalFile(new Path(originalJarPath), submitJarFile);上傳完成。我們在提交作業的時候會在本地先打包成jar檔案然後將配置檔案中的"mapred.jar"設定為本地jar包路徑,當在這裡拷貝到HDFS中後在重新將"mapred.jar"設定為HDFS對應job.jar包的路徑。

  同時這四個檔案都會設定replication個副本,防止熱點出現。

五、然後就會根據我們設定的outputFormat類執行output.checkOutputSpecs(context),進行輸出路徑的檢驗,主要是保證輸出路徑不存在,存在會丟擲異常。

六、對輸入檔案進行分片操作了,int maps = writeSplits(context, submitJobDir)。writeSplits方法會根據是否使用了新API選擇不同的方法寫:

private int writeSplits(org.apache.hadoop.mapreduce.JobContext job,
      Path jobSubmitDir) throws IOException,
      InterruptedException, ClassNotFoundException {
    JobConf jConf = (JobConf)job.getConfiguration();
    int maps;
    if (jConf.getUseNewMapper()) {
      maps = writeNewSplits(job, jobSubmitDir);
    } else {
      maps = writeOldSplits(jConf, jobSubmitDir);
    }
    return maps;
  }
使用了新API後,會呼叫writeNewSplits(job, jobSubmitDir)方法,這個方法程式碼如下:
 private <T extends InputSplit>
  int writeNewSplits(JobContext job, Path jobSubmitDir) throws IOException,
      InterruptedException, ClassNotFoundException {
    Configuration conf = job.getConfiguration();
    InputFormat<?, ?> input =
      ReflectionUtils.newInstance(job.getInputFormatClass(), conf);//預設是TextInputFormat

    List<InputSplit> splits = input.getSplits(job);
    T[] array = (T[]) splits.toArray(new InputSplit[splits.size()]);

    // sort the splits into order based on size, so that the biggest
    // go first,大檔案優先處理
    Arrays.sort(array, new SplitComparator());
    JobSplitWriter.createSplitFiles(jobSubmitDir, conf,
        jobSubmitDir.getFileSystem(conf), array);
    return array.length;//這是mapper的數量
  }

可以看出該方法首先獲取splits陣列資訊後,排序,將會優先處理大檔案。JobSplitWriter.createSplitFiles(jobSubmitDir, conf, jobSubmitDir.getFileSystem(conf), array)方法會將split資訊和SplitMetaInfo都寫入HDFS中,其程式碼如下:

public static <T extends InputSplit> void createSplitFiles(Path jobSubmitDir, 
      Configuration conf, FileSystem fs, T[] splits) 
  throws IOException, InterruptedException {
    FSDataOutputStream out = createFile(fs, 
        JobSubmissionFiles.getJobSplitFile(jobSubmitDir), conf);
    SplitMetaInfo[] info = writeNewSplits(conf, splits, out);
    out.close();
    writeJobSplitMetaInfo(fs,JobSubmissionFiles.getJobSplitMetaFile(jobSubmitDir), 
        new FsPermission(JobSubmissionFiles.JOB_FILE_PERMISSION), splitVersion,
        info);
  }

如上writeNewSplits會將資訊寫入job.split檔案,然後返回SplitMetaInfo陣列資訊,再通過writeJobSplitMetaInfo方法SplitMetaInfo資訊寫入job.splitmetainfo中。

七、然後將配置檔案寫入:jobCopy.writeXml(out);//寫"job.xml"。

八、通過 jobSubmitClient.submitJob(jobId, submitJobDir.toString(), jobCopy.getCredentials())提交job,最終呼叫的是JobTracker.submitJob。

九、返回一個NetworkedJob(status, prof, jobSubmitClient)物件,它實現了RunningJob介面。這個物件可以在JobClient端(比如eclipse,不斷的列印執行日誌)。

 

ps:

一、hadoop版本是1.0.0;

二、上述檔案的提交目錄可以在web ui中開啟相應作業的配置檔案查詢"mapreduce.job.dir",就可以看到檔案的上傳目錄。比如:hdfs://XXXX:8020/user/hadoop/.staging/job_201403141637_0160

 

下一節關注上述的步驟八。

錯誤之處還望大夥指點

 

參考:

http://www.kankanews.com/ICkengine/archives/87415.shtml

相關文章