使用Kafka的Streams API处理错误消息

时间:2017-03-08 08:49:39

标签: error-handling apache-kafka apache-kafka-streams

我有一个基本的流处理流程,看起来像

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

我想知道处理"坏消息的最佳方法"。这可能是我无法正确反序列化的消息,或者处理/过滤逻辑可能以某种意外的方式失败(我没有外部依赖,所以不应该有那种瞬态错误)。

我正在考虑将所有处理/过滤代码包装在try catch中,如果出现异常,则路由到"错误主题"。然后我可以研究该消息并对其进行修改或修改我的代码,然后将其重播为master。如果我让任何异常传播,流似乎会被卡住,并且不会再收到任何消息。

  • 这种方法被认为是最佳做法吗?
  • 有没有方便的Kafka溪流来处理这个问题?我不认为有DLQ的概念......
  • 有什么方法可以阻止卡夫卡干扰"坏消息"?
  • 有哪些替代错误处理方法?

为了完整性,这里是我的代码(伪ish):

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

非常感谢任何帮助。

4 个答案:

答案 0 :(得分:24)

目前,Kafka Streams仅提供有限的错误处理功能。正在进行的工作是为了简化这一过程。目前,您的整体方法似乎是一个很好的方法。

关于处理de /序列化错误的一条评论:手动处理这些错误,需要你进行de / serialization&#34;手动&#34;。这意味着,您需要为Streams应用程序的输入/输出主题配置ByteArraySerde s的键和值,并添加执行de / serialization的map()(即KStream<byte[],byte[]> -> map() -> KStream<keyType,valueType> - - 或者反过来,如果你还想捕获序列化异常)。否则,您无法try-catch反序列化例外。

使用您当前的方法,您只能&#34;验证给定的字符串代表一个有效的文档 - 但可能是这样,消息本身已损坏,并且首先无法在源操作符中转换为String。因此,您实际上并未使用代码覆盖反序列化异常。但是,如果您确定反序列化异常永远不会发生,那么您的方法就足够了。

<强>更新

此问题通过KIP-161解决,并将包含在下一版本1.0.0中。它允许您通过参数default.deserialization.exception.handler注册回调。每次反序列化期间发生异常时都会调用处理程序,并允许您返回DeserializationResponseCONTINUE - &gt;删除记录,或者FAIL这是默认值)

更新2

使用KIP-210(将成为Kafka 1.1的一部分),通过注册ProductionExceptionHandler via config {,它也可以处理生产者方面的错误,类似于消费者部分。 {1}}可以返回default.production.exception.handler

答案 1 :(得分:23)

