由于Map/Reudce Job是运行在hadoop分布式环境中,所以给日常开发map/reduce的时候带来了很多不便,包括调试或者测试等。但是Apache下面一个开源的项目(MRUnit)可以对Map/Reduce进行单元测试,这样就可以使用单元测试用例来对Map/Reduce进行Debug,从而也可能通过丰富的测试用例来进行测试。可以在本地开发机上保证基本业务正确的前提下,再发布到hadoop分布式环境中解决一些分布式带来的问题。
MRUnit的具体使用官网中已经具体的使用说明。(MRUnit web site)
下面主要介绍一下Map/Reudce和MRUnit的最基本的原理,及对MRUnit中进行一些单独的展现来实现一些复杂的业务。
1. MAP
Map的基本原理(为了说明MRUnit在Map的基本原理),Map主要是读取原数据进行map操作,Hadoop Map/Reduce框架在map阶段调用org.apache.hadoop.mapreduce.Mapper.run方法,具体如下:
public void run(Context context) throws IOException, InterruptedException { setup(context); while (context.nextKeyValue()) { map(context.getCurrentKey(), context.getCurrentValue(), context); } cleanup(context); }
从上面的方法,可以看到在run中在开始和结果分别做了setup和cleanup操作,核心操作就是通过map context读取数据,并用读取到的key/value数据调用具体的map逻辑(通常map的逻辑是我们来实现),最终通过context.write把map的结果输出到中间结果(通常以文件形式)中提供reduce使用。
了解了map的基本原因之后,其实MRUnit主要做的就是mock一个map context,用于接受我们在单元测试中模拟数据,及对map的结果与测试用例中期望的结果进行比较。MapDriver通过 withInput和 withOutput来接受input和期望output,调用runTest来执行具体的map测试用例,如下:
mapDriver.withInput(new ChunkKey(), chunkWritable); mapDriver.withOutput(new BytesWritable(SerializeUtil.serializeToBytes(multipleObject)), new BytesWritable(SerializeUtil.serializeToBytes(chunk))); mapDriver.runTest();runTest的具体实现如下,执行具体的run得到map的output,再与期望的output比较:
public void runTest(final boolean orderMatters) { LOG.debug("Mapping input (" + inputKey + ", " + inputVal + ")"); try { final List<Pair<K2, V2>> outputs = run(); validate(outputs, orderMatters); validate(counterWrapper); } catch (final IOException ioe) { LOG.error("IOException in mapper", ioe); throw new RuntimeException("IOException in mapper: ", ioe); } }run的具体实现如下,通过input来mock一个map context,然后调用map的run方法返回map的output。
final List<Pair<K1, V1>> inputs = new ArrayList<Pair<K1, V1>>(); inputs.add(new Pair<K1, V1>(inputKey, inputVal)); final InputSplit inputSplit = new MockInputSplit(); try { final MockMapContextWrapper<K1, V1, K2, V2> wrapper = new MockMapContextWrapper<K1, V1, K2, V2>( inputs, getCounters(), getConfiguration(), inputSplit); final Mapper<K1, V1, K2, V2>.Context context = wrapper.getMockContext(); myMapper.run(context); return wrapper.getOutputs(); } catch (final InterruptedException ie) { throw new IOException(ie); }从上面的代码中可以看到,最主要是需要mock map context,MRUnit中主要mock了map context的nextKeyValue(), getCurrentKey()和getCurrentValue(),mock使用的原数据就是通过withInput输入的值,及write用于把输出写到内部的List对象中,使用 mockito来mock具体的对象,具体如下:
when(context.nextKeyValue()).thenAnswer(new Answer<Boolean>() { @Override public Boolean answer(final InvocationOnMock invocation) { if (inputs.size() > 0) { currentKeyValue = inputs.remove(0); return true; } else { currentKeyValue = null; return false; } } }); when(context.getCurrentKey()).thenAnswer(new Answer<KEYIN>() { @Override public KEYIN answer(final InvocationOnMock invocation) { return currentKeyValue.getFirst(); } }); when(context.getCurrentValue()).thenAnswer(new Answer<VALUEIN>() { @Override public VALUEIN answer(final InvocationOnMock invocation) { return currentKeyValue.getSecond(); } });
doAnswer(new Answer<Object>() { @Override public Object answer(final InvocationOnMock invocation) { final Object[] args = invocation.getArguments(); outputs.add(new Pair(copy(args[0], conf), copy(args[1], conf))); return null; } }).when(context).write((KEYOUT) any(), (VALUEOUT) any());
1.1 Map阶段对MRUnit进行扩展
由于MRUnit只mock了map context中最基本的一些方法,如果使用到别的方法时,需要扩展mock map context,从run方法中可以看到mock出来的context是一个方法级变量,所以无法通过简单的值入来扩展,可以通过继承MapDriver并override run方法来扩展,如果想在setup中通过context来获取map/reduce job name就可以通过如下这种方式来扩展:final Mapper<K1, V1, K2, V2>.Context context = wrapper.getMockContext(); when(context.getJobName()).thenAnswer(new Answer<String>() { @Override public String answer(final InvocationOnMock invocation) { return jobName; } }); myMapper.run(context); return wrapper.getOutputs();2. Reduce
final MockReduceContextWrapper<K1, V1, K2, V2> wrapper = new MockReduceContextWrapper<K1, V1, K2, V2>( inputs, getCounters(), getConfiguration()); final Reducer<K1, V1, K2, V2>.Context context = wrapper.getMockContext(); //mock multiple output object MockMultipleOutputs mockMultipleOutputs = new MockMultipleOutputs(context); //set multiple output setMultipleOutputs(myReducer, mockMultipleOutputs.getMultipleOutputs()); myReducer.run(context); //check output path Assert.assertEquals(outputPath,mockMultipleOutputs.getOutputPath()); //get output list from mock multiple output return mockMultipleOutputs.getOutputs();由于Reduce的结构基本固定,因为map/reduce框架通过这种结构来执行,所以在不改变Reduce结构的前提下,通过反相反射来把mock multiple ouput object注入到reduce中,上面setMultipleOutputs的实现如下:
private void setMultipleOutputs(Reducer<K1, V1, K2, V2> myReducer, MultipleOutputs multipleOutputs) throws Exception { Field field = getFieldByType(myReducer, MultipleOutputs.class); if (field != null) { field.setAccessible(true); field.set(myReducer, multipleOutputs); } } private Field getFieldByType(Reducer<K1, V1, K2, V2> myReducer, Class<?> clazz) { Field[] fields = myReducer.getClass().getDeclaredFields(); Field candidate = null; for (Field field : fields) { if (field.getType().isAssignableFrom(clazz)) { candidate = field; break; } } return candidate; }而mock multiple output主要实现write方法如下,把输入写到内容LIst中,并获取output path来验证reduce输出的目录是否正确:
try {
doAnswer(new Answer<Object>() {
@Override
public Object answer(final InvocationOnMock invocation) {
final Object[] args = invocation.getArguments();
outputPath = (String) args[3];
outputs.add(new Pair(copy(args[1], conf), copy(args[2], conf)));
return null;
}
}).when(output).write(anyString(), any(), any(), anyString());
} catch (Exception e) {
throw new RuntimeException(e);
}
具体使用如下:
reduceDriver.withInput(new BytesWritable(SerializeUtil.serializeToBytes(key)), inputList); reduceDriver.withMultipleOutput("/test/url/20121216/tmp_/m/url-data", new BytesWritable(SerializeUtil.serializeToBytes(url)), new BytesWritable(SerializeUtil.serializeToBytes(urlAnalyzerResult))); reduceDriver.runTest();