Um Guia para o Apache Crunch

Um Guia para o Apache Crunch

1. Introdução

Neste tutorial, demonstraremosApache Crunch com um aplicativo de processamento de dados de exemplo. Vamos executar este aplicativo usando a estruturaMapReduce.

Começaremos cobrindo brevemente alguns conceitos do Apache Crunch. Em seguida, vamos pular para um aplicativo de amostra. Neste aplicativo, faremos processamento de texto:

  • Em primeiro lugar, vamos ler as linhas de um arquivo de texto

  • Mais tarde, vamos dividi-los em palavras e remover algumas palavras comuns

  • Em seguida, agruparemos as palavras restantes para obter uma lista de palavras exclusivas e suas contagens

  • Finalmente, vamos escrever esta lista em um arquivo de texto

2. O que é Crunch?

O MapReduce é uma estrutura de programação paralela distribuída para processar grandes quantidades de dados em um cluster de servidores. Estruturas de software como Hadoop e Spark implementam o MapReduce.

Crunch provides a framework for writing, testing and running MapReduce pipelines in Java. Aqui, não escrevemos os trabalhos MapReduce diretamente. Em vez disso, definimos o pipeline de dados (ou seja, as operações para executar etapas de entrada, processamento e saída) usando as APIs do Crunch. O Crunch Planner os mapeia para os trabalhos do MapReduce e os executa quando necessário.

Therefore, every Crunch data pipeline is coordinated by an instance of the Pipeline interface. Essa interface também define métodos para ler dados em um pipeline por meio de instânciasSource e gravar dados de um pipeline em instânciasTarget.

Temos 3 interfaces para representar dados:

  1. PCollection - uma coleção distribuída e imutável de elementos

  2. PTable<K, V> - um multimapa imutável, distribuído e não ordenado de chaves e valores

  3. PGroupedTable<K, V> - um mapa distribuído e classificado de chaves do tipo K para umIterable V que pode ser iterado exatamente uma vez

DoFn is the base class for all data processing functions. Corresponde às classesMapper,ReducereCombiner no MapReduce. Passamos a maior parte do tempo de desenvolvimento escrevendo e testando cálculos lógicos usando.

Agora que estamos mais familiarizados com o Crunch, vamos usá-lo para construir o aplicativo de exemplo.

3. Configurando um Projeto Crunch

Em primeiro lugar, vamos configurar um Projeto Crunch com Maven. Podemos fazer isso de duas maneiras:

  1. Adicione as dependências necessárias no arquivopom.xml de um projeto existente

  2. Use um arquétipo para gerar um projeto inicial

Vamos dar uma olhada rápida em ambas as abordagens.

3.1. Dependências do Maven

Para adicionar o Crunch a um projeto existente, vamos adicionar as dependências necessárias no arquivopom.xml.

Primeiro, vamos adicionar a bibliotecacrunch-core:


    org.apache.crunch
    crunch-core
    0.15.0

A seguir, vamos adicionar a bibliotecahadoop-client para se comunicar com o Hadoop. Usamos a versão correspondente à instalação do Hadoop:


    org.apache.hadoop
    hadoop-client
    2.2.0
    provided

Podemos verificar o Maven Central para as versões mais recentes das bibliotecascrunch-coreehadoop-client.

3.2. Arquétipo Maven

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

Quando solicitado pelo comando acima, fornecemos a versão Crunch e os detalhes do artefato do projeto.

4. Configuração do pipeline de trituração

Depois de configurar o projeto, precisamos criar um objetoPipeline. Crunch has 3 Pipeline implementations:

  • MRPipeline – executa dentro do Hadoop MapReduce

  • SparkPipeline – executa como uma série de pipelines Spark

  • MemPipeline – executa na memória no cliente e é útil para testes de unidade

Normalmente, desenvolvemos e testamos usando uma instância deMemPipeline. Mais tarde, usamos uma instância deMRPipeline ouSparkPipeline para a execução real.

Se precisássemos de um pipeline na memória, poderíamos usar o método estáticogetInstance para obter a instânciaMemPipeline:

Pipeline pipeline = MemPipeline.getInstance();

Mas, por enquanto, vamos criar uma instância deMRPipeline para executar o aplicativo com Hadoop:

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

5. Ler dados de entrada

Depois de criar o objeto de pipeline, queremos ler os dados de entrada. The Pipeline interface provides a convenience method to read input from a text file,readTextFile(pathName).

Vamos chamar este método para ler o arquivo de texto de entrada:

PCollection lines = pipeline.readTextFile(inputPath);

O código acima lê o arquivo de texto como uma coleção deString.

Como a próxima etapa, vamos escrever um caso de teste para ler a entrada:

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

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

Neste teste, verificamos que obtemos o número esperado de linhas ao ler um arquivo de texto.

6. Etapas do processamento de dados

Depois de ler os dados de entrada, precisamos processá-los. Crunch API contains a number of subclasses of DoFn to handle common data processing scenarios:

  • FilterFn - filtra membros de uma coleção com base em uma condição booleana

  • MapFn - mapeia cada registro de entrada para exatamente um registro de saída

  • CombineFn - combina uma série de valores em um único valor

  • JoinFn - realiza junções como junção interna, junção externa esquerda, junção externa direita e junção externa completa

