web-dev-qa-db-ja.com

KafkaのStreams APIを使用して不良メッセージを処理する

次のような基本的なストリーム処理フローがあります

master topic -> my processing in a mapper/filter -> output topics

そして、「悪いメッセージ」を処理する最良の方法について疑問に思っています。これは、潜在的に私が適切にデシリアライズできないメッセージのようなものである可能性があります、またはおそらく処理/フィルタリングロジックが何らかの予期しない方法で失敗します(外部依存関係がないため、そのような一時的なエラーはありません).

私はすべての処理/フィルタリングコードをtry catchでラップし、例外が発生した場合は「エラートピック」にルーティングすることを検討していました。その後、メッセージを調べて、必要に応じて修正またはコードを修正し、マスターで再生します。例外を伝播させると、ストリームが詰まったように見え、これ以上メッセージがピックアップされません。

  • このアプローチはベストプラクティスと見なされますか?
  • 便利なKafkaこれを処理するストリームの方法はありますか?DLQの概念があるとは思わない...
  • Kafka=「悪いメッセージ」で妨害するのを止める別の方法は何ですか?
  • どのような代替エラー処理アプローチがありますか?

完全を期すために、ここに私のコードがあります(擬似的な):

class Document {
    // Fields
}

class AnalysedDocument {

    Document document;
    String rawValue;
    Exception exception;
    Analysis analysis;

    // All being well
    AnalysedDocument(Document document, Analysis analysis) {...}

    // Analysis failed
    AnalysedDocument(Document document, Exception exception) {...}

    // Deserialisation failed
    AnalysedDocument(String rawValue, Exception exception) {...}
}

KStreamBuilder builder = new KStreamBuilder();
KStream<String, AnalysedPolecatDocument> analysedDocumentStream = builder
    .stream(Serdes.String(), Serdes.String(), "master")
    .mapValues(new ValueMapper<String, AnalysedDocument>() {
         @Override
         public AnalysedDocument apply(String rawValue) {
             Document document;
             try {
                 // Deserialise
                 document = ...
             } catch (Exception e) {
                 return new AnalysedDocument(rawValue, exception);
             }
             try {
                 // Perform analysis
                 Analysis analysis = ...
                 return new AnalysedDocument(document, analysis);
             } catch (Exception e) {
                 return new AnalysedDocument(document, exception);
             }
         }
    });

// Branch based on whether analysis mapping failed to produce errorStream and successStream
errorStream.to(Serdes.String(), customPojoSerde(), "error");
successStream.to(Serdes.String(), customPojoSerde(), "analysed");

KafkaStreams streams = new KafkaStreams(builder, config);
streams.start();

どんな助けも大歓迎です。

28
bm1729

現在、Kafka Streamsは限られたエラー処理機能しか提供していません。これを簡素化するための作業が進行中です。現在のところ、全体的なアプローチは良い方法です。

逆シリアル化エラーの処理に関する1つのコメント:それらのエラーを手動で処理するには、「手動で」逆シリアル化を行う必要があります。つまり、Streamsアプリの入力/出力トピックのキーと値にByteArraySerdesを構成し、デ/シリアル化を行うmap()を追加する必要があります(つまり、KStream<byte[],byte[]> -> map() -> KStream<keyType,valueType>-または、シリアル化の例外もキャッチしたい場合は逆になります)。それ以外の場合、try-catchシリアル化解除の例外はできません。

現在のアプローチでは、指定された文字列が有効なドキュメントを表していることを「のみ」検証しますが、メッセージ自体が破損しており、そもそもソース演算子でStringに変換できない場合があります。したがって、実際にはコードで逆シリアル化の例外をカバーしません。ただし、逆シリアル化の例外が発生しないことが確実な場合は、アプローチでも十分です。

更新

この問題は KIP-161 で解決され、次のリリース1.0.0に含まれます。パラメーターdefault.deserialization.exception.handlerを介してコールバックを登録できます。ハンドラーは、逆シリアル化中に例外が発生するたびに呼び出され、DeserializationResponseCONTINUE->ドロップ先のレコード、またはデフォルトのFAIL)を返すことができます。

