Un guide pour Apache Crunch

Un guide pour Apache Crunch

1. introduction

Dans ce didacticiel, nous allons démontrerApache Crunch avec un exemple d'application de traitement de données. Nous allons exécuter cette application en utilisant le frameworkMapReduce.

Nous commencerons par couvrir brièvement quelques concepts d'Apache Crunch. Ensuite, nous allons passer à un exemple d'application. Dans cette application, nous traiterons du texte:

  • Tout d'abord, nous allons lire les lignes d'un fichier texte

  • Plus tard, nous les diviserons en mots et supprimerons certains mots courants

  • Ensuite, nous regrouperons les mots restants pour obtenir une liste de mots uniques et leur nombre

  • Enfin, nous allons écrire cette liste dans un fichier texte

2. Qu'est-ce que Crunch?

MapReduce est un cadre de programmation parallèle distribué pour le traitement de grandes quantités de données sur un cluster de serveurs. Les infrastructures logicielles telles que Hadoop et Spark implémentent MapReduce.

Crunch provides a framework for writing, testing and running MapReduce pipelines in Java.  Ici, nous n'écrivons pas directement les tâches MapReduce. Nous définissons plutôt le pipeline de données (c.-à-d. les opérations pour effectuer les étapes d’entrée, de traitement et de sortie) à l’aide des API Crunch. Crunch Planner les mappe aux travaux MapReduce et les exécute si nécessaire.

Therefore, every Crunch data pipeline is coordinated by an instance of the Pipeline interface. Cette interface définit également des méthodes de lecture de données dans un pipeline via les instancesSource et d'écriture de données depuis un pipeline vers les instancesTarget.

Nous avons 3 interfaces pour représenter les données:

  1. PCollection - une collection d'éléments immuables et distribués

  2. PTable<K, V> - une multi-map immuable, distribuée et non ordonnée de clés et de valeurs

  3. PGroupedTable<K, V> - une carte distribuée et triée de clés de type K vers unIterable V qui peut être itéré exactement une fois

DoFn is the base class for all data processing functions. Il correspond aux classesMapper,Reducer etCombiner dans MapReduce. Nous passons la majeure partie du temps de développement à écrire et à tester des calculs logiques en l'utilisant.

Maintenant que nous sommes plus familiers avec Crunch, utilisons-le pour créer l'exemple d'application.

3. Mise en place d'un projet Crunch

Tout d’abord, mettons en place un projet Crunch avec Maven. Nous pouvons le faire de deux manières:

  1. Ajouter les dépendances requises dans le fichierpom.xml d'un projet existant

  2. Utiliser un archétype pour générer un projet de démarrage

Jetons un coup d'œil aux deux approches.

3.1. Dépendances Maven

Pour ajouter Crunch à un projet existant, ajoutons les dépendances requises dans le fichierpom.xml.

Tout d'abord, ajoutons la bibliothèquecrunch-core:


    org.apache.crunch
    crunch-core
    0.15.0

Ensuite, ajoutons la bibliothèquehadoop-client pour communiquer avec Hadoop. Nous utilisons la version correspondant à l'installation de Hadoop:


    org.apache.hadoop
    hadoop-client
    2.2.0
    provided

Nous pouvons consulter Maven Central pour les dernières versions des bibliothèquescrunch-core ethadoop-client.

3.2. Archétype 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

Lorsque la commande ci-dessus vous y invite, nous fournissons la version de Crunch et les détails de l'artefact du projet.

4. Configuration du pipeline Crunch

Après avoir configuré le projet, nous devons créer un objetPipeline. Crunch has 3 Pipeline implementations:

  • MRPipeline  - s'exécute dans Hadoop MapReduce

  • SparkPipeline – s'exécute comme une série de pipelines Spark

  • MemPipeline – s'exécute en mémoire sur le client et est utile pour les tests unitaires

Habituellement, nous développons et testons en utilisant une instance deMemPipeline. Plus tard, nous utilisons une instance deMRPipeline ouSparkPipeline pour l'exécution réelle.

Si nous avions besoin d'un pipeline en mémoire, nous pourrions utiliser la méthode statiquegetInstance pour obtenir l'instanceMemPipeline:

Pipeline pipeline = MemPipeline.getInstance();

Mais pour l'instant, créons une instance deMRPipeline pour exécuter l'application avec Hadoop:

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

5. Lire les données d'entrée

Après avoir créé l'objet de pipeline, nous voulons lire les données d'entrée. The Pipeline interface provides a convenience method to read input from a text file,readTextFile(pathName).

Appelons cette méthode pour lire le fichier texte d'entrée:

PCollection lines = pipeline.readTextFile(inputPath);

Le code ci-dessus lit le fichier texte comme une collection deString.

À l’étape suivante, écrivons un scénario de test pour lire l’entrée:

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

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

Dans ce test, nous vérifions que nous obtenons le nombre attendu de lignes lors de la lecture d'un fichier texte.

6. Étapes de traitement des données

Après avoir lu les données d'entrée, nous devons les traiter. Crunch API contains a number of subclasses of DoFn to handle common data processing scenarios:

  • FilterFn - filtre les membres d'une collection en fonction d'une condition booléenne

  • MapFn - mappe chaque enregistrement d'entrée à exactement un enregistrement de sortie

  • CombineFn - combine un certain nombre de valeurs en une seule valeur

  • JoinFn - effectue des jointures telles que la jointure interne, la jointure externe gauche, la jointure externe droite et la jointure externe complète

