Kafkaで処理した直後

1概要

このチュートリアルでは、Kafkaが新しく導入されたTransactional APIを介してプロデューサーアプリケーションとコンシューマーアプリケーション間で1回限りの配信を保証する方法について説明します。

さらに、このAPIを使用して、トランザクションプロデューサとコンシューマを実装し、WordCountの例ではエンドツーエンドの正確に1回の配信を実現します。

2 Kafkaでのメッセージ配信

さまざまな障害のため、メッセージングシステムはプロデューサアプリケーションとコンシューマアプリケーション間のメッセージ配信を保証できません。クライアントアプリケーションがそのようなシステムとどのように対話するかに応じて、次のメッセージセマンティクスが可能です。

  • メッセージングシステムがメッセージを複製することはありませんが、見逃す可能性がある場合

時折メッセージ、我々はそれを at-most-once と呼びます ** または、メッセージを見逃すことはありませんが、ときどき重複する可能性がある場合は、

メッセージ、それを at-least-once と呼びます。 しかし、それが常に複製なしですべてのメッセージを配信するのであれば、それは

一回だけ **

当初、Kafkaは最大1回と少なくとも1回のメッセージ配信のみをサポートしていました。

ただし、** Kafkaブローカーとクライアントアプリケーション間のトランザクションの導入により、Kafkaでの1回限りの配信が保証されます。理解を深めるために、トランザクションクライアントAPIをすぐに確認しましょう。

3 Mavenの依存関係

トランザクションAPIを使用するには、Pomにhttps://search.maven.org/search?q=g:org.apache.kafka%20AND%20a:kafka-clients[KafkaのJavaクライアント]が必要です。

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.0.0</version>
</dependency>

4トランザクション型の変換 - 変換 - 生成ループ

この例では、入力トピック sentences からのメッセージを消費します。

次に、文ごとにすべての単語を数え、個々の単語数を出力トピック counts に送信します。

この例では、 sentences トピックにトランザクションデータがすでにあると仮定します。

4.1. トランザクション対応プロデューサ

それでは、まず典型的なKafkaプロデューサーを追加しましょう。

Properties producerProps = new Properties();
producerProps.put("bootstrap.servers", "localhost:9092");

ただし、 transactional.id を指定して idempotence を有効にする必要があります。

producerProps.put("enable.idempotence", "true");
producerProps.put("transactional.id", "prod-1");

KafkaProducer<String, String> producer = new KafkaProducer(producerProps);

冪等性を有効にしたので、KafkaはこのトランザクションIDをアルゴリズムの一部として使用して、このプロデューサーが送信するすべてのメッセージを重複排除し、冪等性を保証します。

簡単に言うと、プロデューサーが誤って同じメッセージをKafkaに複数回送信した場合、これらの設定によって通知が可能になります。

再起動後も一貫していますが、トランザクションIDが各プロデューサで異なることを確認することだけです。

4.2. トランザクションのプロデューサを有効にする

準備ができたら、 __ initTransaction __を呼び出して、プロデューサがトランザクションを使用できるように準備する必要があります。

producer.initTransactions();

これは、プロデューサをトランザクションを使用できるものとしてブローカに登録し、 ____transactional.idとシーケンス番号、またはエポック で識別します。次に、ブローカーはこれらを使用して、トランザクションログにアクションを先読みします。

そしてその結果、ブローカーは、同じトランザクションIDと以前の エポックを持つプロデューサーに属するそのログからのすべてのアクションを削除します。

4.3. トランザクション対応のコンシューマ

消費すると、トピックパーティション上のすべてのメッセージを順番に読み取ることができます。ただし、** 関連するトランザクションがコミットされるまでトランザクションメッセージの読み取りを待つ必要があることを isolation.level で示すことができます。

Properties consumerProps = new Properties();
consumerProps.put("bootstrap.servers", "localhost:9092");
consumerProps.put("group.id", "my-group-id");
consumerProps.put("enable.auto.commit", "false");
consumerProps.put("isolation.level", "read__committed");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);
consumer.subscribe(singleton(“sentences”));
  • read committed__の値を使用すると、トランザクションが完了する前にトランザクションメッセージを読むことがなくなります。**

isolation.level のデフォルト値は read uncommitted__です。

4.4. トランザクションによる消費と変換

