FlinkとKafkaを使ってデータパイプラインを構築する

1. 概要

Apache Flinkは、Javaで簡単に使用できるストリーム処理フレームワークです。 Apache Kafkaは、高いフォールトトレランスをサポートする分散ストリーム処理システムです。

このチュートリアルでは、これらの2つのテクノロジーを使用してデータパイプラインを構築する方法を見ていきます。

2. インストール

Apache Kafkaをインストールして構成するには、official guideを参照してください。 インストール後、次のコマンドを使用して、flink_inputおよびflink_output:という新しいトピックを作成できます。

 bin/kafka-topics.sh --create \
  --zookeeper localhost:2181 \
  --replication-factor 1 --partitions 1 \
  --topic flink_output

 bin/kafka-topics.sh --create \
  --zookeeper localhost:2181 \
  --replication-factor 1 --partitions 1 \
  --topic flink_input

このチュートリアルでは、ApacheKafkaのデフォルトの構成とデフォルトのポートを使用します。

Apache Flinkは、リアルタイムのストリーム処理技術を可能にします。 The framework allows using multiple third-party systems as stream sources or sinks

Flinkでは、さまざまなコネクタを使用できます。

  • Apache Kafka(ソース/シンク)

  • Apache Cassandra(シンク)

  • Amazon Kinesis Streams(ソース/シンク)

  • Elasticsearch(シンク)

  • Hadoop FileSystem(シンク)

  • RabbitMQ(ソース/シンク)

  • Apache NiFi(ソース/シンク)

  • Twitter Streaming API(ソース)

Flinkをプロジェクトに追加するには、次のMaven依存関係を含める必要があります。


    org.apache.flink
    flink-core
    1.5.0


    org.apache.flink
    flink-connector-kafka-0.11_2.11
    1.5.0

これらの依存関係を追加することで、Kafkaトピックを利用したり、Kafkaトピックを作成したりできます。 Flinkの現在のバージョンはMaven Centralにあります。

4. カフカストリングコンシューマー

To consume data from Kafka with Flink we need to provide a topic and a Kafka address.オフセットを保持するために使用されるグループIDも指定する必要があります。これにより、データ全体を最初から読み取るとは限りません。

FlinkKafkaConsumer の作成を簡単にする静的メソッドを作成しましょう。

public static FlinkKafkaConsumer011 createStringConsumerForTopic(
  String topic, String kafkaAddress, String kafkaGroup ) {

    Properties props = new Properties();
    props.setProperty("bootstrap.servers", kafkaAddress);
    props.setProperty("group.id",kafkaGroup);
    FlinkKafkaConsumer011 consumer = new FlinkKafkaConsumer011<>(
      topic, new SimpleStringSchema(), props);

    return consumer;
}

このメソッドは、topic, kafkaAddress,kafkaGroupを取り、SimpleStringSchemaを使用してデータをデコードしたため、特定のトピックのデータをStringとして消費するFlinkKafkaConsumerを作成します。

クラス名の011は、Kafkaバージョンを示しています。

5. カフカストリングプロデューサー

To produce data to Kafka, we need to provide Kafka address and topic that we want to use.繰り返しますが、さまざまなトピックのプロデューサーを作成するのに役立つ静的メソッドを作成できます。

public static FlinkKafkaProducer011 createStringProducer(
  String topic, String kafkaAddress){

    return new FlinkKafkaProducer011<>(kafkaAddress,
      topic, new SimpleStringSchema());
}

このメソッドは、Kafkaトピックに作成するときにグループIDを指定する必要がないため、引数としてtopickafkaAddressのみを取ります。

6. 文字列ストリーム処理

完全に機能する消費者と生産者がいる場合、Kafkaからのデータを処理し、結果をKafkaに保存して戻すことができます。 ストリーム処理に使用できる関数の完全なリストは、hereにあります。

この例では、各Kafkaエントリの単語を大文字にしてから、Kafkaに書き戻します。

この目的のために、カスタムのMapFunctionを作成する必要があります。

public class WordsCapitalizer implements MapFunction {
    @Override
    public String map(String s) {
        return s.toUpperCase();
    }
}

関数を作成した後、ストリーム処理で使用できます。

