web-dev-qa-db-ja.com

Kafka for spring-kafkaのデッドレターキュー(DLQ)

Spring-kafka 2.1.xを使用してSpring Boot 2.0アプリケーションでDead Letter Queue(DLQ)の概念を実装する最良の方法は何ですか@ KafkaListenerいくつかの事前定義済みのKafka DLQトピックに送信され、単一のメッセージを失わないようにするいくつかのBeanのメソッド?

したがって、消費Kafkaレコードは次のいずれかです。

  1. 正常に処理されました
  2. 処理に失敗し、DLQトピックに送信されました、
  3. 処理に失敗しました(予期しない問題が原因で)DLQトピックに送信されないため、リスナーによって再び消費されます。

ErrorHandlerのカスタム実装でリスナーコンテナーを作成しようとしましたが、レコードの送信がKafkaTemplateを使用してDLQトピックに処理されませんでした。無効な自動コミットと[〜#〜] record [〜#〜] AckModeを使用します。

spring.kafka.enable-auto-ack=false
spring.kafka.listener.ack-mode=RECORD

@Configuration
public class KafkaConfig {
    @Bean
    ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<Integer, String> factory = ...
        ...
        factory.getContainerProperties().setErrorHandler(dlqErrorHandler);
        return factory;
    }
}

@Component
public class DlqErrorHandler implements ErrorHandler {

    @Autowired
    private KafkaTemplate<Object, Object> kafkaTemplate;

    @Value("${dlqTopic}")
    private String dlqTopic;

    @Override
    public void handle(Exception thrownException, ConsumerRecord<?, ?> record) {
        log.error("Error, sending to DLQ...");
        kafkaTemplate.send(dlqTopic, record.key(), record.value());
    }
}

この実装はアイテムを保証しないようです。 DlqErrorHandlerで例外がスローされる場合、レコードはリスナーによって再度消費されません。

トランザクションリスナーコンテナーの使用は役立ちますか?

factory.getContainerProperties().setTransactionManager(kafkaTransactionManager);

Spring Kafkaを使用してDLQコンセプトを実装する便利な方法はありますか?

2018年3月28日更新

Gary Russellの回答のおかげで、次のようにDlqErrorHandlerを実装することで目的の動作を実現できました

@Configuration
public class KafkaConfig {
    @Bean
    ConcurrentKafkaListenerContainerFactory<Integer, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<Integer, String> factory = ...
        ...
        factory.getContainerProperties().setAckOnError(false);
        factory.getContainerProperties().setErrorHandler(dlqErrorHandler);
        return factory;
    }
}

@Component
public class DlqErrorHandler implements ContainerAwareErrorHandler {
    ...
    @Override
    public void handle(Exception thrownException, list<ConsumerRecord<?, ?> records, Consumer<?, ?> consumer, MessageListenerContainer container) {
        Consumerrecord<?, ? record = records.get(0);
        try {
            kafkaTemplate.send("dlqTopic", record.key, record.value());
            consumer.seek(new TopicPartition(record.topic(), record.partition()), record.offset() + 1);
            // Other records may be from other partitions, so seek to current offset for other partitions too
            // ...
        } catch (Exception e) {
            consumer.seek(new TopicPartition(record.topic(), record.partition()), record.offset());
            // Other records may be from other partitions, so seek to current offset for other partitions too
            // ...
            throw new KafkaException("Seek to current after exception", thrownException);
        }
    }
}

この方法で、コンシューマーポーリングが3つのレコード(1、2、3)を返し、2番目のレコードは処理できない場合:

  • 1が処理されます
  • 2は処理されず、DLQに送信されません。
  • 3消費者がrecord.offset()+ 1を求めているおかげで、リスナーに配信されます

DLQへの送信が失敗した場合、コンシューマはrecord.offset()を探し、レコードはリスナーに再配信されます(そしてDLQへの送信はおそらく廃止されます)。

5
Evgeniy Khyst

SeekToCurrentErrorHandler を参照してください。

例外が発生すると、未処理のすべてのレコードが次のポーリングで再配信されるように、コンシューマーを探します。

同じ手法(サブクラスなど)を使用してDLQに書き込み、DLQ書き込みが失敗した場合は現在のオフセット(およびその他の未処理)をシークし、DLQ書き込みが成功した場合は残りのレコードのみをシークできます。

3
Gary Russell