Hadoop之MapReduce(實踐篇)

1、MapReduce編程模型概述

MapReduce應用廣泛的原因之一在於它的易用性。它提供了一個因高度抽象化而變得異常簡單的編程模型。MapReduce是在總結大量應用的共同特點的基礎上抽象出來的分佈式計算框架,它適用的應用場景往往具有一個共同的特點:任務可被分解成相互獨立的子問題。基於該特點,MapReduce編程模型給出了其分佈式編程方法,共分5個步驟:

  • 迭代(iteration):遍歷輸入數據,並將之解析成key/value對;

  • 將輸入key/value對映射(map)成另外一些key/value對;

  • 依據key對中間數據進行分組(grouping);

  • 以組爲單位對數據進行規約(reduce);

  • 迭代。將最終產生的key/value對保存到輸出文件中。

MapReduce將計算過程分解成以上5個步驟帶來的最大好處是組件化和並行化。

爲了實現MapReduce編程模型,Hadoop設計了一系列對外編程接口。從MapReduce自身的命名特點可以看出,MapReduce由兩個階段組成:Map階段和Reduce階段。用戶只需要編寫map()和reduce()兩個方法,即可完成簡單的分佈式程序的設計實現。

map()方法以key/value對作爲輸入,產生另外一系列key/value對作爲中間輸出寫入本地磁盤。MapReduce框架會自動將這些中間數據按照key值進行聚集,且key值相同(用戶可以設定聚集策略,默認情況下是對key值進行哈希取模)的數據被統一交給reduce()方法處理。

reduce()方法以key及對應的value列表作爲輸入,經合併key相同的value值後,產生另外一系列key/value對作爲最終輸出寫入HDFS。

2、MapReduce編程入門之”HelloWorld”

下面以MapReduce中的“helloworld”程序——WordCount爲例介紹程序設計方法。

“hello world”程序是我們學習任何一門編程語言編寫的第一個程序,它簡單且易於理解,能夠幫助我們快速入門。同樣,分佈式處理框架也有自己的“hello world”程序:WordCount。它完成的功能是統計輸入文件中的每個單詞出現的次數。

下面是本人自己編寫的HelloWorld程序——MyWordCount。

MyWordCountMapper.java程序代碼,如下圖所示。因爲MyWordCount是這篇文章的第一個程序,因此在這裏就做詳細的解釋,後續的程序就只做簡單的註釋了。

這裏寫圖片描述

MyWordCountReducer.java程序代碼,如下圖所示。

這裏寫圖片描述

MyWordCountMain.java主程序代碼,如下圖所示。

這裏寫圖片描述

編寫完MapReduce程序後,按照一定的規則制定程序的輸入和輸出目錄,並提交到Hadoop集羣中。作業在Hadoop中的執行過程如下圖所示。Hadoop將輸入數據切分成若干個輸入分片(input split),並將每個split交給一個Map Task處理;Map Task不斷地從對應的split中解析出一個個key/value對,並調用map()方法進行處理,處理完之後根據Reduce Task個數將結果分成若干個分區(partition)寫到本地磁盤;同時,每個Reduce Task從每個Map Task上讀取屬於自己的那個partition,然後使用基於排序的方法將key相同的數據聚集在一起,調用reduce()方法進行處理,並將最終結果輸出到文件中。

下圖爲MyWordCount程序執行的過程。

這裏寫圖片描述

MyWordCount程序在HDFS上執行的結果,如下圖所示。

這裏寫圖片描述

細心的你也許已經注意到,上面的程序還缺少三個基本組件,它們的功能分別是:1、指定輸入文件格式。將輸入數據切分成若干個split,且將每個split中的數據解析成一個個map()方法要求的key/value對;2、確定map()方法產生的每個key/value對發給哪個Reduce Task方法處理;3、指定輸出文件格式,即每個key/value對以何種形式保存到輸出文件中。

在Hadoop MapReduce中,這三個組件分別是InputFormat、Partitioner和OutputFormat,它們均需要用戶根據自己的應用需求進行配置。而對於上面的WordCount程序實例,默認情況下Hadoop採用的默認實現正好可以滿足要求,因而不必再提供。綜合來看,Hadoop MapReduce對外提供了5個可編程組件,分別是InputFormat、Mapper、Partitioner、Reducer以及OutputFormat。

下面再舉一個比較簡單的例子,對一張員工表求每個部門的工資總額,表中的數據內容如下圖所示,每一行的對應的列名分別爲:

