Статьи

Эффективные стратегии тестирования для приложений MapReduce

Эффективные стратегии тестирования для приложений MapReduce

В этой статье я демонстрирую различные стратегии, которые я использовал для тестирования приложений Hadoop MapReduce, и обсуждаю плюсы и минусы каждого из них. Я начну с почтенного примера WordCount, слегка изменив его рефакторинг, чтобы продемонстрировать модульное тестирование частей маппера и редуктора. Далее я покажу, как MRUnit можно использовать для этих же модульных тестов, а также для тестирования картографа и редуктора вместе. Наконец, я покажу, как драйвер задания можно протестировать с локальным исполнителем задания с использованием тестовых данных в локальной файловой системе.

WordCount

В качестве примера используется собственный WordCount Hadoop . Первоначальный источник из учебника Hadoop приведен в листинге 1.

public class WordCount {

    public static class Map extends MapReduceBase implements Mapper<LongWritable, Text, Text, IntWritable> {
        private final static IntWritable one = new IntWritable(1);
        private Text word = new Text();

        public void map(LongWritable key, Text value, OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException {
            String line = value.toString();
            StringTokenizer tokenizer = new StringTokenizer(line);
            while (tokenizer.hasMoreTokens()) {
                word.set(tokenizer.nextToken());
                output.collect(word, one);
            }
        }
    }

    public static class Reduce extends MapReduceBase implements Reducer<Text, IntWritable, Text, IntWritable> {
        public void reduce(Text key, Iterator<IntWritable> values, OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException {
            int sum = 0;
            while (values.hasNext()) {
                sum += values.next().get();
            }
            output.collect(key, new IntWritable(sum));
        }
    }

    public static void main(String[] args) throws Exception {
        JobConf conf = new JobConf(WordCount.class);
        conf.setJobName("wordcount");

        conf.setOutputKeyClass(Text.class);
        conf.setOutputValueClass(IntWritable.class);

        conf.setMapperClass(Map.class);
        conf.setCombinerClass(Reduce.class);
        conf.setReducerClass(Reduce.class);

        conf.setInputFormat(TextInputFormat.class);
        conf.setOutputFormat(TextOutputFormat.class);

        FileInputFormat.setInputPaths(conf, new Path(args[0]));
        FileOutputFormat.setOutputPath(conf, new Path(args[1]));

        JobClient.runJob(conf);
    }
}

Листинг 1. Оригинальная версия WordCount в Hadoop

Первым этапом любой стратегии тестирования является модульное тестирование, и MapReduce ничем не отличается. Для правильного модульного тестирования WordCount я решил немного реорганизовать его, исключив устаревшие вызовы API и переместив внутренние классы преобразователя и редуктора в классы верхнего уровня. Хотя внутренние классы удобны для примера и могли бы быть достаточно легко протестированы на модульном уровне, я полагаю, что их выделение делает лучший дизайн и обеспечивает большую гибкость в реальных ситуациях. (Представьте себе случай использования пропуска общих слов, таких как «a», «an» и «the» — другой преобразователь может быть легко заменен существующим объединителем / редуктором. Аналогично, представьте себе, что переменный вес применяется к словам с другим редуктор.)

Для облегчения модульного тестирования я внес одно дополнительное изменение, о котором я расскажу в следующем разделе. Реорганизованная версия приведена в листинге 2. Весь исходный код этой статьи доступен на github путем клонирования git@github.com: tequalsme / hadoop-examples.git.

public class WordCount extends Configured implements Tool {

    @Override
    public int run(String[] args) throws Exception {
        Configuration conf = getConf();

        Job job = new Job(conf);
        job.setJarByClass(WordCount.class);

        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        job.setMapperClass(WordCountMapper.class);
        job.setCombinerClass(WordCountReducer.class);
        job.setReducerClass(WordCountReducer.class);

        job.setInputFormatClass(TextInputFormat.class);
        job.setOutputFormatClass(TextOutputFormat.class);

        FileInputFormat.setInputPaths(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        return (job.waitForCompletion(true) ? 0 : 1);
    }

    public static void main(String[] args) throws Exception {
        int res = ToolRunner.run(new WordCount(), args);
        System.exit(res);
    }
}
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    private final static IntWritable one = new IntWritable(1);