Implémentons la logique de traitement des données suivante en utilisant ces classes:

  1. Fractionner chaque ligne du fichier d'entrée en mots

  2. Supprimer les mots vides

  3. Compter les mots uniques

6.1. Fractionner une ligne de texte en mots

Tout d’abord, créons la classeTokenizer pour diviser une ligne en mots.

Nous allons étendre la classeDoFn. Cette classe a une méthode abstraite appeléeprocess. Cette méthode traite les enregistrements d'entrée d'unPCollection et envoie la sortie vers unEmitter. 

Nous devons implémenter la logique de division dans cette méthode:

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

Dans l'implémentation ci-dessus, nous avons utilisé la classeSplitter de la bibliothèqueGuava pour extraire des mots d'une ligne.

Ensuite, écrivons un test unitaire pour la 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);
    }
}

Le test ci-dessus vérifie que les mots corrects sont renvoyés.

Enfin, divisons les lignes lues à partir du fichier texte d’entrée à l’aide de cette classe.

La méthodeparallelDo de l'interfacePCollection applique lesDoFn donnés à tous les éléments et renvoie un nouveauPCollection.

Appelons cette méthode sur la collection de lignes et passons une instance deTokenizer:

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

En conséquence, nous obtenons la liste des mots dans le fichier texte d’entrée. Nous supprimerons les mots vides à l'étape suivante.

6.2. Supprimer les mots d'arrêt

De la même manière que pour l'étape précédente, créons une classeStopWordFilter pour filtrer les mots vides.

Cependant, nous allons étendreFilterFn au lieu deDoFn. FilterFn a une méthode abstraite appeléeaccept. Nous devons implémenter la logique de filtrage dans cette méthode:

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

Ensuite, écrivons le test unitaire pour la 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()));
    }
}

Ce test vérifie que la logique de filtrage est effectuée correctement.

Enfin, utilisonsStopWordFilter pour filtrer la liste de mots générée à l'étape précédente. The filter method of PCollection interface applies the given FilterFn to all the elements and returns a new PCollection.

Appelons cette méthode sur la collection de mots et passons une instance deStopWordFilter:

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

En conséquence, nous obtenons la collection filtrée de mots.

6.3. Comptez des mots uniques

Après avoir récupéré la collection filtrée de mots, nous souhaitons compter la fréquence de chaque mot. PCollection interface has a number of methods to perform common aggregations:

  • min - renvoie l'élément minimum de la collection

  • max - renvoie l'élément maximum de la collection

  • length - renvoie le nombre d'éléments dans la collection

  • count - renvoie unPTable qui contient le décompte de chaque élément unique de la collection

Utilisons la méthodecount pour obtenir les mots uniques avec leurs nombres:

// 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. Spécifier la sortie

À la suite des étapes précédentes, nous avons un tableau de mots et leurs comptes. Nous voulons écrire ce résultat dans un fichier texte. 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);

Par conséquent, appelons la méthodewriteTextFile:

pipeline.writeTextFile(counts, outputPath);

8. Gérer l'exécution du pipeline

Jusqu'à présent, toutes les étapes viennent de définir le pipeline de données. Aucune entrée n'a été lue ou traitée. C'est parce queCrunch uses lazy execution model.

Il n'exécute pas les tâches MapReduce tant qu'une méthode qui contrôle la planification et l'exécution des tâches n'est pas appelée sur l'interface Pipeline:

  • run - prépare un plan d'exécution pour créer les sorties requises puis l'exécute de manière synchrone

  • done - exécute tous les travaux restants requis pour générer des sorties, puis nettoie tous les fichiers de données intermédiaires créés

  • runAsync - similaire à la méthode run, mais s'exécute de manière non bloquante

Par conséquent, appelons la méthodedone pour exécuter le pipeline en tant que tâches MapReduce:

PipelineResult result = pipeline.done();

L'instruction ci-dessus exécute les travaux MapReduce pour lire les entrées, les traiter et écrire le résultat dans le répertoire de sortie.

9. Assembler le pipeline

Jusqu'ici, nous avons développé et testé la logique pour lire les données d'entrée, les traiter et écrire dans le fichier de sortie.

Ensuite, rassemblons-les pour créer l'ensemble du pipeline de données:

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. Configuration de lancement Hadoop

Le pipeline de données est donc prêt.

Cependant, nous avons besoin du code pour le lancer. Par conséquent, écrivons la méthodemain pour lancer l'application:

public class WordCount extends Configured implements Tool {

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

ToolRunner.run analyse la configuration Hadoop à partir de la ligne de commande et exécute le travail MapReduce.

11. Exécuter l'application

L'application complète est maintenant prête. Exécutons la commande suivante pour le créer:

mvn package

À la suite de la commande ci-dessus, nous obtenons l'application packagée et un fichier de travail spécial dans le répertoire cible.

Utilisons ce job jar pour exécuter l'application sur Hadoop:

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

L'application lit le fichier d'entrée et écrit le résultat dans le fichier de sortie. Le fichier de sortie contient des mots uniques avec leurs nombres, semblables à ceux-ci:

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

Outre Hadoop, nous pouvons exécuter l’application dans IDE, en tant qu’application autonome ou en tant que test unitaire.

12. Conclusion

Dans ce tutoriel, nous avons créé une application de traitement de données s'exécutant sur MapReduce. Apache Crunch facilite l'écriture, le test et l'exécution de pipelines MapReduce en Java.

Comme d'habitude, le code source complet peut être trouvéover on Github.