1、員工號;2、員工姓名;3、職務;4、老闆號;5、入職日期;6、工資;7、獎金;8、部門號。

這裏寫圖片描述

該實例的MapReduce程序如下。

MyCountSalarySumMapper.java程序代碼,如下圖所示。

這裏寫圖片描述

MyCountSalarySumReducer.java程序代碼,如下圖所示。

這裏寫圖片描述

MyCountSalarySumMain.java主程序代碼,如下圖所示。

這裏寫圖片描述

該實例的MapReduce程序運行結果,如下圖所示。

這裏寫圖片描述

3、MapReduce的核心——Shuffle(洗牌)

Shuffle的本意是洗牌,它把一組有一定規則的數據儘可能地轉換成一組無規則的數據,越隨機越好。MapReduce中的Shuffle過程更像是洗牌的逆過程,把一組無規則的數據儘可能地轉換成一組具有一定規則的數據。

從前面的編程實踐中我們已經知道,MapReduce計算模型包括兩個重要的階段:Map是映射,負責數據的過濾分發;Reduce是規約,負責數據的計算歸併。Reduce的數據來源於Map,Map的輸出就是Reduce的輸入,Reduce通過Shuffle過程來獲取數據。

從Map輸出到Reduce輸入的整個過程可以廣義地稱作爲Shuffle。Shuffle橫跨Map端和Reduce端,在Map端包含Partition、Sort和Spill過程,在Reduce端包含copy和sort過程,整個Shuffle過程如下圖所示:

這裏寫圖片描述

A、Map端

當Map程序開始產生結果的時候,並不是直接寫到文件,而是利用緩存做一些排序方面的預處理工作。每個Map任務都有一個循環內存緩衝區(默認爲100MB),當緩存的數據大小達到80%時,後臺Spill線程就會將緩存的數據寫出到文件,此時Map任務可以繼續輸出結果,但如果緩衝區滿了,則Map任務將暫停等待。

Map程序寫出到文件使用round-robin方式,在寫出到文件之前,先將數據按照Reduce進行分區,對於每一個分區,都會在內存中根據key進行排序,如果配置了Combiner,則排序後執行Combiner(Combiner的使用,可以減少寫入文件和傳輸的數據,提高MapReduce程序的運行效率)。

每當Map程序輸出的結果達到緩衝區的閾值時,都會創建一個文件,在Map程序運行結束時,可能會產生大量的文件。在Map完成前,會將這些文件進行合併和排序,如果文件的數量超過3個,則合併後會再次運行Combiner(1或2個就沒有這個必要了)。如果配置了壓縮,則最終寫入的文件會先進行壓縮,這樣可以減少寫入和傳輸的數據。一旦Map完成,則通知任務管理器,此時Reduce程序就可以開始複製Map的結果數據。

B、Reduce端

Reduce任務通過HTTP向各個Map任務拖取它所需要的數據,每個節點都會啓動一個常駐的HTTP Server,其中一項服務就是響應Reduce拖取Map數據。當有MapOutput的HTTP請求過來的時候,HTTP Server就讀取相應的Map輸出文件中對應這個Reduce部分的數據,然後通過網絡流輸出給Reduce任務。

Map輸出的結果數據都存放在運行Map任務的機器的本地磁盤上,如果Map輸出的結果數據較小就直接讀入到內存中,如果Map輸出的結果數據較大,則需要存放到磁盤上。這樣Reduce任務拖過來的數據有些放在內存中,有些放在磁盤上,需要先對這些數據做一個全局合併操作後,再執行Reduce方法,最終Reduce結果輸出到HDFS文件系統中。

4、MapReduce的特性

A、排序

數據排序是執行許多任務時要完成的第一步工作,比如學生成績評比、數據建立索引等。MapReduce程序默認已經對輸出到HDFS文件的數據進行了排序,它是根據Map端的輸出key值來進行排序,如果key值的數據類型爲數值型,則按照數值升序排序;如果key值爲字符串型,則按照字典升序排序。如果需要MapReduce程序按照我們的意願對輸出數據進行排序,則需要定義自己的比較器,下面仍使用員工表來測試該程序,代碼如下。

MySalarySortMapper.java程序代碼,如下圖所示。

這裏寫圖片描述

MySalarySortComparator.java比較器程序代碼,如下圖所示。

這裏寫圖片描述

MySalarySortMain.java主程序代碼,如下圖所示。

