原帖:http://www.infoq.com/cn/articles/MapReduce-Best-Practice-1
MapRuduce開發對於大多數程序員都會覺得略顯複雜,運行一個WordCount(Hadoop中hello word程序)不僅要熟悉MapRuduce模型,還要了解Linux命令(儘管有Cygwin,但在Windows下運行MapRuduce仍然很麻煩),此外還要學習程序的打包、部署、提交job、調試等技能,這足以讓很多學習者望而退步。
所以如何提高MapReduce開發效率便成了大家很關注的問題。但Hadoop的Committer早已經考慮到這些問題,從而開發了ToolRunner、MRunit(MapReduce最佳實踐第二篇中會介紹)、MiniMRCluster、MiniDFSCluster等輔助工具,幫助解決開發、部署等問題。舉一個自己親身的例子:
1. 使用ToolRunner讓參數傳遞更簡單
關於MapReduce運行和參數配置,你是否有下面的煩惱:
- 將MapReduce Job配置參數寫到java代碼裏,一旦變更意味着修改java文件源碼、編譯、打包、部署一連串事情。
- 當MapReduce 依賴配置文件的時候,你需要手工編寫java代碼使用DistributedCache將其上傳到HDFS中,以便map和reduce函數可以讀取。
- 當你的map或reduce 函數依賴第三方jar文件時,你在命令行中使用”-libjars”參數指定依賴jar包時,但根本沒生效。
其實,Hadoop有個ToolRunner類,它是個好東西,簡單好用。無論在《Hadoop權威指南》還是Hadoop項目源碼自帶的example,都推薦使用ToolRunner。
下面我們看下src/example目錄下WordCount.java文件,它的代碼結構是這樣的:
public class WordCount { // 略... public static void main(String[] args) throws Exception { Configuration conf = new Configuration(); String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs(); // 略... Job job = new Job(conf, "word count"); // 略... System.exit(job.waitForCompletion(true) ? 0 : 1); } }
WordCount.java中使用到了GenericOptionsParser這個類,它的作用是將命令行中參數自動設置到變量conf中。舉個例子,比如我希望通過命令行設置reduce task數量,就這麼寫:
bin/hadoop jar MyJob.jar com.xxx.MyJobDriver -Dmapred.reduce.tasks=5
上面這樣就可以了,不需要將其硬編碼到java代碼中,很輕鬆就可以將參數與代碼分離開。
其它常用的參數還有”-libjars”和-“files”,使用方法一起送上:
bin/hadoop jar MyJob.jar com.xxx.MyJobDriver -Dmapred.reduce.tasks=5 \ -files ./dict.conf \ -libjars lib/commons-beanutils-1.8.3.jar,lib/commons-digester-2.1.jar
參數”-libjars”的作用是上傳本地jar包到HDFS中MapReduce臨時目錄並將其設置到map和reduce task的classpath中;參數”-files”的作用是上傳指定文件到HDFS中mapreduce臨時目錄,並允許map和reduce task讀取到它。這兩個配置參數其實都是通過DistributeCache來實現的。
至此,我們還沒有說到ToolRunner,上面的代碼我們使用了GenericOptionsParser幫我們解析命令行參數,編寫ToolRunner的程序員更懶,它將 GenericOptionsParser調用隱藏到自身run方法,被自動執行了,修改後的代碼變成了這樣:
public class WordCount extends Configured implements Tool { @Override public int run(String[] arg0) throws Exception { Job job = new Job(getConf(), "word count"); // 略... System.exit(job.waitForCompletion(true) ? 0 : 1); return 0; } public static void main(String[] args) throws Exception { int res = ToolRunner.run(new Configuration(), new WordCount(), args); System.exit(res); } }
看看代碼上有什麼不同:
- 讓WordCount繼承Configured並實現Tool接口。
- 重寫Tool接口的run方法,run方法不是static類型,這很好。
- 在WordCount中我們將通過getConf()獲取Configuration對象。
關於GenericOptionsParser更多用法,請點擊這裏:GenericOptionsParser.html
推薦指數:★★★★
推薦理由:通過簡單的幾步,就可以實現代碼與配置隔離、上傳文件到DistributeCache等功能。修改MapReduce參數不需要修改java代碼、打包、部署,提高工作效率。
2. 有效使用Hadoop源碼
作爲MapReduce程序員不可避免的要使用Hadoop源碼,Why?記得2010剛接觸hadoop的時候,總是搞不清舊api和新api的使用方法。寫了一段程序,在一個新api裏面調用某個方法每次都是返回Null,非常惱火,後來附上源碼發現,這個方法真的就是隻做了“return null”並沒有給予實現,最後只得想其它方法曲線救國。總之要想真正瞭解MapReduce開發,源碼是不可缺少的工具。
下面是我的源碼使用實踐,步驟有點麻煩不過配置一次就好:
1. Eclipse中創建Hadoop源碼項目
1.1 下載並解壓縮Hadoop分發包(通常是tar.gz包)
1.2 Eclipse中新建Java項目
1.3 將解壓後hadoop源碼包/src目錄中core, hdfs, mapred, tool幾個目錄(其它幾個源碼根據需要進行選擇)copy到eclipse新建項目的src目錄。
1.4 右鍵點擊eclipse項目,選擇“Properties”,在彈出對話框中左邊菜單選擇“Java Build Path”:
a) 點擊“Source”標籤。先刪除src這個目錄,然後依次添加剛纔copy過來的目錄
b) 點擊當前對話框“Libaries”,點擊“Add External JARs”,在彈出窗口中添加$HADOOPHOME下幾個hadoop程序jar包,然後再次添加$HADOOPHOME /lib、$HADOOP_HOME /lib/jsp-2.1兩個目錄下所有jar包,最後還要添加ANT項目lib目錄下ant.jar文件。
1.5 此時源碼項目應該只有關於找不到sun.security包的錯誤了。這時我們還是在“Libraries”這個標籤中,展開jar包列表最低下的“JRE System Library”,雙擊”Access rules”,在彈出窗口中點擊“add按鈕”,然後在新對話框中"Resolution"下拉框選擇"Accessible","Rule Pattern"填寫*/,保存後就OK了。如下圖:
2. 如何使用這個源碼項目呢?
比如我知道Hadoop某個源碼文件的名稱,在eclipse中可以通過快捷鍵“Ctrl + Shift + R”調出查找窗口,輸入文件名,如“MapTask”,那可以打開這個類的源碼了。
還有個使用場景,當我們編寫MapReduce程序的時候,我想直接打開某個類的源碼,通過上面的操作還是有點麻煩,比如我想看看Job類是如何實現的,當我點擊它的時候會出現下面的情景:
解決辦法很簡單:
點擊圖中“Attach Source”按鈕-> 點擊“Workspace”按鈕->選擇剛纔新建的Hadoop源碼項目。完成後源碼應該就蹦出來了。
總結一下,本實踐中我們獲得了什麼功能:
- 知道hadoop源碼文件名,快速找到該文件
- 寫程序的時候直接查看Hadoop相關類源碼
- Debug程序的時候,可以直接進入源碼查看並跟蹤運行
推薦指數:★★★★
推薦理由:通過源碼可以幫助我們更深入瞭解Hadoop,可以幫助我們解決複雜問題
3. 正確使用壓縮算法
下表資料引用cloudera官方網站的一篇博客,原文點這裏。
Compression | File | Size(GB) | Compression Time (s) | Decompression Time (s) |
None | some_logs | 8.0 | - | - |
Gzip | some_logs.gz | 1.3 | 241 | 72 |
LZO | some_logs.lzo | 2.0 | 55 | 35 |
上面表格與筆者集羣實際環境測試結果一致,所以我們可以得出如下結論:
- LZO文件的壓縮和解壓縮性能要遠遠好於Gzip文件。
- 相同文本文件,使用Gzip壓縮可以比LZO壓縮大幅減少磁盤空間。
上面的結論對我們有什麼幫助呢?在合適的環節使用合適壓縮算法。
在中國的帶寬成本是非常貴的,費用上要遠遠高於美國、韓國等國家。所以在數據傳輸環節,我們希望使用了Gzip算法壓縮文件,目的是減少文件傳輸量,降低帶寬成本。使用LZO文件作爲MapReduce文件的輸入(創建lzo index後是支持自動分片輸入的)。對於大文件,一個map task的輸入將變爲一個block,而不是像Gzip文件一樣讀取整個文件,這將大幅提升MapReduce運行效率。
主流傳輸工具FlumeNG和scribe默認都是非壓縮傳輸的(都是通過一行日誌一個event進行控制的),這點大家在使用時要注意。FlumeNG可以自定義組件方式實現一次傳輸多條壓縮數據,然後接收端解壓縮的方式來實現數據壓縮傳輸,scribe沒有使用過不評論。
另外值得一提的就是snappy,它是由Google開發並開源的壓縮算法的,是Cloudera官方大力提倡在MapReduce中使用的壓縮算法。它的特點是:與LZO文件相近的壓縮率的情況下,還可以大幅提升壓縮和解壓縮性能,但是它作爲MapReduce輸入是不可以分割的。
延伸內容:
Cloudera官方Blog對Snappy介紹:
http://blog.cloudera.com/blog/2011/09/snappy-and-hadoop/
老外上傳的壓縮算法性能測試數據:
推薦指數:★★★★★
推薦理由:壓縮率和壓縮性能一定程度是矛盾體,如何均衡取決於應用場景。使用合適壓縮算法直接關係到老闆的錢,如果能夠節省成本,體現程序員的價值。
4. 在合適的時候使用Combiner
map和 reduce 函數的輸入輸出都是key-value,Combiner和它們是一樣的。作爲map和reduce的中間環節,它的作用是聚合map task的磁盤,減少map端磁盤寫入,減少reduce端處理的數據量,對於有大量shuffle的job來說,性能往往取決於reduce端。因爲reduce 端要經過從map端copy數據、reduce端歸併排序,最後纔是執行reduce方法,此時如果可以減少map task輸出將對整個job帶來非常大的影響。
什麼時候可以使用Combiner?
比如你的Job是WordCount,那麼完全可以通過Combiner對map 函數輸出數據先進行聚合,然後再將Combiner輸出的結果發送到reduce端。
什麼時候不能使用Combiner?
WordCount在reduce端做的是加法,如果我們reduce需求是計算一大堆數字的平均數,則要求reduce獲取到全部的數字進行計算,纔可以得到正確值。此時,是不能使用Combiner的,因爲會其會影響最終結果。 注意事項:即使設置Combiner,它也不一定被執行(受參數min.num.spills.for.combine影響),所以使用Combiner的場景應保證即使沒有Combiner,我們的MapReduce也能正常運行。
推薦指數:★★★★★
推薦理由:在合適的場景使用Combiner,可以大幅提升MapReduce 性能。
5. 通過回調通知知道MapReduce什麼時候完成
你知道什麼時候MapReduce完成嗎?知道它執行成功或是失敗嗎?
Hadoop包含job通知這個功能,要使用它非常容易,藉助我們實踐一的ToolRunner,在命令行裏面就可以進行設置,下面是一個例子:
hadoop jar MyJob.jar com.xxx.MyJobDriver \ -Djob.end.notification.url=http://moniter/mapred_notify/\$jobId/\$jobStatus
通過上面的參數設置後,當MapReduce完成後將會回調我參數中的接口。其中$jobId和$jobStatus會自動被實際值代替。
上面在$jobId和$jobStatus兩個變量前,我添加了shell中的轉義符”\”,如果使用java代碼設置該參數是不需要轉義符的。
總結下:看看我們通過該實踐可以獲得什麼?
- 獲取MapReduce運行時間和回調完成時間,可以分析最耗時Job,最快完成Job。
- 通過MapReduce運行狀態(包括成功、失敗、Kill),可以第一時間發現錯誤,並通知運維。
- 通過獲取MapReduce完成時間,可以第一時間通過用戶,數據已經計算完成,提升用戶體驗
Hadoop這塊功能的源碼文件是JobEndNotifier.java,可以馬上通過本文實踐二看看究竟。其中下面兩個參數就是我通過翻源碼的時候發現的,如果希望使用該實踐趕緊通過ToolRunner設置上吧(別忘了加-D,格式是-Dkey=value)。
- job.end.retry.attempts // 設置回調通知retry次數
- job.end.retry.interval // 設置回調時間間隔,單位毫秒
當然如果hadoop沒有提供Job狀態通知的功能,我們也可以通過採用阻塞模式提交MapReduce Job,然後Job完成後也可以獲知其狀態和運行時間。
推薦指數:★★★
推薦理由:對mapreduce job監控最省事有效的辦法,沒有之一。