Руководство по Apache Crunch

Руководство по Apache Crunch

1. Вступление

В этом руководстве мы продемонстрируемApache Crunch на примере приложения для обработки данных. Мы запустим это приложение на платформеMapReduce.

Мы начнем с краткого обзора некоторых концепций Apache Crunch. Затем мы перейдем к образцу приложения. В этом приложении мы будем обрабатывать текст:

  • Прежде всего, мы прочитаем строки из текстового файла

  • Позже мы разделим их на слова и удалим некоторые общие слова.

  • Затем мы сгруппируем оставшиеся слова, чтобы получить список уникальных слов и их количество.

  • Наконец, мы запишем этот список в текстовый файл.

2. Что такое Хруст?

MapReduce - это распределенная среда параллельного программирования для обработки больших объемов данных на кластере серверов. Программные среды, такие как Hadoop и Spark, реализуют MapReduce.

Crunch provides a framework for writing, testing and running MapReduce pipelines in Java.  Здесь мы не пишем задания MapReduce напрямую. Скорее, мы определяем конвейер данных (т.е. операции для выполнения шагов ввода, обработки и вывода) с использованием API Crunch. Crunch Planner сопоставляет их с заданиями MapReduce и выполняет их при необходимости.

Therefore, every Crunch data pipeline is coordinated by an instance of the Pipeline interface. Этот интерфейс также определяет методы для чтения данных в конвейер через экземплярыSource и записи данных из конвейера в экземплярыTarget.

У нас есть 3 интерфейса для представления данных:

  1. PCollection - неизменяемая распределенная коллекция элементов

  2. PTable<K, V> - неизменяемая, распределенная, неупорядоченная мульти-карта ключей и значений

  3. PGroupedTable<K, V> - распределенная, отсортированная карта ключей типа K вIterable V, которая может повторяться ровно один раз

DoFn is the base class for all data processing functions. Он соответствует классамMapper,Reducer иCombiner в MapReduce. Большую часть времени разработки мы тратим на написание и тестирование логических вычислений, используя его.

Теперь, когда мы более знакомы с Crunch, давайте воспользуемся им для создания примера приложения.

3. Настройка проекта Crunch

Прежде всего, давайте создадим Crunch Project с Maven. Мы можем сделать это двумя способами:

  1. Добавьте необходимые зависимости в файлpom.xml существующего проекта.

  2. Используйте архетип для создания начального проекта

Давайте быстро рассмотрим оба подхода.

3.1. Maven Зависимости

Чтобы добавить Crunch к существующему проекту, давайте добавим необходимые зависимости в файлpom.xml.

Сначала добавим библиотекуcrunch-core:


    org.apache.crunch
    crunch-core
    0.15.0

Затем давайте добавим библиотекуhadoop-client для связи с Hadoop. Мы используем версию, соответствующую установке Hadoop:


    org.apache.hadoop
    hadoop-client
    2.2.0
    provided

Мы можем проверить Maven Central на наличие последних версий библиотекcrunch-core иhadoop-client.

3.2. Maven Archetype

Another approach is to quickly generate a starter project using the Maven archetype provided by Crunch:

mvn archetype:generate -Dfilter=org.apache.crunch:crunch-archetype

В ответ на указанную выше команду мы предоставляем версию Crunch и сведения об артефакте проекта.

4. Crunch Pipeline Setup

После настройки проекта нам нужно создать объектPipeline. Crunch has 3 Pipeline implementations:

  • MRPipeline – выполняется в Hadoop MapReduce

  • SparkPipeline – выполняется как последовательность конвейеров Spark

  • MemPipeline – выполняется в памяти на клиенте и полезен для модульного тестирования

Обычно мы разрабатываем и тестируем с использованием экземпляраMemPipeline. Позже мы используем экземплярMRPipeline илиSparkPipeline для фактического выполнения.

Если нам нужен конвейер в памяти, мы могли бы использовать статический методgetInstance для получения экземпляраMemPipeline:

Pipeline pipeline = MemPipeline.getInstance();

А пока давайте создадим экземплярMRPipeline для выполнения приложения с Hadoop:.

Pipeline pipeline = new MRPipeline(WordCount.class, getConf());

5. Читать входные данные

После создания объекта конвейера мы хотим прочитать входные данные. The Pipeline interface provides a convenience method to read input from a text file,readTextFile(pathName).

Давайте вызовем этот метод для чтения входного текстового файла:

PCollection lines = pipeline.readTextFile(inputPath);

Приведенный выше код читает текстовый файл как наборString.

В качестве следующего шага напишем тестовый пример для чтения ввода:

@Test
public void givenPipeLine_whenTextFileRead_thenExpectedNumberOfRecordsRead() {
    Pipeline pipeline = MemPipeline.getInstance();
    PCollection lines = pipeline.readTextFile(INPUT_FILE_PATH);

    assertEquals(21, lines.asCollection()
      .getValue()
      .size());
}

