JavaによるApache Flinkの紹介

1. 概要

Apache Flinkは、プログラマーが非常に効率的でスケーラブルな方法で膨大な量のデータを処理できるビッグデータ処理フレームワークです。

この記事では、いくつかのcore API concepts and standard data transformations available in the Apache Flink Java APIを紹介します。 このAPIの流暢なスタイルにより、Flinkの中心的な構成要素である分散コレクションを簡単に操作できます。

まず、FlinkのDataSet API変換を確認し、それらを使用して単語カウントプログラムを実装します。 次に、FlinkのDataStream APIについて簡単に説明します。これにより、イベントのストリームをリアルタイムで処理できます。

2. メーベン依存

開始するには、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.バッチモードで処理できる限られたデータソースを扱っている場合は、DataSetAPIを使用します。 無制限のデータストリームをリアルタイムで処理する場合は、DataStreamAPIを使用する必要があります

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. データセットの作成

データ変換の実行を開始するには、プログラムにデータを提供する必要があります。

ExecutionEnvironementを使用してDataSetクラスのインスタンスを作成しましょう。

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

Apache Kafka、CSV、ファイル、または事実上他の任意のデータソースなど、複数のソースからDataSetを作成できます。

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

PersonオブジェクトのDataSetがあるとしましょう:

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

2つのデータセットがある場合、それらをいくつかの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です。

このクラスは、Stringを入力として受け取り、Tuple2<String, Integer>:を生成するFlatMapFunctionインターフェイスを実装します。

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

Collectorクラスでcollect()メソッドを呼び出して、処理パイプラインでデータを転送します。

次の最後のステップは、タプルを最初の要素(単語)でグループ化し、2番目の要素で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);
}

flatMap()groupBy()、およびaggregate()の3種類のFlink変換を使用しています。

単語数の実装が期待どおりに機能していることを確認するテストを作成しましょう。

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は、DataStream APIを介したイベントストリームの処理もサポートしています。 イベントの消費を開始する場合は、最初に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");

通常のDataSetクラスのように、DataStreamのすべての要素に変換を適用できます。

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

実行をトリガーするには、StreamExecutionEnvironmentクラスのexecute()メソッドに続いて、変換の結果を標準出力に出力するprint()などのシンク操作を呼び出す必要があります。

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

次のような出力が生成されます。

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

6.2. イベントのウィンドウ処理

イベントのストリームをリアルタイムで処理する場合、イベントをグループ化し、それらのイベントのウィンドウに何らかの計算を適用する必要がある場合があります。

イベントのストリームがあり、各イベントはイベント番号とイベントがシステムに送信されたときのタイムスタンプで構成されるペアであり、順不同であるが、そうでない場合にのみ許容できると仮定します20秒以上遅れています。

この例では、最初に、数分間隔の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;
        }
    });

次に、イベントを5秒のウィンドウにグループ化し、それらのイベントに変換を適用するウィンドウ操作を定義しましょう。

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

5秒ごとのウィンドウの最後の要素を取得するため、次のように出力されます。

1> (15,1491221519)

2番目のイベントは、指定された遅延しきい値よりも後に到着したため、表示されないことに注意してください。

7. 結論

この記事では、Apache Flinkフレームワークを紹介し、そのAPIで提供される変換のいくつかを調べました。

Flinkの流暢で機能的なDataSetAPIを使用して単語カウントプログラムを実装しました。 次に、DataStream APIを見て、イベントのストリームに単純なリアルタイム変換を実装しました。

これらすべての例とコードスニペットの実装はover on GitHubにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。