更新2

KIP-21 (in Kafka 1.1の一部になります)では、ProductionExceptionHandlerを登録することにより、コンシューマー部分と同様に、プロデューサー側でエラーを処理することも可能です。 CONTINUEを返すことができるconfig default.production.exception.handler経由。

24
Matthias J. Sax

2018年3月23日更新:Kafka 1.0は、はるかに優れた簡単な機能を提供します KIP-161 を介した悪いエラーメッセージ(「毒薬」)の処理。 Kafka 1.0 docs。)の default.deserialization.exception.handler を参照してください。

これは、潜在的に私が適切にデシリアライズできないメッセージのようなものである可能性があります[...]

わかりました。ここでの私の答えは、ほとんどのユーザーにとって最も扱いにくいシナリオである可能性があるため、シリアル化の問題に焦点を当てています。

[...]または処理/フィルタリングロジックが予期せぬ方法で失敗する可能性があります(外部依存関係がないため、そのような一時的なエラーはありません)。

同じ考え方(逆シリアル化)は、処理ロジックの障害にも適用できます。ここでは、ほとんどの人が以下のオプション2(デシリアライゼーション部分を除く)に引き寄せられる傾向がありますが、YMMVです。

私はすべての処理/フィルタリングコードをtry catchでラップし、例外が発生した場合は「エラートピック」にルーティングすることを検討していました。その後、メッセージを調べて、必要に応じて修正またはコードを修正し、マスターで再生します。例外を伝播させると、ストリームが詰まったように見え、これ以上メッセージがピックアップされません。

  • このアプローチはベストプラクティスと見なされますか?

はい、現時点ではこれが道です。基本的に、2つの最も一般的なパターンは、(1)破損したメッセージをスキップするか、(2)破損したレコードを検疫トピック(デッドレターキュー)に送信することです。

  • 便利なKafkaこれを処理するストリームの方法はありますか?DLQの概念があるとは思わない...

はい、デッドレターキューの使用など、これを処理する方法があります。ただし、(少なくとも私見では)それほど便利ではありません。 APIでこれをどのように処理できるかについてのフィードバックがある場合-例:新しい方法または更新された方法、構成設定(「シリアル化/逆シリアル化に失敗して問題のあるレコードがこの検疫トピックに送信された場合」)をお知らせください。 :-)

  • Kafka=「悪いメッセージ」で妨害するのを止める別の方法は何ですか?
  • どのような代替エラー処理アプローチがありますか?

以下の例を参照してください。

FWIW、Kafkaコミュニティは、破損したメッセージをスキップできる新しいCLIツールの追加についても議論しています。ただし、Kafka Streams API、理想的には、このようなシナリオをコードで直接処理し、最後の手段としてのみCLIユーティリティにフォールバックすることをお勧めします。

以下に、Kafka Streams DSLが破損したレコード/メッセージ、別名「ポイズンピル」を処理するためのパターンを示します。これは http://docs.confluent.io/current/ streams/faq.html#handling-corrupted-records-and-deserialization-errors-poison-pill-messages

オプション1:flatMapで破損したレコードをスキップする

これはほとんどのユーザーがやりたいことです。

  • flatMapを使用するのは、入力レコードごとに0、1、またはそれ以上の出力レコードを出力できるためです。破損したレコードの場合、何も出力しない(ゼロのレコード)ため、破損したレコードを無視/スキップします。
  • ここにリストされている他のアプローチと比較したこのアプローチの利点:レコードを1回だけ手動でデシリアライズする必要があります!
  • このアプローチの欠点:flatMapは、潜在的なデータ再パーティション化のために入力ストリームを「マーク」します。つまり、グループ化(groupBy/groupByKeyなどのキーベースの操作を実行する場合)または後で結合すると、データはバックグラウンドで再パーティション化されます。これはコストのかかるステップになる可能性があるため、不必要にそれが発生することは望ましくありません。レコードキーが常に有効であることを知っている場合ORキーを操作する必要がない(したがって、byte[]形式)、flatMapからflatMapValuesに変更できます。これにより、後でストリームを結合/グループ化/集約しても、データの再パーティション化は行われません。

