我在使用Hadoop編寫MapReduce程序時,遇到了一些問題,通過在Google上查詢資料,並結合自己對Hadoop的理解,逐一解決了這些問題。
自定義Writable
Hadoop對MapReduce中Key與Value的類型是有要求的,簡單說來,這些類型必須支持Hadoop的序列化。爲了提高序列化的性能,Hadoop還爲Java中常見的基本類型提供了相應地支持序列化的類型,如IntWritable,LongWritable,併爲String類型提供了Text類型。不過,這些Hadoop內建的類型並不足以支持真實遇到的業務。此時,就需要自定義Writable類,使得它既能夠作爲Job的Key或者Value,又能體現業務邏輯。
假設我已經從豆瓣抓取了書籍的數據,包括書籍的Title以及讀者定義的Tag,並以Json格式存儲在文本文件中。現在我希望提取這些數據中我感興趣的內容,例如指定書籍的Tag列表,包括Tag被標記的次數。這些數據可以作爲向量,爲後面的數據分析提供基礎數據。對於Map,我希望讀取Json文件,然後得到每本書的Title,以及對應的單個Tag信息。作爲Map的輸出,我希望是我自己定義的類型BookTag。它只包括Tag的名稱和標記次數:
|
注意,在write()與readFields()方法中,對於String類型的處理完全不同於Int、Long等類型,它需要調用Text的相關靜態方法。
針對每本書,Map出來的結果可能包含重複的BookTag信息(指Tag Name相同);而我需要得到每個Tag的標記總和,以作爲數據分析的向量。因此,作爲Reduce的輸入,可以是<Text, Iterable>,但輸出則應該是合併了相同Tag信息的結果。爲此,我引入了BookTags類,在其內部維持了一個BookTag的Map,它同樣需要實現Writable。由於BookTags包含了一個集合類型,因此它的實現會略有不同:
|
其實,針對這種嵌套了集合的自定義Writable類型,由於嵌套的類型同樣實現了Writable接口,因而同樣可以調用嵌套類型的write()與readFields()方法,唯一的區別是需要將集合的Size寫入到DataOutput中,以便於在讀取時可以遍歷集合。這實際上是一種Composite模式。
Iterable的奇怪行爲
我需要在reduce()方法中,遍歷傳入的Iterable,以便於對重複的Tag進行累加操作。在遍歷該對象時,我發現了一個奇怪現象,即最終得到的每本書的Tag信息,全部變成了一樣的內容。通過對Reduce Job進行調試,發現每當遍歷到Iterable的下一個元素時,這個最新的值就會覆蓋之前得到的對象,使其變成同一個對象。通過Google,我發現這個問題是Hadoop的奇怪行爲,即Iterable對象的next()方法永遠會返回同一個對象。解決辦法就是在遍歷時,創建一個新對象放到我們要存儲的集合中,如下第5行代碼所示:
|
這裏得到的一個經驗是,在編寫MapReduce程序時,通過調試可以幫助你快速地定位問題。調試時,可以在項目的根目錄下建立input文件夾,將數據源文件放入到該文件夾中,然後在調試的參數中設置即可。
如何進行單元測試
我們同樣可以給MapReduce Job編寫單元測試。除了可以使用Mockito進行Mock之外,我認爲MRUnit可以更好地完成對MapReduce任務的驗證。MRUnit爲Map與Reduce提供了對應的Driver,即MapDriver與ReduceDriver。在編寫測試用例時,我們只需要爲Driver指定Input與Output,然後執行Driver的runTest()方法,即可測試任務的執行是否符合預期。這種預期是針對output輸出的結果而言。以WordCounter爲例,編寫的單元測試如下:
|
Chaining Job
通過利用Hadoop提供的ChainMapper與ChainReducer,可以較爲容易地實現多個Map Job或Reduce Job的鏈接。例如,我們可以將WordCounter分解爲Tokenizer與Upper Case兩個Map任務,最後執行Reduce。遺憾的是,ChainMapper與ChainReducer似乎不支持新版本的API,它要鏈接的Map與Reduce必須派生自MapReduceBase,並實現對應的Mapper或Reducer接口(說明,下面的代碼基本上來自於StackOverFlow的一個帖子)。
|
不知道什麼時候這種機制能夠很好地支持新版的API。