Vamos implementar a seguinte lógica de processamento de dados usando essas classes:

  1. Divida cada linha do arquivo de entrada em palavras

  2. Remova as palavras de parada

  3. Conte as palavras únicas

6.1. Dividir uma linha de texto em palavras

Em primeiro lugar, vamos criar a classeTokenizer para dividir uma linha em palavras.

Vamos estender a classeDoFn. Esta classe possui um método abstrato chamadoprocess. Este método processa os registros de entrada de umPCollectione envia a saída para umEmitter. 

Precisamos implementar a lógica de divisão neste método:

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);
        }
    }
}

Na implementação acima, usamos a classeSplitter da bibliotecaGuava para extrair palavras de uma linha.

A seguir, vamos escrever um teste de unidade para a classeTokenizer:

@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);
    }
}

O teste acima verifica se as palavras corretas são retornadas.

Finalmente, vamos dividir as linhas lidas do arquivo de texto de entrada usando esta classe.

O métodoparallelDo da interfacePCollection aplica oDoFn fornecido a todos os elementos e retorna um novoPCollection.

Vamos chamar este método na coleção de linhas e passar uma instância deTokenizer:

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

Como resultado, obtemos a lista de palavras no arquivo de texto de entrada. Removeremos as palavras de interrupção na próxima etapa.

6.2. Remover palavras de parada

Da mesma forma que na etapa anterior, vamos criar uma classeStopWordFilter para filtrar palavras irrelevantes.

No entanto, vamos estenderFilterFn em vez deDoFn. FilterFn tem um método abstrato chamadoaccept. Precisamos implementar a lógica de filtragem neste método:

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);
    }
}

A seguir, vamos escrever o teste de unidade para a classeStopWordFilter:

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()));
    }
}

Este teste verifica se a lógica de filtragem é realizada corretamente.

Finalmente, vamos usarStopWordFilter para filtrar a lista de palavras gerada na etapa anterior. The filter method of PCollection interface applies the given FilterFn to all the elements and returns a new PCollection.

Vamos chamar este método na coleção de palavras e passar uma instância deStopWordFilter:

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

Como resultado, obtemos a coleção filtrada de palavras.

6.3. Contar palavras únicas

Depois de obter a coleção filtrada de palavras, queremos contar com que frequência cada palavra ocorre. PCollection interface has a number of methods to perform common aggregations:

  • min - retorna o elemento mínimo da coleção

  • max - retorna o elemento máximo da coleção

  • length - retorna o número de elementos na coleção

  • count - retorna umPTable que contém a contagem de cada elemento único da coleção

Vamos usar o métodocount para obter as palavras exclusivas junto com suas contagens:

// 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. Especificar saída

Como resultado das etapas anteriores, temos uma tabela de palavras e suas contagens. Queremos gravar esse resultado em um arquivo de texto. 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);

Portanto, vamos chamar o métodowriteTextFile:

pipeline.writeTextFile(counts, outputPath);

8. Gerenciar execução de pipeline

Todas as etapas até agora definiram o pipeline de dados. Nenhuma entrada foi lida ou processada. Isso ocorre porqueCrunch uses lazy execution model.

Ele não executa as tarefas MapReduce até que um método que controla o planejamento e a execução da tarefa seja invocado na interface do Pipeline:

  • run - prepara um plano de execução para criar as saídas necessárias e, em seguida, executa de forma síncrona

  • done - executa todos os trabalhos restantes necessários para gerar resultados e, em seguida, limpa todos os arquivos de dados intermediários criados

  • runAsync - semelhante ao método de execução, mas executa de forma não bloqueadora

Portanto, vamos chamar o métododone para executar o pipeline como tarefas MapReduce:

PipelineResult result = pipeline.done();

A instrução acima executa os trabalhos do MapReduce para ler a entrada, processá-los e gravar o resultado no diretório de saída.

9. Juntando o Pipeline

Até agora, desenvolvemos e testamos a lógica da unidade para ler os dados de entrada, processá-los e gravar no arquivo de saída.

A seguir, vamos colocá-los juntos para construir todo o pipeline de dados:

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. Configuração de inicialização do Hadoop

O pipeline de dados está pronto.

No entanto, precisamos do código para iniciá-lo. Portanto, vamos escrever o métodomain para iniciar o aplicativo:

public class WordCount extends Configured implements Tool {

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

ToolRunner.run analisa a configuração do Hadoop a partir da linha de comando e executa o trabalho MapReduce.

11. Executar aplicativo

A aplicação completa está pronta. Vamos executar o seguinte comando para criá-lo:

mvn package

Como resultado do comando acima, obtemos o aplicativo empacotado e um jar de trabalho especial no diretório de destino.

Vamos usar este jar de trabalho para executar o aplicativo no Hadoop:

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

O aplicativo lê o arquivo de entrada e grava o resultado no arquivo de saída. O arquivo de saída contém palavras exclusivas, juntamente com suas contagens semelhantes à seguinte:

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

Além do Hadoop, podemos executar o aplicativo no IDE, como um aplicativo independente ou como testes de unidade.

12. Conclusão

Neste tutorial, criamos um aplicativo de processamento de dados em execução no MapReduce. O Apache Crunch facilita a gravação, o teste e a execução de pipelines MapReduce em Java.

Como de costume, o código-fonte completo pode ser encontradoover on Github.