コード例:

Serde<byte[]> bytesSerde = Serdes.ByteArray();
Serde<String> stringSerde = Serdes.String();
Serde<Long> longSerde = Serdes.Long();

// Input topic, which might contain corrupted messages
KStream<byte[], byte[]> input = builder.stream(bytesSerde, bytesSerde, inputTopic);

// Note how the returned stream is of type KStream<String, Long>,
// rather than KStream<byte[], byte[]>.
KStream<String, Long> doubled = input.flatMap(
    (k, v) -> {
      try {
        // Attempt deserialization
        String key = stringSerde.deserializer().deserialize(inputTopic, k);
        long value = longSerde.deserializer().deserialize(inputTopic, v);

        // Ok, the record is valid (not corrupted).  Let's take the
        // opportunity to also process the record in some way so that
        // we haven't paid the deserialization cost just for "poison pill"
        // checking.
        return Collections.singletonList(KeyValue.pair(key, 2 * value));
      }
      catch (SerializationException e) {
        // log + ignore/skip the corrupted message
        System.err.println("Could not deserialize record: " + e.getMessage());
      }
      return Collections.emptyList();
    }
);

オプション2:branchを含むデッドレターキュー

オプション1(破損したレコードを無視する)と比較して、オプション2は「メイン」入力ストリームからメッセージをフィルタリングして検疫トピックに書き込むことにより、破損したメッセージを保持します(デッドレターキュー)。欠点は、有効なレコードのために、手動の逆シリアル化コストを2回支払う必要があることです。

KStream<byte[], byte[]> input = ...;

KStream<byte[], byte[]>[] partitioned = input.branch(
    (k, v) -> {
      boolean isValidRecord = false;
      try {
        stringSerde.deserializer().deserialize(inputTopic, k);
        longSerde.deserializer().deserialize(inputTopic, v);
        isValidRecord = true;
      }
      catch (SerializationException ignored) {}
      return isValidRecord;
    },
    (k, v) -> true
);

// partitioned[0] is the KStream<byte[], byte[]> that contains
// only valid records.  partitioned[1] contains only corrupted
// records and thus acts as a "dead letter queue".
KStream<String, Long> doubled = partitioned[0].map(
    (key, value) -> KeyValue.pair(
        // Must deserialize a second time unfortunately.
        stringSerde.deserializer().deserialize(inputTopic, key),
        2 * longSerde.deserializer().deserialize(inputTopic, value)));

// Don't forget to actually write the dead letter queue back to Kafka!
partitioned[1].to(Serdes.ByteArray(), Serdes.ByteArray(), "quarantine-topic");

オプション3:filterで破損したレコードをスキップする

これは完全を期すためだけに言及しています。このオプションは、オプション1と2を組み合わせたように見えますが、どちらよりも悪いです。オプション1と比較して、有効なレコードに対して手動の逆シリアル化コストを2回支払う必要があります(悪い!)。オプション2と比較すると、破損したレコードをデッドレターキューに保持する機能が失われます。

KStream<byte[], byte[]> validRecordsOnly = input.filter(
    (k, v) -> {
      boolean isValidRecord = false;
      try {
        bytesSerde.deserializer().deserialize(inputTopic, k);
        longSerde.deserializer().deserialize(inputTopic, v);
        isValidRecord = true;
      }
      catch (SerializationException e) {
        // log + ignore/skip the corrupted message
        System.err.println("Could not deserialize record: " + e.getMessage());
      }
      return isValidRecord;
    }
);
KStream<String, Long> doubled = validRecordsOnly.map(
    (key, value) -> KeyValue.pair(
        // Must deserialize a second time unfortunately.
        stringSerde.deserializer().deserialize(inputTopic, key),
        2 * longSerde.deserializer().deserialize(inputTopic, value)));

どんな助けも大歓迎です。

私が助けてくれることを願っています。はいの場合、Kafka Streams APIを改善して、今日よりも優れた/より便利な方法で障害/例外を処理する方法についてのフィードバックをお待ちしています。:-)

23
Michael G. Noll