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



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

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

我想知道处理"坏消息"的最佳方法。这可能是我无法正确反序列化的消息之类的事情,或者处理/过滤逻辑以某种意外的方式失败(我没有外部依赖项,因此应该没有此类暂时性错误)。

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

  • 这种方法是否被视为最佳实践?
  • 有没有方便的卡夫卡流方法来解决这个问题?我不认为有DLQ的概念...
  • 阻止卡夫卡干扰"坏消息"的替代方法是什么?
  • 有哪些替代的错误处理方法?

为了完整起见,这是我的代码(伪代码):

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

任何帮助非常感谢。

目前,Kafka Streams 仅提供有限的错误处理功能。目前正在努力简化这一点。就目前而言,您的整体方法似乎是一个不错的方法。

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

使用当前的方法,您"仅"验证给定字符串是否表示有效的文档 - 但情况可能是消息本身已损坏,并且无法首先在源运算符中转换为String。因此,您实际上并没有用代码涵盖反序列化异常。但是,如果您确定永远不会发生反序列化异常,则此方法也足够了。

更新

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

更新 2

使用 KIP-210(将成为 Kafka 1.1 的一部分),还可以处理生产者端的错误,类似于消费者部分,通过可以返回CONTINUE的配置default.production.exception.handler注册ProductionExceptionHandler

2018年3月23日更新: Kafka 1.0通过KIP-161为不良错误消息("毒丸")的处理提供了比我下面描述的更好,更容易的处理。 请参阅 Kafka 1.0 文档中的 default.deserialization.exception.handler。

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

好的,我在这里的答案侧重于(反)序列化问题,因为对于大多数用户来说,这可能是最棘手的情况。

[...]或者处理/过滤逻辑可能以某种意想不到的方式失败(我没有外部依赖项,所以应该没有此类暂时性错误)。

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

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

  • 这种方法是否被视为最佳实践?

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

  • 有没有方便的卡夫卡流方法来解决这个问题?我不认为有DLQ的概念...

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

  • 阻止卡夫卡干扰"坏消息"的替代方法是什么?
  • 有哪些替代的错误处理方法?

请参阅下面我的示例。

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,因为它允许您为每个输入记录输出零条、一条或多条输出记录。 在记录损坏的情况下,我们不输出任何内容(零记录),从而忽略/跳过损坏的记录。
  • 与此处列出的其他方法相比,此方法的好处是:我们只需要手动反序列化记录一次!
  • 这种方法的缺点:flatMap"标记"输入流以进行潜在的数据重新分区,即如果您执行基于键的操作,例如分组(groupBy/groupByKey)或之后的联接,您的数据将在后台重新分区。 由于这可能是一个代价高昂的步骤,我们不希望这种情况不必要地发生。 如果您知道记录键始终有效,或者您不需要对键进行操作(因此将它们保留为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 通过从"主"输入流中过滤出并将它们写入隔离主题(想想:死信队列)来保留损坏的邮件。 缺点是,对于有效的记录,我们必须支付两次手动反序列化成本。

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 以比现在更好/更方便的方式处理故障/异常的反馈。:-)

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

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 值类的逻辑;branch()的诀窍是返回KStream<keyClass, ?>[],以便进一步允许将单个数组项投射到适当的类。

如果要将异常(自定义异常)发送到另一个主题 (ERROR_TOPIC_NAME):

@Bean
public KStream<String, ?> kafkaStreamInput(StreamsBuilder kStreamBuilder) {
KStream<String, InputModel> input = kStreamBuilder.stream(INPUT_TOPIC_NAME);
return service.messageHandler(input);
}
public KStream<String, ?> messageHandler(KStream<String, InputModel> inputTopic) {
KStream<String, Object> output;
output = inputTopic.mapValues(v -> {
try {
//return InputModel
return normalMethod(v);
} catch (Exception e) {
//return ErrorModel
return errorHandler(e);
}
});

output.filter((k, v) -> (v instanceof ErrorModel)).to(KafkaStreamsConfig.ERROR_TOPIC_NAME);
output.filter((k, v) -> (v instanceof InputModel)).to(KafkaStreamsConfig.OUTPUT_TOPIC_NAME);
return output;
}

如果要处理 Kafka 异常并跳过它:

@Autowired
public ConsumerErrorHandler(
KafkaProducer<String, ErrorModel> dlqProducer) {
this.dlqProducer = dlqProducer;
}
@Bean
ConcurrentKafkaListenerContainerFactory<?, ?> kafkaListenerContainerFactory(
ConcurrentKafkaListenerContainerFactoryConfigurer configurer,
ObjectProvider<ConsumerFactory<Object, Object>> kafkaConsumerFactory) {
ConcurrentKafkaListenerContainerFactory<Object, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
configurer.configure(factory, kafkaConsumerFactory.getIfAvailable());
factory.setErrorHandler(((exception, data) -> {
ErrorModel errorModel = ErrorModel.builder().message()
.status("500").build();
assert data != null;
dlqProducer.send(new ProducerRecord<>(DLQ_TOPIC, data.key().toString(), errorModel));
}));
return factory;
}

上述所有答案虽然有效且有用,但它们假设您的流拓扑是无状态的。例如回到原始示例,

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

"我在映射器/过滤器中的处理"应该是无状态的。 即不重新分区(也称为写入持久重新分区主题)或执行toTable()(也称为写入更改日志主题)。如果处理在拓扑中进一步失败,并且您提交事务(通过遵循上面提到的 3 个选项中的任何一个 - 平面图、分支或过滤器 - 那么您必须满足手动或编程方式最终删除该不一致状态的需求。这意味着为自动执行此操作编写额外的自定义代码。

我个人希望 Streams 也能为任何未经处理的运行时异常提供LogAndSkip选项,而不仅仅是反序列化和生产异常。

有人对此有任何想法吗?

我不相信这些例子在使用Avro时根本不起作用。

当无法解析模式时(例如,有错误/非 avro 消息损坏主题),首先没有反序列化keyvalue,因为当调用 DSL.branch()代码时,异常已经被抛出(或处理)。

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

KIP-161 确实解释了如何使用处理程序,但是,将其视为拓扑的一部分要流畅得多。

相关内容

  • 没有找到相关文章

最新更新