В этом тесте мы проверяем, получаем ли мы ожидаемое количество строк при чтении текстового файла.

6. Шаги обработки данных

После прочтения входных данных нам нужно их обработать. Crunch API contains a number of subclasses of DoFn to handle common data processing scenarios:

  • FilterFn - фильтрует элементы коллекции на основе логического условия

  • MapFn - отображает каждую входную запись ровно на одну выходную запись

  • CombineFn - объединяет несколько значений в одно значение

  • JoinFn - выполняет соединения, такие как внутреннее соединение, левое внешнее соединение, правое внешнее соединение и полное внешнее соединение

Давайте реализуем следующую логику обработки данных с помощью этих классов:

  1. Разделить каждую строку во входном файле на слова

  2. Удалить стоп-слова

  3. Подсчитайте уникальные слова

6.1. Разбить строку текста на слова

Прежде всего, давайте создадим классTokenizer, чтобы разбить строку на слова.

Мы расширим классDoFn. В этом классе есть абстрактный методprocess. Этот метод обрабатывает входные записи изPCollection и отправляет выходные вEmitter. 

Нам нужно реализовать логику расщепления в этом методе:

public class Tokenizer extends DoFn {
    private static final Splitter SPLITTER = Splitter
      .onPattern("\\s+")
      .omitEmptyStrings();

    @Override
    public void process(String line, Emitter emitter) {
        for (String word : SPLITTER.split(line)) {
            emitter.emit(word);
        }
    }
}

В приведенной выше реализации мы использовали классSplitter из библиотекиGuava для извлечения слов из строки.

Затем давайте напишем модульный тест для классаTokenizer:

@RunWith(MockitoJUnitRunner.class)
public class TokenizerUnitTest {

    @Mock
    private Emitter emitter;

    @Test
    public void givenTokenizer_whenLineProcessed_thenOnlyExpectedWordsEmitted() {
        Tokenizer splitter = new Tokenizer();
        splitter.process("  hello  world ", emitter);

        verify(emitter).emit("hello");
        verify(emitter).emit("world");
        verifyNoMoreInteractions(emitter);
    }
}

Приведенный выше тест проверяет, что верные слова возвращены.

Наконец, давайте с помощью этого класса разделим строки, прочитанные из входного текстового файла.

МетодparallelDo интерфейсаPCollection применяет данныйDoFn ко всем элементам и возвращает новыйPCollection.

Давайте вызовем этот метод для коллекции строк и передадим экземплярTokenizer:

PCollection words = lines.parallelDo(new Tokenizer(), Writables.strings());

В результате мы получаем список слов во входном текстовом файле. На следующем шаге мы удалим стоп-слова.

6.2. Удалить стоп-слова

Как и в предыдущем шаге, давайте создадим классStopWordFilter для фильтрации стоп-слов.

Однако мы расширимFilterFn вместоDoFn. FilterFn имеет абстрактный метод под названиемaccept. Нам нужно реализовать логику фильтрации в этом методе:

public class StopWordFilter extends FilterFn {

    // English stop words, borrowed from Lucene.
    private static final Set STOP_WORDS = ImmutableSet
      .copyOf(new String[] { "a", "and", "are", "as", "at", "be", "but", "by",
        "for", "if", "in", "into", "is", "it", "no", "not", "of", "on",
        "or", "s", "such", "t", "that", "the", "their", "then", "there",
        "these", "they", "this", "to", "was", "will", "with" });

    @Override
    public boolean accept(String word) {
        return !STOP_WORDS.contains(word);
    }
}

Затем давайте напишем модульный тест для классаStopWordFilter:

public class StopWordFilterUnitTest {

    @Test
    public void givenFilter_whenStopWordPassed_thenFalseReturned() {
        FilterFn filter = new StopWordFilter();

        assertFalse(filter.accept("the"));
        assertFalse(filter.accept("a"));
    }

    @Test
    public void givenFilter_whenNonStopWordPassed_thenTrueReturned() {
        FilterFn filter = new StopWordFilter();

        assertTrue(filter.accept("Hello"));
        assertTrue(filter.accept("World"));
    }

    @Test
    public void givenWordCollection_whenFiltered_thenStopWordsRemoved() {
        PCollection words = MemPipeline
          .collectionOf("This", "is", "a", "test", "sentence");
        PCollection noStopWords = words.filter(new StopWordFilter());

        assertEquals(ImmutableList.of("This", "test", "sentence"),
         Lists.newArrayList(noStopWords.materialize()));
    }
}

Этот тест проверяет, что логика фильтрации выполняется правильно.

Наконец, давайте использоватьStopWordFilter для фильтрации списка слов, созданного на предыдущем шаге. The filter method of PCollection interface applies the given FilterFn to all the elements and returns a new PCollection.

