Hadoop MapReduce開發最佳實踐(上篇)


原帖: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運行和參數配置,你是否有下面的煩惱:

  1. 將MapReduce Job配置參數寫到java代碼裏,一旦變更意味着修改java文件源碼、編譯、打包、部署一連串事情。
  2. 當MapReduce 依賴配置文件的時候,你需要手工編寫java代碼使用DistributedCache將其上傳到HDFS中,以便map和reduce函數可以讀取。
  3. 當你的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);
    }
}

看看代碼上有什麼不同:

  1. 讓WordCount繼承Configured並實現Tool接口。
  2. 重寫Tool接口的run方法,run方法不是static類型,這很好。
  3. 在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源碼項目。完成後源碼應該就蹦出來了。

總結一下,本實踐中我們獲得了什麼功能:

  1. 知道hadoop源碼文件名,快速找到該文件
  2. 寫程序的時候直接查看Hadoop相關類源碼
  3. 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

上面表格與筆者集羣實際環境測試結果一致,所以我們可以得出如下結論:

  1. LZO文件的壓縮和解壓縮性能要遠遠好於Gzip文件。
  2. 相同文本文件,使用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/

老外上傳的壓縮算法性能測試數據:

http://pastebin.com/SFaNzRuf

推薦指數:★★★★★

推薦理由:壓縮率和壓縮性能一定程度是矛盾體,如何均衡取決於應用場景。使用合適壓縮算法直接關係到老闆的錢,如果能夠節省成本,體現程序員的價值。

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代碼設置該參數是不需要轉義符的。

總結下:看看我們通過該實踐可以獲得什麼?

  1. 獲取MapReduce運行時間和回調完成時間,可以分析最耗時Job,最快完成Job。
  2. 通過MapReduce運行狀態(包括成功、失敗、Kill),可以第一時間發現錯誤,並通知運維。
  3. 通過獲取MapReduce完成時間,可以第一時間通過用戶,數據已經計算完成,提升用戶體驗

Hadoop這塊功能的源碼文件是JobEndNotifier.java,可以馬上通過本文實踐二看看究竟。其中下面兩個參數就是我通過翻源碼的時候發現的,如果希望使用該實踐趕緊通過ToolRunner設置上吧(別忘了加-D,格式是-Dkey=value)。

  1. job.end.retry.attempts // 設置回調通知retry次數
  2. job.end.retry.interval // 設置回調時間間隔,單位毫秒

當然如果hadoop沒有提供Job狀態通知的功能,我們也可以通過採用阻塞模式提交MapReduce Job,然後Job完成後也可以獲知其狀態和運行時間。

推薦指數:★★★

推薦理由:對mapreduce job監控最省事有效的辦法,沒有之一。


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