Introduction à Apache Flink avec Java

1. Vue d'ensemble

Apache Flink est une infrastructure de traitement de données volumineuses qui permet aux programmeurs de traiter la grande quantité de données de manière très efficace et évolutive.

Dans cet article, nous allons présenter certains descore API concepts and standard data transformations available in the Apache Flink Java API. Le style fluide de cette API facilite le travail avec la structure centrale de Flink - la collection distribuée.

Tout d'abord, nous allons jeter un œil aux transformations de l'APIDataSet de Flink et les utiliser pour implémenter un programme de comptage de mots. Ensuite, nous examinerons brièvement l'APIDataStream de Flink, qui vous permet de traiter des flux d'événements en temps réel.

2. Dépendance Maven

Pour commencer, nous devons ajouter des dépendances Maven aux bibliothèquesflink-java etflink-test-utils:


    org.apache.flink
    flink-java
    1.2.0


    org.apache.flink
    flink-test-utils_2.10
    1.2.0
    test

3. Concepts principaux de l'API

Lorsque vous travaillez avec Flink, nous devons connaître quelques notions liées à son API:

  • Every Flink program performs transformations on distributed collections of data. Diverses fonctions de transformation des données sont fournies, notamment le filtrage, le mappage, la jonction, le regroupement et l'agrégation

  • A sink operation in Flink triggers the execution of a stream to produce the desired result of the program, comme l'enregistrement du résultat dans le système de fichiers ou son impression sur la sortie standard

  • Les transformations Flink sont paresseuses, ce qui signifie qu'elles ne sont pas exécutées tant qu'une opérationsink n'est pas appelée

  • The Apache Flink API supports two modes of operations — batch and real-time. Si vous avez affaire à une source de données limitée qui peut être traitée en mode batch, vous utiliserez l'APIDataSet. Si vous souhaitez traiter des flux de données illimités en temps réel, vous devez utiliser l'APIDataStream

4. Transformations de l'API DataSet

Le point d'entrée du programme Flink est une instance de la classeExecutionEnvironment - cela définit le contexte dans lequel un programme est exécuté.

Créons unExecutionEnvironment pour démarrer notre traitement:

ExecutionEnvironment env
  = ExecutionEnvironment.getExecutionEnvironment();

Note that when you launch the application on the local machine, it will perform processing on the local JVM. Si vous souhaitez démarrer le traitement sur un cluster de machines, vous devez installerApache Flink sur ces machines et configurer lesExecutionEnvironment en conséquence.

4.1. Créer un DataSet

Pour commencer à effectuer des transformations de données, nous devons fournir notre programme avec les données.

Créons une instance de la classeDataSet en utilisant nosExecutionEnvironement:

DataSet amounts = env.fromElements(1, 29, 40, 50);

Vous pouvez créer unDataSet à partir de plusieurs sources, telles qu'Apache Kafka, un CSV, un fichier ou pratiquement toute autre source de données.

4.2. Filtrer et réduire

Une fois que vous avez créé une instance de la classeDataSet, vous pouvez lui appliquer des transformations.

Supposons que vous souhaitiez filtrer les nombres supérieurs à un certain seuil, puis les additionner tous. Vous pouvez utiliser les transformationsfilter() etreduce() pour y parvenir:

int threshold = 30;
List collect = amounts
  .filter(a -> a > threshold)
  .reduce((integer, t1) -> integer + t1)
  .collect();

assertThat(collect.get(0)).isEqualTo(90);

Notez que la méthodecollect() est une opérationsink qui déclenche les transformations de données réelles.

4.3. Map

Supposons que vous ayez unDataSet d’objetsPerson:

private static class Person {
    private int age;
    private String name;

    // standard constructors/getters/setters
}

Ensuite, créons unDataSet de ces objets:

DataSet personDataSource = env.fromCollection(
  Arrays.asList(
    new Person(23, "Tom"),
    new Person(75, "Michael")));

Supposons que vous souhaitiez extraire uniquement le champage de chaque objet de la collection. Vous pouvez utiliser la transformationmap() pour obtenir uniquement un champ spécifique de la classePerson:

List ages = personDataSource
  .map(p -> p.age)
  .collect();

assertThat(ages).hasSize(2);
assertThat(ages).contains(23, 75);

4.4. Join