Давайте вызовем этот метод для коллекции слов и передадим экземплярStopWordFilter:

PCollection noStopWords = words.filter(new StopWordFilter());

В результате мы получаем отфильтрованный набор слов.

6.3. Подсчитайте уникальные слова

После получения отфильтрованной коллекции слов мы хотим посчитать, как часто встречается каждое слово. PCollection interface has a number of methods to perform common aggregations:с

  • min - возвращает минимальный элемент коллекции

  • max - возвращает максимальный элемент коллекции

  • length - возвращает количество элементов в коллекции

  • count - возвращаетPTable, который содержит количество каждого уникального элемента коллекции

Давайте воспользуемся методомcount, чтобы получить уникальные слова и их количество:

// The count method applies a series of Crunch primitives and returns
// a map of the unique words in the input PCollection to their counts.
PTable counts = noStopWords.count();

7. Укажите вывод

В результате предыдущих шагов у нас есть таблица слов и их количество. Мы хотим записать этот результат в текстовый файл. The Pipeline interface provides convenience methods to write output:с

void write(PCollection collection, Target target);

void write(PCollection collection, Target target,
  Target.WriteMode writeMode);

 void writeTextFile(PCollection collection, String pathName);

Поэтому вызовем методwriteTextFile:

pipeline.writeTextFile(counts, outputPath);

8. Управление выполнением конвейера

Все шаги до сих пор только что определили конвейер данных. Никакие входные данные не были прочитаны или обработаны. Это потому, чтоCrunch uses lazy execution model.

Он не запускает задания MapReduce до тех пор, пока в интерфейсе конвейера не будет вызван метод, контролирующий планирование и выполнение заданий:

  • run - подготавливает план выполнения для создания требуемых выходов, а затем выполняет его синхронно

  • done - запускает все оставшиеся задания, необходимые для генерации выходных данных, а затем очищает все созданные промежуточные файлы данных

  • runAsync - аналогичен методу run, но выполняется без блокировки

Поэтому давайте вызовем методdone для выполнения конвейера как задания MapReduce:

PipelineResult result = pipeline.done();

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

9. Соединяя трубопровод

До сих пор мы разработали и протестировали логику для чтения входных данных, их обработки и записи в выходной файл.

Затем давайте объединим их, чтобы построить весь конвейер данных:

public int run(String[] args) throws Exception {
    String inputPath = args[0];
    String outputPath = args[1];

    // Create an object to coordinate pipeline creation and execution.
    Pipeline pipeline = new MRPipeline(WordCount.class, getConf());

    // Reference a given text file as a collection of Strings.
    PCollection lines = pipeline.readTextFile(inputPath);

    // Define a function that splits each line in a PCollection of Strings into
    // a PCollection made up of the individual words in the file.
    // The second argument sets the serialization format.
    PCollection words = lines.parallelDo(new Tokenizer(), Writables.strings());

    // Take the collection of words and remove known stop words.
    PCollection noStopWords = words.filter(new StopWordFilter());

    // The count method applies a series of Crunch primitives and returns
    // a map of the unique words in the input PCollection to their counts.
    PTable counts = noStopWords.count();

    // Instruct the pipeline to write the resulting counts to a text file.
    pipeline.writeTextFile(counts, outputPath);

    // Execute the pipeline as a MapReduce.
    PipelineResult result = pipeline.done();

    return result.succeeded() ? 0 : 1;
}

10. Конфигурация запуска Hadoop

Таким образом, конвейер данных готов.

Однако нам нужен код для его запуска. Поэтому напишем методmain для запуска приложения:

public class WordCount extends Configured implements Tool {

    public static void main(String[] args) throws Exception {
        ToolRunner.run(new Configuration(), new WordCount(), args);
    }

ToolRunner.run анализирует конфигурацию Hadoop из командной строки и выполняет задание MapReduce.

11. Запустить приложение

Полное приложение теперь готово. Давайте запустим следующую команду, чтобы построить его:

mvn package

В результате выполнения вышеприведенной команды мы получаем упакованное приложение и специальный jar-файл задания в целевом каталоге.

Давайте воспользуемся этим файлом задания для выполнения приложения в Hadoop:

hadoop jar target/crunch-1.0-SNAPSHOT-job.jar  

Приложение читает входной файл и записывает результат в выходной файл. Выходной файл содержит уникальные слова и их количество, подобное следующему:

[Add,1]
[Added,1]
[Admiration,1]
[Admitting,1]
[Allowance,1]

Помимо Hadoop, мы можем запускать приложение в IDE, как отдельное приложение или как модульные тесты.

12. Заключение

В этом уроке мы создали приложение для обработки данных, работающее на MapReduce. Apache Crunch позволяет легко писать, тестировать и выполнять конвейеры MapReduce на Java.

Как обычно, полный исходный код можно найтиover on Github.