Введение в Apache Flink с Java

1. обзор

Apache Flink - это среда обработки больших данных, которая позволяет программистам обрабатывать огромное количество данных очень эффективным и масштабируемым образом.

В этой статье мы познакомим вас с некоторымиcore API concepts and standard data transformations available in the Apache Flink Java API. Свободный стиль этого API позволяет легко работать с центральной конструкцией Flink - распределенной коллекцией.

Сначала мы взглянем на преобразования API FlinkDataSet и воспользуемся ими для реализации программы подсчета слов. Затем мы кратко рассмотрим API FlinkDataStream, который позволяет обрабатывать потоки событий в режиме реального времени.

2. Maven Dependency

Для начала нам нужно добавить зависимости Maven в библиотекиflink-java иflink-test-utils:


    org.apache.flink
    flink-java
    1.2.0


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

3. Основные концепции API

При работе с Flink нам нужно знать несколько вещей, связанных с его API:

  • Every Flink program performs transformations on distributed collections of data. Предоставляется множество функций для преобразования данных, включая фильтрацию, отображение, объединение, группировку и агрегирование.

  • A sink operation in Flink triggers the execution of a stream to produce the desired result of the program, например, сохранение результата в файловой системе или его печать на стандартный вывод

  • Преобразования Flink ленивы, то есть они не выполняются до тех пор, пока не будет вызвана операцияsink

  • The Apache Flink API supports two modes of operations — batch and real-time. Если вы имеете дело с ограниченным источником данных, который можно обрабатывать в пакетном режиме, вы будете использовать APIDataSet. Если вы хотите обрабатывать неограниченные потоки данных в реальном времени, вам нужно будет использовать APIDataStream

4. Преобразования DataSet API

Точкой входа в программу Flink является экземпляр классаExecutionEnvironment - он определяет контекст, в котором выполняется программа.

Давайте создадимExecutionEnvironment, чтобы начать нашу обработку:

ExecutionEnvironment env
  = ExecutionEnvironment.getExecutionEnvironment();

Note that when you launch the application on the local machine, it will perform processing on the local JVM. Если вы хотите начать обработку на кластере машин, вам нужно будет установитьApache Flink на этих машинах и соответствующим образом настроитьExecutionEnvironment.

4.1. Создание набора данных

Чтобы начать выполнять преобразование данных, нам нужно снабдить нашу программу данными.

Давайте создадим экземпляр классаDataSet, используя нашExecutionEnvironement:

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

Вы можете создатьDataSet из нескольких источников, таких как Apache Kafka, CSV, файл или практически любой другой источник данных.

4.2. Фильтр и уменьшение

Создав экземпляр классаDataSet, вы можете применять к нему преобразования.

Допустим, вы хотите отфильтровать числа, превышающие определенный порог, а затем просуммировать их все.. Для этого можно использовать преобразованияfilter() иreduce():

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

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

Обратите внимание, что методcollect() - это операцияsink, которая запускает фактические преобразования данных.

4.3. Map

Допустим, у вас естьDataSet объектовPerson:

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

    // standard constructors/getters/setters
}

Затем давайте создадимDataSet этих объектов:

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

Предположим, вы хотите извлечь только полеage из каждого объекта коллекции. Вы можете использовать преобразованиеmap(), чтобы получить только определенное поле классаPerson:

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

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

4.4. Join

Когда у вас есть два набора данных, вы можете захотеть объединить их в каком-либо полеid. Для этого вы можете использовать преобразованиеjoin().

Создадим коллекции транзакций и адресов пользователя:

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

Первое поле в обоих кортежах имеет типInteger, и это полеid, в котором мы хотим объединить оба набора данных.

Чтобы выполнить фактическую логику соединения, нам нужно реализовать интерфейсKeySelector для адреса и транзакции:

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

Каждый селектор возвращает только поле, в котором должно выполняться соединение.

К сожалению, здесь нельзя использовать лямбда-выражения, потому что Flink нужна информация об общем типе.

Затем давайте реализуем логику слияния, используя эти селекторы:

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

Допустим, у вас есть следующая коллекцияTuple2:

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

Если вы хотите отсортировать эту коллекцию по первому полю кортежа, вы можете использовать преобразованиеsortPartitions():

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

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

5. Количество слов

Проблема с подсчетом слов часто используется для демонстрации возможностей сред обработки больших данных. Основное решение включает подсчет вхождений слов в текстовом вводе. Давайте воспользуемся Flink, чтобы реализовать решение этой проблемы.

В качестве первого шага в нашем решении мы создаем классLineSplitter, который разделяет наш ввод на токены (слова), собирая для каждого токенаTuple2 пар ключ-значение. В каждом из этих кортежей ключ - это слово, найденное в тексте, а значение - целое число (1).

Этот класс реализует интерфейсFlatMapFunction, который принимаетString в качестве входных данных и выдаетTuple2<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)));
    }
}

Мы вызываем методcollect() в классеCollector, чтобы продвигать данные вперед в конвейере обработки.

Наш следующий и последний шаг - сгруппировать кортежи по их первым элементам (словам), а затем выполнить агрегированиеsum для вторых элементов, чтобы произвести подсчет вхождений слов:

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

Мы используем три типа преобразований Flink:flatMap(),groupBy() иaggregate().

Давайте напишем тест, чтобы убедиться, что реализация подсчета слов работает должным образом:

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. DataStream API

6.1. Создание потока данных

Apache Flink также поддерживает обработку потоков событий через API-интерфейс DataStream. Если мы хотим начать принимать события, нам сначала нужно использовать классStreamExecutionEnvironment:

StreamExecutionEnvironment executionEnvironment
 = StreamExecutionEnvironment.getExecutionEnvironment();

Затем мы можем создать поток событий, используяexecutionEnvironment из различных источников. Это может быть некоторая шина сообщений, напримерApache Kafka, но в этом примере мы просто создадим источник из пары строковых элементов:

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

Мы можем применить преобразования к каждому элементуDataStream, как в обычном классеDataSet:

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

Чтобы запустить выполнение, нам нужно вызвать операцию приемника, такую ​​какprint(), которая будет просто выводить результат преобразований в стандартный вывод, следуя методуexecute() в классеStreamExecutionEnvironment:

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

Он выдаст следующий вывод:

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

6.2. Окно событий

При обработке потока событий в реальном времени иногда может потребоваться сгруппировать события вместе и применить некоторые вычисления к окну этих событий.

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

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

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

Затем давайте определим операцию окна, чтобы сгруппировать наши события в пятисекундные окна и применить преобразование к этим событиям:

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

Он получит последний элемент каждого пятисекундного окна, поэтому выводит:

1> (15,1491221519)

Обратите внимание, что мы не видим второе событие, потому что оно пришло позже указанного порога задержки.

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

В этой статье мы познакомились с платформой Apache Flink и рассмотрели некоторые преобразования, входящие в ее API.

Мы реализовали программу подсчета слов, используя удобный и функциональный API DataSet от Flink. Затем мы рассмотрели API-интерфейс DataStream и реализовали простое преобразование потока событий в реальном времени.

Реализацию всех этих примеров и фрагментов кода можно найти вover on GitHub - это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.