Lorsque vous avez deux ensembles de données, vous souhaiterez peut-être les joindre sur un champid. Pour cela, vous pouvez utiliser la transformationjoin().

Créons des collections de transactions et d’adresses d’un utilisateur:

Tuple3 address
  = new Tuple3<>(1, "5th Avenue", "London");
DataSet> addresses
  = env.fromElements(address);

Tuple2 firstTransaction
  = new Tuple2<>(1, "Transaction_1");
DataSet> transactions
  = env.fromElements(firstTransaction, new Tuple2<>(12, "Transaction_2"));

Le premier champ des deux tuples est de typeInteger, et c'est un champid sur lequel nous voulons joindre les deux ensembles de données.

Pour exécuter la logique de jonction réelle, nous devons implémenter une interfaceKeySelector pour l'adresse et la transaction:

private static class IdKeySelectorTransaction
  implements KeySelector, Integer> {
    @Override
    public Integer getKey(Tuple2 value) {
        return value.f0;
    }
}

private static class IdKeySelectorAddress
  implements KeySelector, Integer> {
    @Override
    public Integer getKey(Tuple3 value) {
        return value.f0;
    }
}

Chaque sélecteur renvoie uniquement le champ sur lequel la jointure doit être effectuée.

Malheureusement, il n’est pas possible d’utiliser des expressions lambda ici car Flink a besoin d’informations de type générique.

Ensuite, mettons en œuvre la logique de fusion à l'aide de ces sélecteurs:

List, Tuple3>>
  joined = transactions.join(addresses)
  .where(new IdKeySelectorTransaction())
  .equalTo(new IdKeySelectorAddress())
  .collect();

assertThat(joined).hasSize(1);
assertThat(joined).contains(new Tuple2<>(firstTransaction, address));

4.5. Sort

Supposons que vous ayez la collection suivante deTuple2:

Tuple2 secondPerson = new Tuple2<>(4, "Tom");
Tuple2 thirdPerson = new Tuple2<>(5, "Scott");
Tuple2 fourthPerson = new Tuple2<>(200, "Michael");
Tuple2 firstPerson = new Tuple2<>(1, "Jack");
DataSet> transactions = env.fromElements(
  fourthPerson, secondPerson, thirdPerson, firstPerson);

Si vous souhaitez trier cette collection par le premier champ du tuple, vous pouvez utiliser la transformationsortPartitions():

List> sorted = transactions
  .sortPartition(new IdKeySelectorTransaction(), Order.ASCENDING)
  .collect();

assertThat(sorted)
  .containsExactly(firstPerson, secondPerson, thirdPerson, fourthPerson);

5. Nombre de mots

Le problème du nombre de mots est un problème couramment utilisé pour présenter les capacités des frameworks de traitement de données volumineuses. La solution de base consiste à compter les occurrences de mots dans une entrée de texte. Utilisons Flink pour mettre en œuvre une solution à ce problème.

Dans la première étape de notre solution, nous créons une classeLineSplitter qui divise notre entrée en jetons (mots), collectant pour chaque jeton unTuple2 de paires clé-valeur. Dans chacun de ces tuples, la clé est un mot trouvé dans le texte et la valeur est le nombre entier un (1).

Cette classe implémente l'interfaceFlatMapFunction qui prendString comme entrée et produit unTuple2<String, Integer>:

public class LineSplitter implements FlatMapFunction> {

    @Override
    public void flatMap(String value, Collector> out) {
        Stream.of(value.toLowerCase().split("\\W+"))
          .filter(t -> t.length() > 0)
          .forEach(token -> out.collect(new Tuple2<>(token, 1)));
    }
}

Nous appelons la méthodecollect() sur la classeCollector pour faire avancer les données dans le pipeline de traitement.

Notre prochaine et dernière étape consiste à grouper les tuples par leurs premiers éléments (mots), puis à effectuer un agrégatsum sur les seconds éléments pour produire un décompte des occurrences de mot:

public static DataSet> startWordCount(
  ExecutionEnvironment env, List lines) throws Exception {
    DataSet text = env.fromCollection(lines);

    return text.flatMap(new LineSplitter())
      .groupBy(0)
      .aggregate(Aggregations.SUM, 1);
}

Nous utilisons trois types de transformations Flink:flatMap(),groupBy() etaggregate().