這裏寫圖片描述

該實例的MapReduce程序運行結果,如下圖所示,第一列是按降序排序的員工工資,第二列是員工號。

這裏寫圖片描述

B、序列化

在MapReduce程序中,數據通常需要通過網絡進行傳輸或者持久化到本地文件,因而需要對使用到的數據類型進行序列化,在MapReduce中,其基本數據類型都實現了序列化,因而可以直接進行使用。在MapReduce程序中,對一個實體類進行序列化,只需要在類定義時實現Writable接口即可。下面舉一個按照類中的多個數據進行排序的實例(對員工表中的數據,按照部門號和工資進行升序排序),由於該程序不僅使用到了數據實體類,還需要使用到比較器Comparator,因此需要繼承WritableComparable接口。該實例代碼如下。

注意:下面MyEmployeeEntity實體類get和set方法代碼沒有給出。

MyEmployeeEntity.java員工實體類程序代碼,如下圖所示。

這裏寫圖片描述
這裏寫圖片描述

MyEmployeeObjectSortMapper.java程序代碼,如下圖所示。

這裏寫圖片描述
這裏寫圖片描述

MyEmployeeObjectSortMain.java主程序代碼,如下圖所示。

這裏寫圖片描述

該實例的MapReduce程序運行結果如下圖所示。從圖中可以看到,最後一列部門號是按照10,20,30升序排序,在部門號相同的情況下,又按照倒數第二列員工工資升序進行排序。

這裏寫圖片描述

C、分區

在MapReduce程序中,使用分區可以將數據進行歸類存儲,並且能夠提高數據查詢的效率。默認情況下,在MapReduce中只有一個分區,使用的是HashPartitioner,根據key的hashcode%reducetask的結果來分區,一個分區對應HDFS上的一個輸出文件。如果要按照我們自己的需求進行分區,則需要自定義數據分發組件繼承抽象類:Partitioner。需要注意的是,分區是按照Map端輸出key的值來進行計算的。下面舉一個實例,對員工表按照部門號進行分區,代碼如下。

MyPartitionMapper.java程序代碼,如下圖所示。

這裏寫圖片描述

MyPartitionPartitioner.java分區組件類程序代碼,如下圖所示。

這裏寫圖片描述

MyPartitionReducer.java程序代碼,如下圖所示。

這裏寫圖片描述

MyPartitionMain.java主程序代碼,如下圖所示。

這裏寫圖片描述

該實例的MapReduce程序運行結果,如下圖所示。從圖中可以看到,程序的結果分別存放到了三個文件中,每個文件中存放的是一類部門號的員工。

這裏寫圖片描述

D、合併

每一個Map可能會產生大量的輸出,Combiner的作用就是在Map端對輸出先做一次合併操作,以減少傳輸到Reducer端的數據量。Combiner最基本的功能是實現本地key的歸併,Combiner具有類似本地Reducer的作用。如果不用Combiner,那麼所有的結果都是Reducer完成,效率會相對低下;使用Combiner,先完成的Map會在本地進行聚合,提升速度。

需要注意的是,Combiner的使用要非常謹慎,因爲它在MapReduce過程中可能調用也可能不調用,可能調用一次也可能調用多次。Combiner的輸出是Reducer的輸入,如果Combiner是可插拔的,添加Combiner決不能改變最終的計算結果,所以Combiner只應該用於Reducer的輸入key/value與輸出key/value類型完全一致,且不影響最終結果的場合,比如累加、求最大值等。

下面舉個實例,求多個文件中所有數據的累加和,代碼如下。

MyCombineMapper.java程序代碼,如下圖所示。

這裏寫圖片描述

MyCombineCombiner.java連接器組件類程序代碼,如下圖所示。

這裏寫圖片描述

MyCombinerReducer.java程序代碼,如下圖所示。

這裏寫圖片描述

MyCombinerMain.java主程序代碼,如下圖所示。

這裏寫圖片描述

程序的輸入路徑爲:/data/,該目錄下包含三個文件,分別爲data1.txt,data2.txt,data3.txt,,程序運行時會依次讀取這三個文件中的內容,最終運行結果如下圖所示:

這裏寫圖片描述

5、使用MapReduce實現倒排索引

倒排索引是文檔檢索系統中最常用的數據結構,被廣泛地應用於全文搜索引擎。它主要是用來存儲某個單詞(或詞組)在一個文檔或一組文檔中的存儲位置的映射,即提供了一種根據內容來查找文檔的方法。由於不是根據文檔來確定文檔所包含的內容,而是進行相反的操作,因而被稱爲倒排索引(Inverted Index)。