    // protected to allow unit testing
    protected Text word = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        String line = value.toString();
        StringTokenizer tokenizer = new StringTokenizer(line);
        while (tokenizer.hasMoreTokens()) {
            word.set(tokenizer.nextToken());
            context.write(word, one);
        }
    }
}
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {        @Override    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {        int sum = 0;        for (IntWritable val : values) {            sum += val.get();        }        context.write(key, new IntWritable(sum));    }}

Листинг 2. WordCount, реорганизованный для тестирования

 

 

Модульное тестирование с помощью Mocks

Используя реорганизованную версию WordCount, мы готовы создавать модульные тесты для мапперов и редукторов. В каждом случае это делается путем насмешки объекта Context и проверки правильности поведения. Я выбрал Mockito для моей насмешливой основы.

Модульный тест mapper показан в листинге 3. В init () создается объект mapper вместе с макетом Context. Методы тестирования отправляют содержимое в маппер и проверяют, что в макете контекста были вызваны правильные методы.

public class WordCountMapperTest {    private WordCountMapper mapper;    private Context context;    private IntWritable one;        @Before    public void init() throws IOException, InterruptedException {        mapper = new WordCountMapper();        context = mock(Context.class);        mapper.word = mock(Text.class);        one = new IntWritable(1);    }    @Test    public void testSingleWord() throws IOException, InterruptedException {        mapper.map(new LongWritable(1L), new Text("foo"), context);                InOrder inOrder = inOrder(mapper.word, context);        assertCountedOnce(inOrder, "foo");    }        @Test    public void testMultipleWords() throws IOException, InterruptedException {        mapper.map(new LongWritable(1L), new Text("one two three four"), context);                InOrder inOrder = inOrder(mapper.word, context, mapper.word, context, mapper.word, context, mapper.word, context);                assertCountedOnce(inOrder, "one");        assertCountedOnce(inOrder, "two");        assertCountedOnce(inOrder, "three");        assertCountedOnce(inOrder, "four");    }        private void assertCountedOnce(InOrder inOrder, String w) throws IOException, InterruptedException {      inOrder.verify(mapper.word).set(eq(w));      inOrder.verify(context).write(eq(mapper.word), eq(one));    }}

Листинг 3. WordCountMapperTest

Небольшое изменение, о котором я упоминал ранее, заключается в том, что я сделал защищенной переменную-член word в WordCountMapper и заменил ее на макет в модульном тесте. Это было необходимо, потому что Hadoop повторно использует объекты между последовательными вызовами map () (а также lower ()); Вот почему Mapper может вызывать «word.set (tokenizer.nextToken ());» вместо «word = new Text (tokenizer.nextToken ());». Это сделано из соображений производительности, но создает проблемы при тестировании. В «testMultipleWords ()» Mockito не может проверять последовательные записи в объекты, которые используются повторно. Но, посмеиваясь над объектом Text и используя InOrder от Mockito, можно правильно написать тест. Я думаю сделать одно небольшое изменение в картографе, чтобы облегчить тестирование; если вы не согласны,рассмотрите возможность использования другой среды для насмешек, которая может не ставить эту проблему.

Тестирование редуктора похоже на тестирование картографа, без осложнений для повторно используемого объекта Text. Модульный тест редуктора приведен в листинге 4.

public class WordCountReducerTest {    private WordCountReducer reducer;    private Context context;        @Before    public void init() throws IOException, InterruptedException {        reducer = new WordCountReducer();        context = mock(Context.class);    }    @Test    public void testSingleWord() throws IOException, InterruptedException {        List<IntWritable> values = Arrays.asList(new IntWritable(1), new IntWritable(4), new IntWritable(7));                reducer.reduce(new Text("foo"), values, context);                verify(context).write(new Text("foo"), new IntWritable(12));    }}

Листинг 4. WordCountReducerTest

 

Счетчики тестирования

Счетчики можно тестировать аналогичным образом: макетировать счетчик, получать ссылку на макет при доступе и проверять, правильно ли он был увеличен. Сопоставитель, который использует Counter, показан в листинге 5, а его модульный тест — в листинге 6.

public class WordCountMapperWithCounter extends Mapper<LongWritable, Text, Text, IntWritable> {    private final static IntWritable one = new IntWritable(1);        enum Counters {        TOTAL_WORDS    }        // protected to allow unit testing    protected Text word = new Text();    @Override    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {        String line = value.toString();        StringTokenizer tokenizer = new StringTokenizer(line);        while (tokenizer.hasMoreTokens()) {            word.set(tokenizer.nextToken());            context.write(word, one);            context.getCounter(Counters.TOTAL_WORDS).increment(1);        }    }}

Листинг 5. WordCountMapperWithCounter

public class WordCountMapperWithCounterTest {    private WordCountMapperWithCounter mapper;    private Context context;    private Counter counter;    private IntWritable one;    @Before    public void init() throws IOException, InterruptedException {        mapper = new WordCountMapperWithCounter();        context = mock(Context.class);        mapper.word = mock(Text.class);        one = new IntWritable(1);        counter = mock(Counter.class);        when(context.getCounter(WordCountMapperWithCounter.Counters.TOTAL_WORDS)).thenReturn(counter);    }    @Test    public void testSingleWord() throws IOException, InterruptedException {        mapper.map(new LongWritable(1L), new Text("foo"), context);        InOrder inOrder = inOrder(mapper.word, context, counter);        assertCountedOnce(inOrder, "foo");    }    @Test    public void testMultipleWords() throws IOException, InterruptedException {        mapper.map(new LongWritable(1L), new Text("one two three four"), context);        InOrder inOrder = inOrder(mapper.word, context, counter, mapper.word, context, counter, mapper.word, context,                counter, mapper.word, context, counter);        assertCountedOnce(inOrder, "one");        assertCountedOnce(inOrder, "two");        assertCountedOnce(inOrder, "three");        assertCountedOnce(inOrder, "four");    }    private void assertCountedOnce(InOrder inOrder, String w) throws IOException, InterruptedException {        inOrder.verify(mapper.word).set(eq(w));        inOrder.verify(context).write(eq(mapper.word), eq(one));        inOrder.verify(counter).increment(1);    }}

 

Листинг 6. WordCountMapperWithCounterTest

MRUnit

Во многих случаях может быть достаточно модульного тестирования картографа и редуктора с помощью макетов. Однако существует альтернативный подход, который может предложить дополнительный уровень охвата. MRUnit — это модуль модульного тестирования для Hadoop. Он начинался как предложение с открытым исходным кодом, включенное в дистрибутив Cloudera для Hadoop, и в настоящее время является проектом Apache Incubator. Он предоставляет классы для тестирования картографов и редукторов по отдельности и вместе.

Тесты WordCount с использованием MRUnit показаны в листинге 7. Этот класс тестирует маппер изолированно, редуктор изолированно, а маппер и редуктор вместе как единое целое. В «setup ()» драйверы создаются для преобразователя, преобразователя и преобразователя, а классы WordCount задаются для драйверов. В «testMapper ()» вызов «withInput ()» передает вход в маппер, а последовательность вызовов «withOutput ()» устанавливает ожидаемый результат. Затем «runTest ()» выполняет отображение и проверяет ожидаемый результат.

public class WordCountMRUnitTest {    MapReduceDriver<LongWritable, Text, Text, IntWritable, Text, IntWritable> mapReduceDriver;    MapDriver<LongWritable, Text, Text, IntWritable> mapDriver;    ReduceDriver<Text, IntWritable, Text, IntWritable> reduceDriver;        @Before    public void setup() {        WordCountMapper mapper = new WordCountMapper();        WordCountReducer reducer = new WordCountReducer();        mapDriver = new MapDriver<LongWritable, Text, Text, IntWritable>();        mapDriver.setMapper(mapper);        reduceDriver = new ReduceDriver<Text, IntWritable, Text, IntWritable>();        reduceDriver.setReducer(reducer);        mapReduceDriver = new MapReduceDriver<LongWritable, Text, Text, IntWritable, Text, IntWritable>();        mapReduceDriver.setMapper(mapper);        mapReduceDriver.setReducer(reducer);                Configuration conf = new Configuration();        // add config here as needed        mapReduceDriver.setConfiguration(conf);        reduceDriver.setConfiguration(conf);        mapDriver.setConfiguration(conf);    }        @Test    public void testMapper() {        mapDriver.withInput(new LongWritable(1), new Text("cat cat dog"));        mapDriver.withOutput(new Text("cat"), new IntWritable(1));        mapDriver.withOutput(new Text("cat"), new IntWritable(1));        mapDriver.withOutput(new Text("dog"), new IntWritable(1));        mapDriver.runTest();    }    @Test    public void testReducer() throws IOException {        List<IntWritable> values = new ArrayList<IntWritable>();        values.add(new IntWritable(1));        values.add(new IntWritable(1));        reduceDriver.withInput(new Text("cat"), values);        reduceDriver.withOutput(new Text("cat"), new IntWritable(2));        reduceDriver.runTest();    }    @Test    public void testMapReduce() throws IOException {        mapReduceDriver.withInput(new LongWritable(1), new Text("cat cat dog"));        mapReduceDriver.addOutput(new Text("cat"), new IntWritable(2));        mapReduceDriver.addOutput(new Text("dog"), new IntWritable(1));        mapReduceDriver.runTest();    }}

 

Листинг 7. WordCountMRUnitTest

Аналогичные процедуры применяются для тестирования изолированного редуктора, а также для тестирования комбинации картограф / редуктор. Эта способность, наряду с простотой использования, делает MRUnit очень привлекательным выбором.

 

 

Тестирование водителя

К этому моменту я показал способы модульного тестирования картографического устройства и редуктора как по отдельности, так и в составе устройства. Что еще предстоит проверить, так это класс водителя. Самый простой способ сделать это — использовать локального исполнителя.

WordCountDriverTest (Листинг 8.) демонстрирует этот подход. В «setup ()» тест создает новую конфигурацию и настраивает ее для использования локальной файловой системы и локального исполнителя заданий. Он также создает объекты Path для указания на входные данные (листинг 9) и место для размещения выходных данных. Наконец, он получает ссылку на локальную файловую систему и удаляет все предыдущие выходные данные.

Метод test создает экземпляр класса драйвера, передает объект Configuration и выполняет задание. Затем тест проверяет правильный код выхода и содержимое вывода.

public class WordCountDriverTest {    private Configuration conf;    private Path input;    private Path output;    private FileSystem fs;        @Before    public void setup() throws IOException {        conf = new Configuration();        conf.set("fs.default.name", "file:///");        conf.set("mapred.job.tracker", "local");                input = new Path("src/test/resources/input");        output = new Path("target/output");                fs = FileSystem.getLocal(conf);        fs.delete(output, true);    }    @Test    public void test() throws Exception {        WordCount wordCount = new WordCount();        wordCount.setConf(conf);                int exitCode = wordCount.run(new String[] {input.toString(), output.toString()});        assertEquals(0, exitCode);                validateOuput();    }    private void validateOuput() throws IOException {        InputStream in = null;        try {            in = fs.open(new Path("target/output/part-r-00000"));                        BufferedReader br = new BufferedReader(new InputStreamReader(in));            assertEquals("five\t1", br.readLine());            assertEquals("four\t1", br.readLine());            assertEquals("one\t3", br.readLine());            assertEquals("six\t1", br.readLine());            assertEquals("three\t1", br.readLine());            assertEquals("two\t2", br.readLine());                    } finally {            IOUtils.closeStream(in);        }    }}

 

Листинг 8. WordCountDriverTest

onetwothree four fiveone twosixone

 

Листинг 9. Входные данные для WordCountDriverTest

Следующие шаги

Теперь, когда программа прошла достаточное модульное тестирование, ее можно запустить в тестовом кластере, чтобы выявить потенциальные проблемы интеграции и масштабирования. Или классы Hadoop MiniDFSCluster и MiniMRCluster могут быть использованы для создания дополнительных тестов, которые выполняются на псевдокластере.

Вывод

Тестирование является важной частью разработки приложений MapReduce. Я надеюсь, что представленные здесь стратегии помогут вам определить наилучшие подходы для ваших нужд.

 

Ссылки