プロデューサとコンシューマの両方がトランザクションによる書き込みと読み取りを行うように設定されたので、入力トピックからレコードを消費し、各レコードの各単語を数えることができます。

ConsumerRecords<String, String> records = consumer.poll(ofSeconds(60));
Map<String, Integer> wordCountMap =
  records.records(new TopicPartition("input", 0))
    .stream()
    .flatMap(record -> Stream.of(record.value().split(" ")))
    .map(word -> Tuple.of(word, 1))
    .collect(Collectors.toMap(tuple ->
      tuple.getKey(), t1 -> t1.getValue(), (v1, v2) -> v1 + v2));

上記のコードに関してトランザクション的なものは何もないことに注意してください。ただし、** read committedを使用したので、同じトランザクション内で入力トピックに書き込まれたメッセージは、すべて書き込まれるまでこのコンシューマによって読み取られません。

これで、計算した単語数を出力トピックに送信できます。

トランザクションを使用して、結果をどのように生成できるかを見てみましょう。

4.5. 送信API

カウントを新しいメッセージとして送信するには、同じトランザクション内で beginTransaction を呼び出します。

producer.beginTransaction();

それでは、キーを単語に、カウントを値にして、それぞれを「件数」トピックに書き込むことができます。

wordCountMap.forEach((key,value) ->
    producer.send(new ProducerRecord<String,String>("counts",key,value.toString())));

プロデューサはデータをキーで分割できるので、トランザクションメッセージは複数のパーティションにまたがることができ、それぞれが別々のコンシューマによって読み込まれることになります。

トランザクション内で、プロデューサは複数のスレッドを使用してレコードを並行して送信できます。

4.6. オフセットの確定

そして最後に、消費したばかりのオフセットをコミットする必要があります。 トランザクションでは、通常どおりオフセットを読み取り元の入力トピックにコミットします。 ただし、** それらをプロデューサのトランザクションに送信します。

これをすべて1回の呼び出しで実行できますが、最初に各トピックパーティションのオフセットを計算する必要があります。

Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = new HashMap<>();
for (TopicPartition partition : records.partitions()) {
    List<ConsumerRecord<String, String>> partitionedRecords = records.records(partition);
    long offset = partitionedRecords.get(partitionedRecords.size() - 1).offset();
    offsetsToCommit.put(partition, new OffsetAndMetadata(offset + 1));
}
  • トランザクションにコミットしているのは次のオフセットであり、1を追加する必要があることに注意してください。

次に、計算したオフセットをトランザクションに送信します。

producer.sendOffsetsToTransaction(offsetsToCommit, "my-group-id");

4.7. トランザクションのコミットまたは中止

そして最後に、トランザクションをコミットすることができます。これはオフセットをトランザクション自体と同様に __consumer offsets ____topicにアトミックに書き込みます。

producer.commitTransaction();

これはバッファされたメッセージをそれぞれのパーティションにフラッシュします。さらに、Kafkaブローカーはそのトランザクション内のすべてのメッセージを消費者が利用できるようにします。

もちろん、処理中に何か問題が発生した場合、たとえば例外をキャッチした場合、__abortTransactionを呼び出すことができます。

try {
 //... read from input topic
 //... transform
 //... write to output topic
  producer.commitTransaction();
} catch ( Exception e ) {
  producer.abortTransaction();
}

そして、バッファリングされたメッセージを削除して、ブローカからトランザクションを削除します。

  • ブローカーで設定された__max.transaction.timeout.msの前にコミットも中止もしない場合、Kafkaブローカーはトランザクション自体を中止します。 ** このプロパティのデフォルト値は900,000ミリ秒または15分です。

5その他の消費 - 変換 - 生成

今見たのは、同じKafkaクラスタに対して読み書きを行う基本的な consume-transform-produce ループです。

逆に、異なるKafkaクラスタに対して読み書きを行う必要があるアプリケーションは、古い __commitSync および commitAsync __APIを使用する必要があります。

通常、アプリケーションは、トランザクション性を維持するためにコンシューマオフセットを外部の状態ストレージに格納します。

6. 結論

データが重要なアプリケーションでは、エンドツーエンドの1回限りの処理が不可欠です。

このチュートリアルでは、トランザクションを使用してKafkaを使用してこれを正確に実行する方法を説明し、その原理を説明するためにトランザクションベースの単語カウントの例を実装しました。

GitHubのコードサンプル をすべてチェックしてください。