Écrivons un test pour affirmer que l'implémentation du nombre de mots fonctionne comme prévu:

List lines = Arrays.asList(
  "This is a first sentence",
  "This is a second sentence with a one word");

DataSet> result = WordCount.startWordCount(env, lines);

List> collect = result.collect();

assertThat(collect).containsExactlyInAnyOrder(
  new Tuple2<>("a", 3), new Tuple2<>("sentence", 2), new Tuple2<>("word", 1),
  new Tuple2<>("is", 2), new Tuple2<>("this", 2), new Tuple2<>("second", 1),
  new Tuple2<>("first", 1), new Tuple2<>("with", 1), new Tuple2<>("one", 1));

6. API DataStream

6.1. Créer un DataStream

Apache Flink prend également en charge le traitement des flux d'événements via son API DataStream. Si nous voulons commencer à consommer des événements, nous devons d'abord utiliser la classeStreamExecutionEnvironment:

StreamExecutionEnvironment executionEnvironment
 = StreamExecutionEnvironment.getExecutionEnvironment();

Ensuite, nous pouvons créer un flux d'événements en utilisant lesexecutionEnvironment à partir de diverses sources. Cela pourrait être un bus de messages commeApache Kafka, mais dans cet exemple, nous allons simplement créer une source à partir de quelques éléments de chaîne:

DataStream dataStream = executionEnvironment.fromElements(
  "This is a first sentence",
  "This is a second sentence with a one word");

Nous pouvons appliquer des transformations à chaque élément desDataStream comme dans la classe normaleDataSet:

SingleOutputStreamOperator upperCase = text.map(String::toUpperCase);

Pour déclencher l'exécution, nous devons invoquer une opération de puits telle queprint() qui affichera simplement le résultat des transformations sur la sortie standard, en suivant la méthodeexecute() sur la classeStreamExecutionEnvironment:

upperCase.print();
env.execute();

Il produira la sortie suivante:

1> THIS IS A FIRST SENTENCE
2> THIS IS A SECOND SENTENCE WITH A ONE WORD

6.2. Fenêtrage d'événements

Lors du traitement d'un flux d'événements en temps réel, il peut parfois être nécessaire de regrouper des événements et d'appliquer des calculs à une fenêtre de ces événements.

Supposons que nous ayons un flux d’événements, où chaque événement est une paire composée du numéro de l’événement et de l’horodatage de son envoi à notre système, et que nous pouvons tolérer les événements non conformes dans l’ordre mais uniquement s’ils ne le sont pas. plus de vingt secondes de retard.

Pour cet exemple, créons d'abord un flux simulant deux événements espacés de plusieurs minutes et définissons un extracteur d'horodatage qui spécifie notre seuil de retard:

SingleOutputStreamOperator> windowed
  = env.fromElements(
  new Tuple2<>(16, ZonedDateTime.now().plusMinutes(25).toInstant().getEpochSecond()),
  new Tuple2<>(15, ZonedDateTime.now().plusMinutes(2).toInstant().getEpochSecond()))
  .assignTimestampsAndWatermarks(
    new BoundedOutOfOrdernessTimestampExtractor
      >(Time.seconds(20)) {

        @Override
        public long extractTimestamp(Tuple2 element) {
          return element.f1 * 1000;
        }
    });

Ensuite, définissons une opération de fenêtre pour regrouper nos événements en fenêtres de cinq secondes et appliquer une transformation à ces événements:

SingleOutputStreamOperator> reduced = windowed
  .windowAll(TumblingEventTimeWindows.of(Time.seconds(5)))
  .maxBy(0, true);
reduced.print();

Il aura le dernier élément de chaque fenêtre de cinq secondes, donc il affiche:

1> (15,1491221519)

Notez que nous ne voyons pas le deuxième événement car il est arrivé plus tard que le seuil de retard spécifié.

7. Conclusion

Dans cet article, nous avons présenté le framework Apache Flink et examiné certaines des transformations fournies avec son API.

Nous avons mis en œuvre un programme de comptage de mots à l'aide de l'API DataSet fluide et fonctionnelle de Flink. Nous avons ensuite examiné l'API DataStream et mis en œuvre une simple transformation en temps réel sur un flux d'événements.

L'implémentation de tous ces exemples et extraits de code peut être trouvéeover on GitHub - c'est un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.