public static void capitalize() {
    String inputTopic = "flink_input";
    String outputTopic = "flink_output";
    String consumerGroup = "example";
    String address = "localhost:9092";
    StreamExecutionEnvironment environment = StreamExecutionEnvironment
      .getExecutionEnvironment();
    FlinkKafkaConsumer011 flinkKafkaConsumer = createStringConsumerForTopic(
      inputTopic, address, consumerGroup);
    DataStream stringInputStream = environment
      .addSource(flinkKafkaConsumer);

    FlinkKafkaProducer011 flinkKafkaProducer = createStringProducer(
      outputTopic, address);

    stringInputStream
      .map(new WordsCapitalizer())
      .addSink(flinkKafkaProducer);
}

アプリケーションは、flink_inputトピックからデータを読み取り、ストリームに対して操作を実行してから、結果をKafkaのflink_output topicに保存します。

FlinkとKafkaを使用して文字列を処理する方法を見てきました。 ただし、多くの場合、カスタムオブジェクトに対して操作を実行する必要があります。 これを行う方法については、次の章で説明します。

7. カスタムオブジェクトの逆シリアル化

次のクラスは、送信者と受信者に関する情報を含む単純なメッセージを表します。

@JsonSerialize
public class InputMessage {
    String sender;
    String recipient;
    LocalDateTime sentAt;
    String message;
}

以前は、SimpleStringSchemaを使用してKafkaからのメッセージを逆シリアル化していましたが、現在はwe want to deserialize data directly to custom objectsを使用しています。

これを行うには、カスタムDeserializationSchema:が必要です

public class InputMessageDeserializationSchema implements
  DeserializationSchema {

    static ObjectMapper objectMapper = new ObjectMapper()
      .registerModule(new JavaTimeModule());

    @Override
    public InputMessage deserialize(byte[] bytes) throws IOException {
        return objectMapper.readValue(bytes, InputMessage.class);
    }

    @Override
    public boolean isEndOfStream(InputMessage inputMessage) {
        return false;
    }

    @Override
    public TypeInformation getProducedType() {
        return TypeInformation.of(InputMessage.class);
    }
}

ここでは、メッセージはKafkaでJSONとして保持されると想定しています。

タイプLocalDateTimeのフィールドがあるため、LocalDateTimeオブジェクトのJSONへのマッピングを処理するJavaTimeModule, を指定する必要があります。

Flink schemas can’t have fields that aren’t serializableは、すべての演算子(スキーマや関数など)がジョブの開始時にシリアル化されるためです。

Apache Sparkにも同様の問題があります。 この問題の既知の修正の1つは、上記のObjectMapperで行ったように、フィールドをstaticとして初期化することです。 これは最も美しいソリューションではありませんが、比較的単純であり、機能します。

メソッドisEndOfStreamは、特定のデータが受信されるまでストリームを処理する必要がある特別な場合に使用できます。 しかし、私たちの場合は必要ありません。

8. カスタムオブジェクトのシリアル化

ここで、システムにメッセージのバックアップを作成する可能性を持たせたいと仮定しましょう。 プロセスを自動化し、各バックアップは1日に送信されるメッセージで構成する必要があります。

また、バックアップメッセージには一意のIDが割り当てられている必要があります。

この目的のために、次のクラスを作成できます。

public class Backup {
    @JsonProperty("inputMessages")
    List inputMessages;
    @JsonProperty("backupTimestamp")
    LocalDateTime backupTimestamp;
    @JsonProperty("uuid")
    UUID uuid;

    public Backup(List inputMessages,
      LocalDateTime backupTimestamp) {
        this.inputMessages = inputMessages;
        this.backupTimestamp = backupTimestamp;
        this.uuid = UUID.randomUUID();
    }
}

UUID生成メカニズムは重複を許可するため、完全ではないことに注意してください。 ただし、この例の範囲ではこれで十分です。

BackupオブジェクトをJSONとしてKafkaに保存したいので、SerializationSchemaを作成する必要があります。

public class BackupSerializationSchema
  implements SerializationSchema {

    ObjectMapper objectMapper;
    Logger logger = LoggerFactory.getLogger(BackupSerializationSchema.class);

    @Override
    public byte[] serialize(Backup backupMessage) {
        if(objectMapper == null) {
            objectMapper = new ObjectMapper()
              .registerModule(new JavaTimeModule());
        }
        try {
            return objectMapper.writeValueAsString(backupMessage).getBytes();
        } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
            logger.error("Failed to parse JSON", e);
        }
        return new byte[0];
    }
}