通常情況下,倒排索引由一個單詞(或詞組)以及相關的文檔列表組成,文檔列表中的文檔或者是標識文檔的ID號,或者是指文檔所在位置的URL,如下圖所示:

這裏寫圖片描述

從上圖可以看到,單詞1出現在了{文檔1,文檔5,文檔8,… …}中,單詞2出現在了{文檔2,文檔6,文檔12,… …}中,而單詞3出現在了{文檔1,文檔5,文檔16,… …}中。在實際應用場景中,通常還需要給每個文檔添加一個權重值,用來表示每個文檔與搜索內容的相關度,如下圖所示。

這裏寫圖片描述

最常見的是使用詞頻來作爲權重值,即記錄單詞在文檔中出現的次數。以英文單詞爲例,如下圖所示,索引文件中的“MapReduce”一行表示:“MapReduce”這個單詞在文本T0中出現過一次,在文本T1中出現過一次,在文本T2中出現過一次。當搜索條件爲“MapReduce”、“is”、“Simple”時,對應的集合爲:{T0,T1,T2}∩{T0,T1}∩{T0,T1}={T0,T1},即文檔T0和T1包含了所要索引的全部單詞,而且只有T0是連續的。

這裏寫圖片描述

更復雜的權重還可能要記錄單詞在多少個文檔中出現過,或者考慮單詞在文檔中的位置信息(單詞是否出現在標題中,反映了單詞在文檔中的重要性)等。下面舉一個倒排索引的實例:

這裏寫圖片描述
這裏寫圖片描述

實現上述倒排索引實例的MapReduce程序代碼如下。

MyRevertedIndexMapper.java程序代碼,如下圖所示。

這裏寫圖片描述

MyRevertedIndexReducer.java程序代碼,如下圖所示。

這裏寫圖片描述
這裏寫圖片描述

MyRevertedIndexMain.java主程序代碼,如下圖所示。

這裏寫圖片描述

該實例的MapReduce程序運行結果如下圖所示。從圖中可以看到,輸出結果按照單詞的字母順序進行了排序,這是MapReduce程序默認做的,結果與上面分析給出的圖是一致的。

這裏寫圖片描述

6、使用MRUnit進行單元測試

使用MRUnit對MapReduce程序進行單元測試,需要從官網下載相應的jar包,下載地址爲:http://mrunit.apache.org/。其基本原理是結合使用了JUnit和EasyMock,核心的單元測試依賴於JUnit,並且MRUnit實現了一套Mock對象來控制MapReduce框架的輸入和輸出,從整體上來說,語法比較簡單。在使用的過程中,需要特別注意的是,將mrunit-1.1.0-hadoop2.jar添加到Build Path中,並且將mockito-all-1.8.5.jar從Build Path中去掉。

下面以MyWordCount程序爲例,簡單介紹下如何使用MRUnit對MapReduce程序進行單元測試。

測試MyWordCountMapper類的基本功能的單元測試方法代碼,如下圖所示。我們需要很清楚該類的功能邏輯,對於輸入的一條數據,輸出的結果是怎樣的,這樣才能在單元測試方法代碼中對輸入和輸出數據進行指定。當測試成功時,Eclipse工具會在界面左側的Junit區域以綠色進行提示。

這裏寫圖片描述

測試MyWordCountReducer類的基本功能的單元測試方法代碼,如下圖所示。同樣地,我們也需要知道該類的功能邏輯,輸入數據和輸出數據分別是怎樣的,然後在測試方法代碼中進行指定。由於Reducer的輸入數據中,Value數據是一個集合,因此在測試方法中使用列表進行指定。

這裏寫圖片描述

測試整個MyWordCount MapReduce程序的基本功能的單元測試方法代碼,如下圖所示。這個測試方法將自己編寫的Mapper類和Reducer類結合起來進行單元測試,由於MapReduce程序默認對數字和字母進行升序排序,因此在指定Reducer輸出數據時,需要嚴格按照順序進行設定,否則程序運行會失敗。

這裏寫圖片描述


更多大數據技術精彩內容,歡迎關注
這裏寫圖片描述

參考文獻:

——《Hadoop技術內幕 深入理解MapReduce架構設計與實現原理》
——《CSDN其他博文》
——《潭州大數據課程課件》

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