1前言hadoop的mapreduce提交到集群環(huán)境中出問(wèn)題的定位是比較麻煩的,有時(shí)需要一遍遍的修改代碼和打出日志來(lái)排查一個(gè)很小的問(wèn)題,如果數(shù)據(jù)量大的話調(diào)試起來(lái)相當(dāng)耗時(shí)間。因此有必要使用良好的單元測(cè)試手段來(lái)盡早的消除明顯的bug(當(dāng)然僅有單元測(cè)試是不夠的,畢竟跟集群的運(yùn)行環(huán)境還是不一樣的)。 然而做mapreduce的單元測(cè)試會(huì)有一些障礙,比如Map和Reduce一些參數(shù)對(duì)象是在運(yùn)行時(shí)由hadoop框架傳入的,例如OutputCollector、Reporter、InputSplit等。這就需要有Mock手段。最初寫(xiě)mapreduce單元測(cè)試的時(shí)候自己寫(xiě)了幾個(gè)簡(jiǎn)單的Mock也基本能滿足需要,后來(lái)發(fā)現(xiàn)MRUnit比我寫(xiě)的要好用所以研究了一下就采用了。MRUnit是專門為hadoop mapreduce寫(xiě)的單元測(cè)試框架,API簡(jiǎn)潔明了,簡(jiǎn)單實(shí)用。但也有一些薄弱的地方,比如不支持MultipleOutputs(很多情況下我們會(huì)用MultipleOutputs作為多文件輸出,后面將介紹如何增強(qiáng)MRUnit使之支持MultipleOutputs)。 2 MRUnitMRUnit針對(duì)不同測(cè)試對(duì)象分別使用以下幾種Driver: l MapDriver,針對(duì)單獨(dú)的Map測(cè)試。 l ReduceDriver,針對(duì)單獨(dú)的Reduce測(cè)試。 l MapReduceDriver,將Map和Reduce連貫起來(lái)測(cè)試。 l PipelineMapReduceDriver,將多個(gè)Map-Reduce pair貫串測(cè)試。 MapDriver單獨(dú)測(cè)試Map的例子,假設(shè)我們要計(jì)算一個(gè)賣家的平均發(fā)貨速度。Map將搜集每一次發(fā)貨的時(shí)間間隔。針對(duì)Map的測(cè)試, //這是被測(cè)試的Map private Map mapper; private MapDriver<LongWritable, Text, Text, TimeInfo> mapDriver; @Before public void setUp() { mapper = new Map(); mapDriver = new MapDriver<LongWritable, Text, Text, TimeInfo>(); }
@Test public void testMap_timeFormat2() { String sellerId = "444"; //模擬輸入一行(withInput),假設(shè)從這行數(shù)據(jù)中我們可以獲得賣家(sellerId) //某一次時(shí)間間隔 為10小時(shí). //我們期望它輸出sellerId為key,value為代表1次10小時(shí)的TimeInfo對(duì)象。 //(withOutput) //如果輸入數(shù)據(jù)經(jīng)過(guò)Map計(jì)算后為期望的結(jié)果,那么測(cè)試通過(guò)。 Text mapInputValue = new Text("……"); mapDriver.withMapper(mapper) .withInput(null, mapInputValue) .withOutput(new Text(sellerId), new TimeInfo(1, 10)) .runTest(); } ReduceDriver針對(duì)Reduce的單獨(dú)測(cè)試,還是這個(gè)例子。Reduce為根據(jù)Map或Combiner輸出的n次時(shí)間間隔的總和來(lái)計(jì)算平均時(shí)間。 private Reduce reducer; @Before public void setUp() { reducer = new Reduce(); reduceDriver = new ReduceDriver<Text, TimeInfo, Text, LongWritable>(reducer); }
@Test public void testReduce () { List<TimeInfo> values = new ArrayList<TimeInfo>(); values.add(new TimeInfo(1, 3));//一次3小時(shí) values.add(new TimeInfo(2, 5));//兩次總共5小時(shí) values.add(new TimeInfo(3, 7));//三次總共7小時(shí) //values作為444這個(gè)賣家的reduce輸入, //期望計(jì)算出平均為2小時(shí) reduceDriver.withReducer(reducer) .withInput(new Text("444"), values) .withOutput(new Text("444"),new LongWritable(2)) .runTest(); } MapReduceDriver以下為Map和Reduce聯(lián)合測(cè)試的例子, private MapReduceDriver<LongWritable, Text, Text, TimeInfo, Text, LongWritable> mrDriver; private Map mapper; private Reduce reducer; @Before public void setUp() { mapper = new Map(); reducer = new Reduce(); mrDriver = new MapReduceDriver<LongWritable, Text, Text, TimeInfo, Text, LongWritable>(mapper, reducer); }
@Test public void testMapReduce_3record_1user() { Text mapInputValue1 = new Text("……"); Text mapInputValue2 = new Text("……"); Text mapInputValue3 = new Text("……"); //我們期望從以上三條Map輸入計(jì)算后, //從reduce輸出得到444這個(gè)賣家的平均時(shí)間為2小時(shí). mrDriver.withInput(null, mapInputValue1) .withInput(null, mapInputValue2) .withInput(null, mapInputValue3) .withOutput(new Text("444"),new LongWritable(2)) .runTest(); }
3 增強(qiáng)MRUnit下面介紹為MRUnit框架增加了支持MultipleOutputs、從文件加載數(shù)據(jù)集和自動(dòng)裝配等幾個(gè)特性,使它更加便于使用。 如何支持MultipleOutputs然而很多場(chǎng)景下我們需要使用MultipleOutputs作為reduce的多文件輸出,MRUnit缺少支持。分析源碼后為MRUnit增強(qiáng)擴(kuò)展了兩個(gè)Driver:ReduceMultipleOutputsDriver和MapReduceMultipleOutputDriver來(lái)支持MultipleOutputs。
ReduceMultipleOutputsDriverReduceMultipleOutputsDriver是ReduceDriver的增強(qiáng)版本,假設(shè)前面例子中的Reduce使用了MultipleOutputs作為輸出,那么Reduce的測(cè)試將出現(xiàn)錯(cuò)誤。
使用ReduceMultipleOutputsDriver改造上面的測(cè)試用例(注意粗體部分), private Reduce reducer; @Before public void setUp() { reducer = new Reduce(); //注意這里ReduceDriver改為使用ReduceMultipleOutputsDriver reduceDriver = new ReduceMultipleOutputsDriver<Text, TimeInfo, Text, LongWritable>(reducer); }
@Test public void testReduce () { List<TimeInfo> values = new ArrayList<TimeInfo>(); values.add(new TimeInfo(1, 3));//一次3小時(shí) values.add(new TimeInfo(2, 5));//兩次總共5小時(shí) values.add(new TimeInfo(3, 7));//三次總共7小時(shí) //values作為444這個(gè)賣家的reduce輸入, //期望計(jì)算出平均為2小時(shí) reduceDriver.withReducer(reducer) .withInput(new Text("444"), values) //Note //假設(shè)使用id(444)%8的方式來(lái)分文件 //表示期望"somePrefix"+444%8這個(gè)collector將搜集到數(shù)據(jù)xxx . withMutiOutput ("somePrefix"+444%8,new Text("444"),new LongWritable(2)) .runTest(); }
MapReduceMultipleOutputDriver跟ReduceMultipleOutputsDriver類似,MapReduceMultipleOutputDriver用來(lái)支持使用了MultipleOutputs的Map-Reduce聯(lián)合測(cè)試。MapReduceDriver一節(jié)中的例子將改為, private MapReduceDriver<LongWritable, Text, Text, TimeInfo, Text, LongWritable> mrDriver; private Map mapper; private Reduce reducer; @Before public void setUp() { mapper = new Map(); reducer = new Reduce(); //改為使用ReduceMultipleOutputsDriver mrDriver = new ReduceMultipleOutputsDriver<LongWritable, Text, Text, TimeInfo, Text, LongWritable>(mapper, reducer); }
@Test public void testMapReduce_3record_1user() { Text mapInputValue1 = new Text("……"); Text mapInputValue2 = new Text("……"); Text mapInputValue3 = new Text("……"); //我們期望從以上三條Map輸入計(jì)算后, //從reduce輸出得到444這個(gè)賣家的平均時(shí)間為2小時(shí). mrDriver.withInput(null, mapInputValue1) .withInput(null, mapInputValue2) .withInput(null, mapInputValue3) //表示期望"somePrefix"+444%8這個(gè)collector將搜集到數(shù)據(jù)xxx . withMutiOutput ("somePrefix"+444%8,new Text("444"),new LongWritable(2)) .runTest(); }
如何從文件加載輸入從以上例子看到使用MRUnit需要重復(fù)寫(xiě)很多類似的代碼,并且需要把輸入數(shù)據(jù)寫(xiě)在代碼中,顯得不是很優(yōu)雅,如果能從文件加載數(shù)據(jù)則會(huì)方便很多。因此通過(guò)使用annotation和擴(kuò)展JUnit runner,增強(qiáng)了MRUnit來(lái)解決這個(gè)問(wèn)題。 改造上面的例子,使得map的輸入自動(dòng)從文件加載,并且消除大量使用MRUnit框架API的代碼。 @RunWith(MRUnitJunit4TestClassRunner.class) public class XXXMRUseAnnotationTest {
//表示自動(dòng)初始化mrDriver,并加載數(shù)據(jù)(如果需要) @MapInputSet @MapReduce(mapper = Map.class, reducer = Reduce.class) private MapReduceDriver<LongWritable, Text, Text, TimeInfo, Text, LongWritable> mrDriver;
@Test @MapInputSet("ConsignTimeMRUseAnnotationTest.txt")//從這里加載輸入數(shù)據(jù) public void testMapReduce_3record_1user() { //只需要編寫(xiě)驗(yàn)證代碼 mrDriver. withMutiOutput ("somePrefix"+444%8,new Text("444"),new LongWritable(2)) .runTest(); } } |
|
來(lái)自: 風(fēng)自向前 > 《Hadoop》