Hadoop作業提交分析(四)

  前面我們所分析的部分其實只是Hadoop作業提交的前奏曲,真正的作業提交代碼是在MR程序的main裏,RunJar在最後會動態調用這個main,在(二)裏有說明。我們下面要做的就是要比RunJar更進一步,讓作業提交能在編碼時就可實現,就像Hadoop Eclipse Plugin那樣可以對包含Mapper和Reducer的MR類直接Run on Hadoop。

  一般來說,每個MR程序都會有這麼一段類似的作業提交代碼,這裏拿WordCount的舉例:

複製代碼
Configuration conf = new Configuration();
String[] otherArgs
= new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length != 2) {
System.err.println(
"Usage: wordcount <in> <out>");
System.exit(
2);
}
Job job
= new Job(conf, "word count");
job.setJarByClass(WordCount.
class);
job.setMapperClass(TokenizerMapper.
class);
job.setCombinerClass(IntSumReducer.
class);
job.setReducerClass(IntSumReducer.
class);
job.setOutputKeyClass(Text.
class);
job.setOutputValueClass(IntWritable.
class);
FileInputFormat.addInputPath(job,
new Path(otherArgs[0]));
FileOutputFormat.setOutputPath(job,
new Path(otherArgs[1]));
System.exit(job.waitForCompletion(
true) ? 0 : 1);
複製代碼

  首先要做的是構建一個Configuration對象,並進行參數解析。接着構建提交作業用的Job對象,並設置作業Jar包、對應Mapper和Reducer類、輸入輸出的Key和Value的類及作業的輸入和輸出路徑,最後就是提交作業並等待作業結束。這些只是比較基本的設置參數,實際還支持更多的設置參數,這裏就不一一介紹,詳細的可參考API文檔。

  一般分析代碼都從開始一步步分析,但我們的重點是分析提交過程中發生的事,這裏我們先不理前面的設置對後面作業的影響,我們直接跳到作業提交那一步進行分析,當碰到問題需要分析前面的代碼時我會再分析。

  當調用job.waitForCompletion時,其內部調用的是submit方法來提交,如果傳入參數爲ture則及時打印作業運作信息,否則只是等待作業結束。submit方法進去後,還有一層,裏面用到了job對象內部的jobClient對象的submitJobInternal來提交作業,從這個方法纔開始做正事。進去第一件事就是獲取jobId,用到了jobSubmitClient對象,jobSubmitClient對應的類是JobSubmissionProtocol的實現之一(目前有兩個實現,JobTracker和LocalJobRunner),由此可判斷出jobSubmitClient對應的類要麼是JobTracker,要麼是LocalJobRunner。呃,這下有點想法了,作業提交是上到JobTracker去,還是在本地執行?可能就是看這個jobSunmitClient初始化時得到的是哪個類的實例了,我們可以稍稍的先往後看看,你會發現submitJobInternal最後用了jobSubmitClient.submitJob(jobId)來提交作業,再稍稍看看JobTracker和LocalJobRunner的submitJob實現,看來確實是這麼回事。好,那我們就先跳回去看看這個jobSubmitClient是如何初始化的。在JobClient的init中我們可以發現jobSubmitClient的初始化語句:

複製代碼
String tracker = conf.get("mapred.job.tracker", "local");
if ("local".equals(tracker)) {
this.jobSubmitClient = new LocalJobRunner(conf);
}
else {
this.jobSubmitClient = createRPCProxy(JobTracker.getAddress(conf), conf);
}
複製代碼

  哈,跟conf中的mapred.job.tracker屬性有關,如果你沒設置,那默認得到的值就是local,jobSubmitClient也就會被賦予LocalJobRunner的實例。平時,我們開發時一般都只是引用lib裏面的庫,不引用conf文件夾裏的配置文件,這裏就能解釋爲什麼我們直接Run as Java Application時,作業被提交到Local去運行了,而不是Hadoop Cluster中。那我們把conf文件夾添加到classpath,就能Run on Hadoop了麼?目前下結論尚早,我們繼續分析(你添加了conf文件夾後,可以提交試一試,會爆出一個很明顯的讓你知道還差什麼的錯誤,這裏我就賣賣官子,先不說)。

  jobId獲取到後,在SystemDir基礎上加jobId構建了提交作業的目錄submitJobDir,SystemDir由JobClient的getSystemDir方法得出,這個SystemDir在構建fs對象時很重要,確定了返回的fs的類型。下去的configureCommandLineOptions方法主要是把作業依賴的第三方庫或文件上傳到fs中,並做classpath映射或Symlink,以及一些參數設置,都是些細微活,這裏不仔細分析。我們主要關心裏面的兩個地方,一個是:

FileSystem fs = getFs();

  看上去很簡單,一句話,就是獲取FileSystem的實例,但其實裏面繞來繞去,有點頭暈。因爲Hadoop對文件系統進行了抽象,所以這裏獲得fs實例的類型決定了你是在hdfs上操作還是在local fs上操作。好了,我們衝進去看看。

複製代碼
public synchronized FileSystem getFs() throws IOException {
if (this.fs == null) {
Path sysDir
= getSystemDir();
this.fs = sysDir.getFileSystem(getConf());
}
return fs;
}
複製代碼

  看見了吧,fs是由sysDir的getFileSystem返回的。我們再衝,由於篇幅,下面就只列出主要涉及的語句。

複製代碼
FileSystem.get(this.toUri(), conf);

CACHE.get(uri, conf);

fs
= createFileSystem(uri, conf);

Class
<?> clazz = conf.getClass("fs." + uri.getScheme() + ".impl", null);
if (clazz == null) {
throw new IOException("No FileSystem for scheme: " + uri.getScheme());
}
FileSystem fs
= (FileSystem)ReflectionUtils.newInstance(clazz, conf);
fs.initialize(uri, conf);
return fs;
複製代碼

  又是跟conf有關,看來conf是得實時跟住的。這裏用到了Java的反射技術,用來動態生成相應的類實例。其中的class獲取與uri.getScheme有密切關係,而uri就是在剛纔的sysDir基礎上構成,sysDir的值又最終是由jobSubmitClient的實例決定的。如果jobSubmitClient是JobTracker的實例,那Scheme就是hdfs。如果是LocalJobRunner的實例,那就是file。從core-default.xml你可以找到如下的信息:

複製代碼
<property>
<name>fs.file.impl</name>
<value>org.apache.hadoop.fs.LocalFileSystem</value>
<description>The FileSystem for file: uris.</description>
</property>

 

<property>
<name>fs.hdfs.impl</name>
<value>org.apache.hadoop.hdfs.DistributedFileSystem</value>
<description>The FileSystem for hdfs: uris.</description>
</property>
複製代碼

  所以在前面的作業提交代碼中,在初始化Job實例時,很多事已經決定了,由conf文件夾中的配置文件決定。Configuration是通過當前線程上下文的類加載器來加載類和資源文件的,所以要想Run on Hadoop,第一步必須要讓Conf文件夾進入Configuration的類加載器的搜索路徑中,也就是當前線程上下文的類加載器。

  第二個要注意的地方是:

複製代碼
String originalJarPath = job.getJar();

if (originalJarPath != null) { // copy jar to JobTracker's fs

// use jar name if job is not named.
if ("".equals(job.getJobName())){
job.setJobName(
new Path(originalJarPath).getName());
}
job.setJar(submitJarFile.toString());
fs.copyFromLocalFile(
new Path(originalJarPath), submitJarFile);
fs.setReplication(submitJarFile, replication);
fs.setPermission(submitJarFile,
new FsPermission(JOB_FILE_PERMISSION));
}
else {
LOG.warn(
"No job jar file set. User classes may not be found. "+
"See JobConf(Class) or JobConf#setJar(String).");
}
複製代碼

  因爲client在提交作業到Hadoop時需要把作業打包成jar,然後copy到fs的submitJarFile路徑中。如果我們想Run on Hadoop,那就必須自己把作業的class文件打個jar包,然後再提交。在Eclipse中,這就比較容易了。這裏假設你啓用了自動編譯功能。我們可以在代碼的開始階段加入一段代碼用來打包bin文件夾裏的class文件爲一個jar包,然後再執行後面的常規操作。

  在configureCommandLineOptions方法之後,submitJobInternal會檢查輸出文件夾是否已存在,如果存在則拋出異常。之後,就開始劃分作業數據,並根據split數得到map tasks的數量。最後,就是把作業配置文件寫入submitJobFile,並調用jobSubmitClient.submitJob(jobId)最終提交作業。

  至此,對Hadoop的作業提交分析也差不多了,有些地方講的比較囉嗦,有些又講得點到而止,但大體的過程以及一些較重要的東西還是說清楚了,其實就是那麼回事。下去的文章我們會在前面的jobUtil基礎上增加一些功能來支持Run on Hadoop,其實主要就是增加一個打包Jar的方法。

  To be continued...


-------------------------------------------------------------------------------

轉載原文地址:http://www.cnblogs.com/spork/archive/2010/04/21/1717552.html

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