更新于2018年3月23日: Kafka 1.0提供更好,更轻松的处理错误消息(&#34;毒丸&#34;) KIP-161比我在下面描述的要好。请参阅Kafka 1.0文档中的default.deserialization.exception.handler

  

这可能是我无法正确反序列化的消息[...]

好的,我的回答主要集中在(反)序列化问题上,因为对于大多数用户来说这可能是最棘手的场景。

  

[...]或者处理/过滤逻辑可能以某种意外的方式失败(我没有外部依赖,所以不应该有那种瞬态错误)。

同样的思想(用于反序列化)也可以应用于处理逻辑中的失败。在这里,大多数人倾向于选择下面的选项2(减去反序列化部分),但是YMMV。

  

我正在考虑将所有处理/过滤代码包装在try catch中,如果出现异常,则路由到&#34;错误主题&#34;。然后我可以研究该消息并对其进行修改或修改我的代码,然后将其重播为master。如果我让任何异常传播,流似乎会被卡住,并且不会再收到任何消息。

     
      
  • 这种方法被认为是最佳做法吗?
  •   

是的,目前这是要走的路。基本上,两种最常见的模式是(1)跳过损坏的邮件或(2)将损坏的记录发送到隔离主题,即死信队列。

  
      
  • 有没有方便的Kafka溪流来处理这个问题?我不认为有DLQ的概念......
  •   

是的,有办法解决这个问题,包括使用死信队列。但是,它(至少是恕我直言)还不方便。如果您对API应如何处理此问题有任何反馈 - 例如通过新的或更新的方法,配置设置(&#34;如果序列化/反序列化失败,则将有问题的记录发送到此隔离主题&#34;) - 请告诉我们。 : - )

  
      
  • 有什么方法可以阻止卡夫卡干扰&#34;坏消息&#34;?
  •   
  • 有哪些替代错误处理方法?
  •   

请参阅下面的示例。

FWIW,Kafka社区还在讨论添加一个新的CLI工具,它允许您跳过已损坏的邮件。但是,作为Kafka Streams API的用户,我认为您希望直接在代码中处理此类方案,并且仅作为最后的手段回退到CLI实用程序。

以下是Kafka Streams DSL处理损坏的记录/消息的一些模式,即#34;毒丸&#34;。这取自http://docs.confluent.io/current/streams/faq.html#handling-corrupted-records-and-deserialization-errors-poison-pill-messages

选项1:使用flatMap

跳过损坏的记录

这可以说是大多数用户想要做的事情。

  • 我们使用flatMap,因为它允许您为每个输入记录输出零个,一个或多个输出记录。在记录损坏的情况下,我们不输出任何内容(零记录),从而忽略/跳过损坏的记录。
  • 与此处列出的其他方法相比,此方法的好处是:我们只需要手动反序列化一次记录一次!
  • 这种方法的缺点:flatMap&#34;标记&#34;潜在数据重新分区的输入流,即如果您执行基于密钥的操作(如分组(groupBy / groupByKey)或之后加入,您的数据将在后台重新分区。由于这可能是一个代价高昂的步骤,我们不希望不必要地发生这种情况。如果您知道记录密钥始终有效或者您不需要对密钥进行操作(从而将它们保持为&#34;原始&#34;密钥为byte[]格式),您可以更改从flatMapflatMapValues,即使您稍后加入/分组/聚合流,也不会导致数据重新分区。

代码示例:

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通过将它们从&#34; main&#34;中过滤掉来保留损坏的消息。输入流并将它们写入隔离主题(想想:死信队列)。缺点是,对于有效记录,我们必须支付两次手动反序列化费用。

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相比,您将无法在死信队列中保留损坏的记录。

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以更好/更方便的方式处理故障/异常而感谢您的反馈。 : - )

答案 2 :(得分:1)

对于处理逻辑,您可以采用这种方法:

someKStream 

    .mapValues(inputValue -> {
        // for each execution the below "return" could provide a different class than the previous run!
        // e.g. "return isFailedProcessing ? failValue : successValue;" 
        // where failValue and successValue have no related classes
        return someObject; // someObject class vary at runtime depending on your business
    }) // here you'll have KStream<whateverKeyClass, Object> -> yes, Object for the value!

    // you could have a different logic for choosing  
    // the target topic, below is just an example
    .to((k, v, recordContext) -> v instanceof failValueClass ?
            "dead-letter-topic" : "success-topic",
            // you could completelly ignore the "Produced" part 
            // and rely on spring-boot properties only, e.g. 
            // spring.kafka.streams.properties.default.key.serde=yourKeySerde
            // spring.kafka.streams.properties.default.value.serde=org.springframework.kafka.support.serializer.JsonSerde
            Produced.with(yourKeySerde, 
                            // JsonSerde could be an instance configured as you need 
                            // (with type mappings or headers setting disabled, etc)
                            new JsonSerde<>())); 

您的课程虽然有所不同,并且落入了不同的主题,但是将按预期进行序列化。

当不使用to()而是想继续进行其他处理时,他可以使用branch()来根据kafka-value类拆分逻辑; branch()的诀窍是返回KStream<keyClass, ?>[],以便进一步允许将单个数组项转换为适当的类。

答案 3 :(得分:-1)

我认为与 Avro 一起使用时,这些示例根本不起作用。

当无法解析该模式时(例如,不良/非错误消息破坏了主题),首先无需keyvalue进行反序列化,因为当DSL .branch()代码被调用时,该异常已被抛出(或处理)。

任何人都可以确认是否确实如此吗?使用Avro时,您在这里提到的非常流畅的方法是不可能的吗?

KIP-161确实说明了如何使用处理程序,但是,将其视为拓扑的一部分会更流利。