9. タイムスタンプメッセージ

毎日のすべてのメッセージのバックアップを作成するため、メッセージにはタイムスタンプが必要です。

Flinkは、3つの異なる時間特性EventTime, ProcessingTime, IngestionTime.を提供します

この場合、メッセージが送信された時刻を使用する必要があるため、EventTime.を使用します。

EventTimewe need a TimestampAssigner which will extract timestamps from our input dataを使用するには:

public class InputMessageTimestampAssigner
  implements AssignerWithPunctuatedWatermarks {

    @Override
    public long extractTimestamp(InputMessage element,
      long previousElementTimestamp) {
        ZoneId zoneId = ZoneId.systemDefault();
        return element.getSentAt().atZone(zoneId).toEpochSecond() * 1000;
    }

    @Nullable
    @Override
    public Watermark checkAndGetNextWatermark(InputMessage lastElement,
      long extractedTimestamp) {
        return new Watermark(extractedTimestamp - 1500);
    }
}

LocalDateTimeEpochSecondに変換する必要があります。これは、Flinkで期待される形式だからです。 タイムスタンプを割り当てた後、すべての時間ベースの操作は、sentAtフィールドからの時間を使用して操作します。

Flinkはタイムスタンプがミリ秒単位であると想定し、toEpochSecond()は秒単位で時間を返すため、1000を掛ける必要があり、Flinkはウィンドウを正しく作成します。

Flinkは、Watermark. Watermarks are useful in case of data that don’t arrive in the order they were sent.の概念を定義します。透かしは、要素の処理に許可される最大遅延を定義します。

透かしよりも低いタイムスタンプを持つ要素は、まったく処理されません。

10. 時間ウィンドウの作成

バックアップが1日の間に送信されたメッセージのみを収集するようにするために、ストリームでtimeWindowAllメソッドを使用できます。これにより、メッセージがウィンドウに分割されます。

ただし、各ウィンドウからのメッセージを集約して、Backupとして返す必要があります。

これを行うには、カスタムのAggregateFunctionが必要です。

public class BackupAggregator
  implements AggregateFunction, Backup> {

    @Override
    public List createAccumulator() {
        return new ArrayList<>();
    }

    @Override
    public List add(
      InputMessage inputMessage,
      List inputMessages) {
        inputMessages.add(inputMessage);
        return inputMessages;
    }

    @Override
    public Backup getResult(List inputMessages) {
        return new Backup(inputMessages, LocalDateTime.now());
    }

    @Override
    public List merge(List inputMessages,
      List acc1) {
        inputMessages.addAll(acc1);
        return inputMessages;
    }
}

11. バックアップの集約

適切なタイムスタンプを割り当て、AggregateFunctionを実装したら、最終的にKafka入力を取得して処理できます。

public static void createBackup () throws Exception {
    String inputTopic = "flink_input";
    String outputTopic = "flink_output";
    String consumerGroup = "example";
    String kafkaAddress = "192.168.99.100:9092";
    StreamExecutionEnvironment environment
      = StreamExecutionEnvironment.getExecutionEnvironment();
    environment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime);
    FlinkKafkaConsumer011 flinkKafkaConsumer
      = createInputMessageConsumer(inputTopic, kafkaAddress, consumerGroup);
    flinkKafkaConsumer.setStartFromEarliest();

    flinkKafkaConsumer.assignTimestampsAndWatermarks(
      new InputMessageTimestampAssigner());
    FlinkKafkaProducer011 flinkKafkaProducer
      = createBackupProducer(outputTopic, kafkaAddress);

    DataStream inputMessagesStream
      = environment.addSource(flinkKafkaConsumer);

    inputMessagesStream
      .timeWindowAll(Time.hours(24))
      .aggregate(new BackupAggregator())
      .addSink(flinkKafkaProducer);

    environment.execute();
}

12. 結論

この記事では、ApacheFlinkとApacheKafkaを使用して簡単なデータパイプラインを作成する方法を紹介しました。

いつものように、コードはover on Githubで見つけることができます。