【Spring连载】使用Spring访问 Apache Kafka(十七)----处理异常

【Spring连载】使用Spring访问 Apache Kafka(十七)----处理异常Handling Exceptions

  • 一、监听器错误处理程序Listener Error Handlers
  • 二、容器错误处理程序Container Error Handlers
  • 三、回退处理程序Back Off Handlers
  • 四、默认错误处理程序DefaultErrorHandler
  • 五、使用batch错误处理程序的转换错误Conversion Errors with Batch Error Handlers
  • 六、重试完整批Retrying Complete Batches
  • 七、容器停止错误处理程序Container Stopping Error Handlers
  • 八、委托错误处理程序Delegating Error Handler
  • 九、日志错误处理程序Logging Error Handler
  • 十、为record和batch监听器使用不同的常见错误处理程序Using Different Common Error Handlers for Record and Batch Listeners
  • 十一、常见错误处理程序摘要Common Error Handler Summary
  • 十二、遗留错误处理程序及其替代Legacy Error Handlers and Their Replacements
    • 12.1 将自定义遗留错误处理程序实现迁移到CommonErrorHandler Migrating Custom Legacy Error Handler Implementations to CommonErrorHandler
  • 十三、rollback之后处理器After-rollback Processor
  • 十四、投递尝试头Delivery Attempts Header
  • 十五、监听信息头Listener Info Header
  • 十六、发布死信记录Publishing Dead-letter Records
  • 十七、管理死信记录头Managing Dead Letter Record Headers
  • 十八、ExponentialBackOffWithMaxRetries Implementation

本文描述如何处理在使用Spring for Apache Kafka时可能出现的各种异常。

一、监听器错误处理程序Listener Error Handlers

@KafkaListener注解有一个属性:errorHandler。
你可以使用errorHandler来提供KafkaListenerErrorHandler实现的bean名称。这个函数式接口有一个方法,如下所示:

@FunctionalInterface
public interface KafkaListenerErrorHandler {

    Object handleError(Message<?> message, ListenerExecutionFailedException exception) throws Exception;

}

你可以访问由消息转换器生成的spring-messaging Message对象和监听器引发的异常,该异常被封装在ListenerExecutionFailedException中。错误处理程序可以抛出原始异常或新异常,这些异常被抛出到容器中。错误处理程序返回的任何内容将被忽略。 你可以在MessagingMessageConverter和BatchMessagingMessageConverter上设置rawRecordHeader属性,这会将原始ConsumerRecord添加到“KafkaHeaders.RAW_DATA” header中的转换后的Message。例如,如果你希望在监听器错误处理程序中使用DeadLetterPublishingRecoverer,这将非常有用。它可能用于request/reply场景,在该场景中,你希望在一定次数的重试后,在dead letter主题中捕获失败记录后,将失败结果发送给发件人。

@Bean
KafkaListenerErrorHandler eh(DeadLetterPublishingRecoverer recoverer) {
    return (msg, ex) -> {
        if (msg.getHeaders().get(KafkaHeaders.DELIVERY_ATTEMPT, Integer.class) > 9) {
            recoverer.accept(msg.getHeaders().get(KafkaHeaders.RAW_DATA, ConsumerRecord.class), ex);
            return "FAILED";
        }
        throw ex;
    };
}

KafkaListenerErrorHandler有一个子接口(ConsumerAwareListenerErrorHandler),可以通过以下方法访问消费者对象:

Object handleError(Message<?> message, ListenerExecutionFailedException exception, Consumer<?, ?> consumer);

另一个子接口(ManualAckListenerErrorHandler)在使用手动AckMode时提供对Acknowledgment对象的访问。

Object handleError(Message<?> message, ListenerExecutionFailedException exception,
			Consumer<?, ?> consumer, @Nullable Acknowledgment ack);

在任何一种情况下,都不应该对consumer执行任何seek,因为容器不知道它们。

二、容器错误处理程序Container Error Handlers

从2.8版本开始,遗留的ErrorHandler和BatchErrorHandler接口已被新的CommonErrorHandler所取代。这些错误处理程序可以处理record 和batch监听器的错误,允许单个监听器容器工厂为这两种类型的监听器创建容器。现在,可以使用CommonErrorHandler的实现来取代大多数遗留框架错误处理程序。遗留接口仍然由监听器容器和监听器容器工厂支持;它们将在将来的版本中被弃用。
有关将自定义错误处理程序迁移到CommonErrorHandler的信息,请参阅将自定义遗留错误处理程序实现迁移到CommonErrorHandler。
在使用事务时,默认情况下不配置错误处理程序,因此异常将回滚事务。事务容器的错误处理由AfterRollbackProcessor处理。如果你在使用事务时提供了自定义错误处理程序,那么它必须抛出异常才能回滚事务。
CommonErrorHandler接口有一个默认的方法isAckAfterHandle(),容器调用它来确定如果错误处理程序返回而没有抛出异常,是否应该提交偏移量;默认情况下返回true。
通常,当错误未被“处理”时(例如,在执行seek操作之后),框架提供的错误处理程序将抛出异常。默认情况下,容器会以ERROR级别记录此类异常。所有的框架错误处理程序都继承了KafkaExceptionLogLevelAware,它允许你控制这些异常的日志级别。

/**
 * Set the level at which the exception thrown by this handler is logged.
 * @param logLevel the level (default ERROR).
 */
public void setLogLevel(KafkaException.Level logLevel) {
    ...
}

你可以为容器工厂中的所有监听器指定一个全局错误处理程序。下面的例子展示了如何这样做:

@Bean
public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<Integer, String>>
        kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<Integer, String> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    ...
    factory.setCommonErrorHandler(myErrorHandler);
    ...
    return factory;
}

默认情况下,如果带注解的监听器方法抛出异常,它会被抛出到容器,消息会根据容器配置被处理。
容器在调用错误处理程序之前将提交任何pending的偏移量。
如果你使用的是Spring Boot,只需将错误处理程序添加为@Bean,Boot就会将其添加到自动配置的工厂中。

三、回退处理程序Back Off Handlers

错误处理程序(如DefaultErrorHandler)使用BackOff来确定重试传递之前等待的时间。你可以配置自定义的BackOffHandler。默认处理程序只是挂起线程,直到回退时间过去(或者容器停止)。框架还提供ContainerPausingBackOffHandler,它暂停监听器容器,直到回退时间过去,然后恢复容器。当延迟长于consumer属性max.poll.interval.ms时,这很有用。注意,实际回退时间将受到pollTimeout容器属性的影响。

四、默认错误处理程序DefaultErrorHandler

这个DefaultErrorHandler取代了SeekToCurrentErrorHandler和RecoveringBatchErrorHandler。注意,batch监听器的回退行为(当抛出BatchListenerFailedException以外的异常时)等效于重试完整批。
DefaultErrorHandler可以被配置为提供与seek未处理的记录偏移量相同的语义,但不需要实际seek。相反,记录由监听器容器保留,并在错误处理程序退出后(以及在执行单个暂停的poll()后)重新提交给监听器,以保持consumer alive;如果正在使用非阻塞重试(Non-Blocking Retries)或ContainerPausingBackOffHandler,则暂停可以扩展到多个polls)。错误处理程序向容器返回一个结果,该结果指示当前失败的记录是否可以重新提交,或者,它是否已恢复,然后它将不会被再次发送到监听器。要启用此模式,请将seekAfterError属性设置为false。
错误处理程序可以恢复(跳过)一直失败的记录。默认情况下,在十次失败后,会在ERROR级别记录失败的record。你可以使用自定义恢复器(BiConsumer)和BackOff来配置处理程序,BackOff控制delivery尝试和每次尝试之间的延迟。将FixedBackOff与FixedBackOff.UNLIMITED_ATTEMPTS一起使用会导致无限次重试。以下示例配置三次尝试后的恢复:

DefaultErrorHandler errorHandler =
    new DefaultErrorHandler((record, exception) -> {
        // recover after 3 failures, with no back off - e.g. send to a dead-letter topic
    }, new FixedBackOff(0L, 2L));

要使用此处理程序的自定义实例配置监听器容器,需要将其添加到容器工厂。例如,使用@KafkaListener容器工厂,你可以像下面这样添加DefaultErrorHandler:

@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
    ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory();
    factory.setConsumerFactory(consumerFactory());
    factory.getContainerProperties().setAckMode(AckMode.RECORD);
    factory.setCommonErrorHandler(new DefaultErrorHandler(new FixedBackOff(1000L, 2L)));
    return factory;
}

对于record监听器,它将重试最多2次delivery(共3次delivery),以1秒的时间间隔,而不是默认配置(FixedBackOff(0L, 9))。在重试次数用完后会记录失败。
作为一个例子;如果poll返回六条记录(在分区0、1、2中各有两条),并且监听器在第四条记录上抛出异常,则容器通过提交前三条的偏移量来acknowledge消息。DefaultErrorHandler为分区1 seek偏移量1,为分区2 seek偏移量0。下一个poll()返回三个未处理的记录。
如果AckMode是BATCH,则容器在调用错误处理程序之前提交前两个分区的偏移量。
对于batch监听器,它必须抛出BatchListenerFailedException,提示batch中的哪些记录失败。
事件顺序为:

  • 在索引之前提交记录的偏移量。
  • 如果重试次数未用完,执行seek,以便重新deliver所有剩余记录(包括失败的记录)。
  • 如果重试次数已用完,尝试恢复失败的记录(默认仅记录日志)并执行seek,以便重新传递剩余的记录(不包括失败的记录)。恢复记录的偏移量会被提交。
  • 如果重试次数已用完且恢复失败,则将执行seek,如同重试次数未用完一样。
    DefaultErrorHandler可以被配置为提供与seek未处理的记录偏移量相同的语义,但不需要实际seek。相反,错误处理程序会创建一个新的ConsumerRecords只包含未处理的记录,然后这些记录将被提交给监听器(在执行单个暂停的poll()之后,以保持consumer alive)。要启用此模式,请将seekAfterError属性设置为false。
    在重试次数用完后,默认的恢复器(recoverer)会记录失败的record。你可以使用自定义恢复器,也可以使用框架提供的恢复器,如DeadLetterPublishingRecoverer。
    当使用POJO batch监听器(例如List)时,并且你没有完整的consumer记录要添加到异常中,你可以只添加失败记录的索引:
@KafkaListener(id = "recovering", topics = "someTopic")
public void listen(List<Thing> things) {
    for (int i = 0; i < records.size(); i++) {
        try {
            process(things.get(i));
        }
        catch (Exception e) {
            throw new BatchListenerFailedException("Failed to process", i);
        }
    }
}

当容器配置为“AckMode.MMANUAL_IMMEDIATE”时,可以配置错误处理程序来提交已恢复记录的偏移量;将commitRecovered属性设置为true。
另请参见发布死信记录。
使用事务时,DefaultAfterRollbackProcessor也提供类似的功能。请参阅回滚后处理器。
DefaultErrorHandler认为某些异常是fatal的,并跳过对此类异常的重试;在第一次失败时调用恢复器。默认情况下,被视为fatal的异常有:

  • DeserializationException
  • MessageConversionException
  • ConversionException
  • MethodArgumentResolutionException
  • NoSuchMethodException
  • ClassCastException
    因为这些异常不太可能在重试delivery时得到解决。
    你可以将更多异常类型添加到不可重试类别,或者完全替换已分类异常的映射。有关更多信息,请参阅DefaultErrorHandler.addNotRetryableException()和DefaultErrorHandler.setClassifications()的Javadocs,以及spring-retry BinaryExceptionClassifier的那些文档。
    以下是一个将IllegalArgumentException添加到不可重试异常中的示例:
@Bean
public DefaultErrorHandler errorHandler(ConsumerRecordRecoverer recoverer) {
    DefaultErrorHandler handler = new DefaultErrorHandler(recoverer);
    handler.addNotRetryableExceptions(IllegalArgumentException.class);
    return handler;
}

错误处理程序可以配置一个或多个RetryListener,接收重试和恢复进度的通知。

@FunctionalInterface
public interface RetryListener {

    void failedDelivery(ConsumerRecord<?, ?> record, Exception ex, int deliveryAttempt);

    default void recovered(ConsumerRecord<?, ?> record, Exception ex) {
    }

    default void recoveryFailed(ConsumerRecord<?, ?> record, Exception original, Exception failure) {
    }

	default void failedDelivery(ConsumerRecords<?, ?> records, Exception ex, int deliveryAttempt) {
	}

	default void recovered(ConsumerRecords<?, ?> records, Exception ex) {
	}

	default void recoveryFailed(ConsumerRecords<?, ?> records, Exception original, Exception failure) {
	}

}

如果恢复器失败(引发异常),则失败的记录将包含在seek中。如果恢复器失败,默认情况下将重置BackOff,并且在再次尝试恢复之前,重新deliver将再次执行。若要在恢复失败后跳过重试,请将错误处理程序的resetStateOnRecoveryFailure设置为false。
你可以为错误处理程序提供BiFunction, Exception, BackOff>,根据失败的记录或异常来确定要使用的BackOff:

handler.setBackOffFunction((record, ex) -> { ... });

如果函数返回null,则将使用处理程序的默认BackOff。
将resetStateOnExceptionChange设置为true,如果异常类型在每次失败中发生变化,则重试序列将重新启动(包括选择新的BackOff,如果已配置)。如果为false,则不考虑异常类型。
另请参阅投递尝试头Delivery Attempts Header。

五、使用batch错误处理程序的转换错误Conversion Errors with Batch Error Handlers

将MessageConverter与ByteArrayDeserializer、BytesDeserializer或StringDeserializer以及DefaultErrorHandler一起使用时,batch监听器现在可以正确处理转换错误。当发生转换错误时,payload被设置为null,并且反序列化异常被添加到record header中,类似于ErrorHandlingDeserializer。监听器中有ConversionException的列表,因此监听器可以抛出BatchListenerFailedException,指出发生转换异常的第一个索引。

@KafkaListener(id = "test", topics = "topic")
void listen(List<Thing> in, @Header(KafkaHeaders.CONVERSION_FAILURES) List<ConversionException> exceptions) {
    for (int i = 0; i < in.size(); i++) {
        Foo foo = in.get(i);
        if (foo == null && exceptions.get(i) != null) {
            throw new BatchListenerFailedException("Conversion error", exceptions.get(i), i);
        }
        process(foo);
    }
}

六、重试完整批Retrying Complete Batches

Retrying Complete Batches是batch监听器的DefaultErrorHandler的fallback行为,其中监听器抛出了除BatchListenerFailedException以外的异常。
在重新deliver batch时,无法保证该批次具有相同数量的记录和重新deliver的记录的顺序相同。因此,不可能轻松地维护batch的重试状态。FallbackBatchErrorHandler采用以下方法。如果batch 监听器抛出的异常不是BatchListenerFailedException,则会从内存中的batch记录中执行重试。为了避免在扩展的重试序列中发生rebalance,错误处理程序暂停consumer,在每次重试时,在睡眠前对其进行poll以获取back off,然后再次调用监听器。当重试次数用完时,会为batch中的每个记录调用ConsumerRecordRecoverer。如果恢复器抛出异常,或者线程在睡眠期间中断,那么该批记录将在下一次poll时重新deliver。在退出之前,无论结果如何,都会恢复consumer。
此机制不能用于事务。
在BackOff的间隔等待时,错误处理程序将以短暂的睡眠循环,直到达到所需的延迟,同时检查容器是否已停止,从而允许睡眠在stop()之后很快退出,而不导致延迟。

七、容器停止错误处理程序Container Stopping Error Handlers

如果监听器抛出异常,CommonContainerStoppingErrorHandler会停止容器。对于record 监听器,当AckMode为RECORD时,将提交已处理记录的偏移量。对于record 监听器,当AckMode为任何手动值时,将提交已acknowledge记录的偏移量。对于batch监听器,当AckMode为BATCH时,在容器重新启动时会replay整个batch。
容器停止后,将引发一个包装了ListenerExecutionFailedException的异常。这是为了使事务回滚(如果启用了事务)。

八、委托错误处理程序Delegating Error Handler

CommonDelegatingErrorHandler可以根据异常类型委托给不同的错误处理程序。例如,你可能希望为大多数异常调用DefaultErrorHandler,然后为其他异常调用CommonContainerStoppingErrorHandler。

九、日志错误处理程序Logging Error Handler

CommonLoggingErrorHandler只是在日志记录异常;当使用record监听器,先前轮询的剩余记录会被传递给监听器。对于batch监听器,batch中的所有records将被记录。

十、为record和batch监听器使用不同的常见错误处理程序Using Different Common Error Handlers for Record and Batch Listeners

如果希望对record和batch监听器使用不同的错误处理策略,框架提供了CommonMixedErrorHandler,允许为每种监听器类型配置特定的错误处理程序。

十一、常见错误处理程序摘要Common Error Handler Summary

  • DefaultErrorHandler
  • CommonContainerStoppingErrorHandler
  • CommonDelegatingErrorHandler
  • CommonLoggingErrorHandler
  • CommonMixedErrorHandler

十二、遗留错误处理程序及其替代Legacy Error Handlers and Their Replacements

遗留错误处理程序 替代
LoggingErrorHandler CommonLoggingErrorHandler
BatchLoggingErrorHandler CommonLoggingErrorHandler
ConditionalDelegatingErrorHandler DelegatingErrorHandler
ConditionalDelegatingBatchErrorHandler DelegatingErrorHandler
ContainerStoppingErrorHandler CommonContainerStoppingErrorHandler
ContainerStoppingBatchErrorHandler CommonContainerStoppingErrorHandler
SeekToCurrentErrorHandler DefaultErrorHandler
SeekToCurrentBatchErrorHandler 没有替换,使用带有无限BackOff的DefaultErrorHandler。
RecoveringBatchErrorHandler DefaultErrorHandler
RetryingBatchErrorHandler 没有替换,使用DefaultErrorHandler并抛出BatchListenerFailedException以外的异常。

12.1 将自定义遗留错误处理程序实现迁移到CommonErrorHandler Migrating Custom Legacy Error Handler Implementations to CommonErrorHandler

请参阅CommonErrorHandler中的javadocs。
要替换ErrorHandler或ConsumerAwareErrorHandler实现,你应该实现handleOne(),并让seeksAfterHandle()返回false(默认值)。你还应该实现handleOtherException(),以处理发生在记录处理范围之外的异常(例如consumer错误)。
要替换RemainingRecordsErrorHandler实现,应实现handleRemaining()并重写seeksAfterHandle()以返回true(错误处理程序必须执行必要的seek)。你还应该实现handleOtherException(),以处理发生在记录处理范围之外的异常(例如consumer错误)。
要替换任何BatchErrorHandler实现,你应该实现handleBatch()。你还应该实现handleOtherException(),以处理发生在记录处理范围之外的异常(例如consumer错误)。

十三、rollback之后处理器After-rollback Processor

在使用事务时,如果监听器抛出异常(如果存在错误处理程序,则抛出异常),则事务将回滚。默认情况下,任何未处理的记录(包括失败的记录)都会在下一次轮询中重新提取。这是通过在DefaultAfterRollbackProcessor中执行seek操作来实现的。使用batch监听器,将重新处理整批记录(容器不知道batch中的哪条记录失败)。要修改此行为,可以使用自定义的AfterRollbackProcessor配置监听器容器。例如,对于record-based的监听器,你可能希望跟踪失败的记录,并在多次尝试后放弃,也许可以将其发布到一个死信(dead-letter)主题。
DefaultAfterRollbackProcessor现在可以恢复(跳过)不断失败的记录。默认情况下,在十次失败后,会记录失败的记录(ERROR 级别)。你可以使用自定义恢复程序(BiConsumer)和最大失败数来配置processor。将maxFailures属性设置为负数会导致无限次重试。以下示例配置三次尝试后的恢复:

AfterRollbackProcessor<String, String> processor =
    new DefaultAfterRollbackProcessor((record, exception) -> {
        // recover after 3 failures, with no back off - e.g. send to a dead-letter topic
    }, new FixedBackOff(0L, 2L));

当你不使用事务时,你可以通过配置DefaultErrorHandler来实现类似的功能。请参阅容器错误处理程序。
使用batch监听器无法进行恢复(Recovery ),因为框架不知道batch中的哪条记录一直在失败。在这种情况下,应用程序监听器必须处理不断失败的记录。
另请参见发布死信记录。
DefaultAfterRollbackProcessor可以在新事务中调用(在失败事务回滚后启动)。然后,如果你使用DeadLetterPublishingRecoverer发布失败的记录,则processor会将恢复的记录在原始主题/分区中的偏移量发送到事务。要启用此功能,请在DefaultAfterRollbackProcessor上设置commitRecovered和kafkaTemplate属性。
如果恢复器失败(引发异常),则失败的记录将包含在seek中。如果恢复器失败,默认情况下将重置BackOff,并且在再次尝试恢复之前,redeliveries将再次经历back offs。在早期版本中,BackOff并未重置,在下一次失败时会重新尝试恢复。要恢复到以前的行为,请将processor的resetStateOnRecoveryFailure属性设置为false。
你现在可以为processor提供BiFunction, Exception, BackOff>,根据失败的记录或异常来确定要使用的BackOff:

handler.setBackOffFunction((record, ex) -> { ... }

如果函数返回null,则将使用processor的默认BackOff。
将resetStateOnExceptionChange设置为true,如果异常类型在各失败之间发生变化,则重试序列将重新启动(包括选择新的BackOff,如果配置了此选项)。默认情况下,不考虑异常类型。

从2.3.1版本开始,类似于DefaultErrorHandler,DefaultAfterRollbackProcessor认为某些异常是fatal的,并跳过对此类异常的重试;在第一次失败时调用恢复器。默认情况下,被视为fatal的异常有:

  • DeserializationException
  • MessageConversionException
  • ConversionException
  • MethodArgumentResolutionException
  • NoSuchMethodException
  • ClassCastException

因为这些异常不太可能在重试delivery时得到解决。
你可以将更多异常类型添加到不可重试类别,或者完全替换已分类异常的映射。有关更多信息,请参阅DefaultAfterRollbackProcessor.setClassifications()的Javadocs,以及spring-retry BinaryExceptionClassifier的Javadocs。
以下是一个将IllegalArgumentException添加到不可重试异常中的示例:

@Bean
public DefaultAfterRollbackProcessor errorHandler(BiConsumer<ConsumerRecord<?, ?>, Exception> recoverer) {
    DefaultAfterRollbackProcessor processor = new DefaultAfterRollbackProcessor(recoverer);
    processor.addNotRetryableException(IllegalArgumentException.class);
    return processor;
}

另请参阅投递尝试头。
对于当前kafka-clients,容器无法检测ProducerFencedException是由再平衡(rebalance)引起的,还是生产者的transactional.id因超时或过期而被吊销。因为在大多数情况下,它是由再平衡引起的,所以容器不会调用AfterRollbackProcessor(因为我们不再被分配分区,所以不适合seek分区)。如果你确保超时足够大,可以处理每个事务,并定期执行“空”事务(例如通过ListenerContainerIdleEvent),则可以避免由于超时和过期而设置围栏(fencing)。或者,你可以将stopContainerWhenFenced容器属性设置为true,容器将停止,从而避免记录丢失。你可以使用ConsumerStoppedEvent并检查Reason的属性FENCED来检测此情况。由于该事件还引用了容器,因此可以使用此事件重新启动容器。
在等待BackOff间隔时,错误处理程序将以短暂的睡眠循环,直到达到所需的延迟,同时检查容器是否已停止,从而允许睡眠在stop()之后很快退出,而不是导致延迟。
processor可以配置一个或多个RetryListener,接收重试和恢复进度的通知。

@FunctionalInterface
public interface RetryListener {

    void failedDelivery(ConsumerRecord<?, ?> record, Exception ex, int deliveryAttempt);

    default void recovered(ConsumerRecord<?, ?> record, Exception ex) {
    }

    default void recoveryFailed(ConsumerRecord<?, ?> record, Exception original, Exception failure) {
    }

}

十四、投递尝试头Delivery Attempts Header

以下内容仅适用于record监听器,不适用于batch监听器。
当使用实现DeliveryAttemptAware的ErrorHandler或AfterRollbackProcessor时,可以启用向记录添加“KafkaHeaders.DELIVERY_ATTEMPT” 的header(kafka_deliveryAttempt)。此header的值是一个从1开始的递增整数。收到原始ConsumerRecord时整数在byte[4]中。

int delivery = ByteBuffer.wrap(record.headers()
    .lastHeader(KafkaHeaders.DELIVERY_ATTEMPT).value())
    .getInt()

当将@KafkaListener与DefaultKafkaHeaderMapper或SimpleKafkaHeaderMapper一起使用时,可以通过在listener方法中添加@Header(KafkaHeads.DELIVERY_ATTEMPT)int DELIVERY作为参数来获得。
若要启用此header的填充,请将容器属性deliveryAttemptHeader设置为true。默认情况下,它是禁用的,以避免查找每条记录的状态和添加header的开销。
DefaultErrorHandler和DefaultAfterRollbackProcessor支持此功能。

十五、监听信息头Listener Info Header

在某些情况下,能够知道监听器在哪个容器中运行是很有用的。
你现在可以在监听器容器上设置listenerInfo属性,或者在@KafkaListener注解上设置info属性。然后,容器会将其添加到所有传入消息的“KafkaListener.LISTENER_INFO”头中;然后,它可以用于record拦截器、过滤器等,也可以用于监听器本身。

@KafkaListener(id = "something", topic = "topic", filter = "someFilter",
        info = "this is the something listener")
public void listen2(@Payload Thing thing,
        @Header(KafkaHeaders.LISTENER_INFO) String listenerInfo) {
...

在RecordInterceptor或RecordFilterStrategy实现中使用时,header在consumer 记录中作为字节数组,使用KafkaListenerAnnotationBeanPostProcessor的charSet属性进行转换。
当从consumer记录创建MessageHeaders时,header映射器也会转换为String,并且从不将此header映射到出站(outbound)记录上。
对于POJO batch监听器,header被复制到batch的每个成员中,并且在转换后也可以作为单个String参数使用。

@KafkaListener(id = "list2", topics = "someTopic", containerFactory = "batchFactory",
        info = "info for batch")
public void listen(List<Thing> list,
        @Header(KafkaHeaders.RECEIVED_KEY) List<Integer> keys,
        @Header(KafkaHeaders.RECEIVED_PARTITION) List<Integer> partitions,
        @Header(KafkaHeaders.RECEIVED_TOPIC) List<String> topics,
        @Header(KafkaHeaders.OFFSET) List<Long> offsets,
        @Header(KafkaHeaders.LISTENER_INFO) String info) {
            ...
}

如果batch监听器有一个过滤器,并且该过滤器导致一个空batch,则需要将required=false添加到@Header参数中,因为该info对于空batch不可用。
如果你收到List,则信息在每个Message的“KafkaHeaders.LISTENER_info”header中。
有关消费batch的详细信息,请参见batch监听器。

十六、发布死信记录Publishing Dead-letter Records

当达到记录的最大失败次数时,可以使用记录恢复器配置DefaultErrorHandler和DefaultAfterRollbackProcessor。框架提供了DeadLetterPublishingRecoverer,它将失败的消息发布到另一个topic。recoverer需要一个KafkaTemplate,用于发送记录。你也可以选择使用“BiFunction, Exception, TopicPartition>”来配置它,该函数用于解析目标主题和分区。
默认情况下,死信记录被发送到名为“.DLT”的主题(原始主题名加.DLT后缀)以及与原始记录相同的分区。因此,当您使用默认的解析器时,死信主题的分区数必须至少与原始主题的分区数量相同。

如果返回的TopicPartition有一个负分区,那么ProducerRecord中没有设置该分区,所以该分区是由Kafka选择的。任何ListenerExecutionFailedException(例如,当在@KafkaListener方法中检测到异常时抛出)都会配置groupId属性。这允许目标解析程序除了使用ConsumerRecord中的信息来选择死信主题之外,还可以使用此信息。
以下示例显示了如何注入自定义目标解析程序:

DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template,
        (r, e) -> {
            if (e instanceof FooException) {
                return new TopicPartition(r.topic() + ".Foo.failures", r.partition());
            }
            else {
                return new TopicPartition(r.topic() + ".other.failures", r.partition());
            }
        });
CommonErrorHandler errorHandler = new DefaultErrorHandler(recoverer, new FixedBackOff(0L, 2L));

发送到死信主题的记录有以下headers:

  • KafkaHeaders.DLT_EXCEPTION_FQCN:Exception类名(通常是ListenerExecutionFailedException,但也可以是其他类)。
  • KafkaHeaders.DLT_EXCEPTION_CAUSE_FQCN:异常原因类名(如果存在)。
  • KafkaHeaders.DLT_EXCEPTION_STACKTRACE:异常堆栈跟踪。
  • KafkaHeaders.DLT_EXCEPTION_MESSAGE:异常消息。
  • KafkaHeaders.DLT_KEY_EXCEPTION_FQCN:异常类名(仅限key反序列化错误)。
  • KafkaHeaders.DLT_KEY_EXCEPTION_STACKTRACE:异常堆栈跟踪(仅限key反序列化错误)。
  • KafkaHeaders.DLT_KEY_EXCEPTION_MESSAGE:异常消息(仅限key反序列化错误)。
  • KafkaHeaders.DLT_ORIGINAL_TOPIC:原始主题。
  • KafkaHeaders.DLT_ORIGINAL_PARTITION:原始分区。
  • KafkaHeaders。DLT_ORIGINAL_OFFSET:原始偏移量。
  • KafkaHeaders.DLT_ORIGINAL_TIMESTAMP:原始时间戳。
  • KafkaHeaders.DLT_ORIGINAL_TIMESTAMP_TYPE:原始时间戳类型。
  • KafkaHeaders.DLT_ORIGINAL_CONSUMER_GROUP:未能处理记录的原始consumer组。
    Key异常仅由反序列化异常引起,因此不存在DLT_KEY_EXCEPTION_CAUSE_FQCN。
    有两种机制可以添加更多的header。
  1. 创建recoverer子类并覆盖createProducerRecord()-调用super.createProducerRecord()并添加更多header。
  2. 提供一个BiFunction来接收消费者记录和异常,返回一个Headers对象;来自那里的headers将被复制到最终producer记录中;另请参阅管理死信记录头。使用setHeadersFunction()设置BiFunction。
    第二个实现起来更简单,但第一个有更多的可用信息,包括已经组装好的标准headers。
    当与ErrorHandlingDeserializer一起使用时,发布者将把死信生产者记录中的value()恢复为未能反序列化的原始值。以前,value()为null,用户代码必须解码消息头中的DeserializationException。此外,您还可以向发布者提供多个KafkaTemplate;例如,如果你希望从反序列化异常中发布byte[],以及发布反序列化成功的value,它使用了不同序列化程序,则可能需要这样做。下面是一个使用String和byte[]序列化程序的KafkaTemplate配置发布服务器的示例:
@Bean
public DeadLetterPublishingRecoverer publisher(KafkaTemplate<?, ?> stringTemplate,
        KafkaTemplate<?, ?> bytesTemplate) {

    Map<Class<?>, KafkaTemplate<?, ?>> templates = new LinkedHashMap<>();
    templates.put(String.class, stringTemplate);
    templates.put(byte[].class, bytesTemplate);
    return new DeadLetterPublishingRecoverer(templates);
}

发布者使用map的key来定位适合即将发布的value()的template。建议使用LinkedHashMap,以便按顺序检查key。
当发布null值时,并且有多个template时,恢复器将为Void类寻找一个template;如果不存在,将使用values().iterator()中的第一个template。
你可以使用setFailIfSendResultIsError方法,以便在消息发布失败时抛出异常。你还可以使用setWaitForSendResultTimeout设置验证发送方成功的超时时间。
如果恢复器失败(引发异常),则失败的记录将包含在seek中。如果恢复器失败,默认情况下将重置BackOff,并且在再次尝试恢复之前,重新deliver将再次经历back off。在早期版本中,BackOff未重置,并在下一次失败时重新尝试恢复。若要恢复到以前的行为,请将错误处理程序的resetStateOnRecoveryFailure属性设置为false。
将resetStateOnExceptionChange设置为true,如果异常类型在各次失败之间发生变化,则重试序列将重新启动(包括选择新的BackOff,如果配置了此选项)。默认情况下,不考虑异常类型。
恢复器也可以与Kafka Streams一起使用-有关更多信息,请参阅从反序列化异常中恢复(Recovery from Deserialization Exceptions)。
ErrorHandlingDeserializer在header “ErrorHandlingDeserializer.VALUE_DESERIALIZER_EXCEPTION_HEADER”和“ErrorHandlingDeserializer.KEY_DESERIALIZER_EXCEPTION_HEADER”中添加反序列化异常(使用java 序列化)。默认情况下,这些header不会保留在发布到死信主题的消息中。如果键和值都未能反序列化,则在发送给DLT的记录中填充这两者的原始值。
如果传入的记录相互依赖,但可能会无序到达,则将失败的记录重新发布到原始主题的尾部(多次)可能会很有用,而不是直接发送到死信主题。有关示例,请参阅此stackoverflow问题。
以下错误处理程序配置将完全做到这一点:

@Bean
public ErrorHandler eh(KafkaOperations<String, String> template) {
    return new DefaultErrorHandler(new DeadLetterPublishingRecoverer(template,
            (rec, ex) -> {
                org.apache.kafka.common.header.Header retries = rec.headers().lastHeader("retries");
                if (retries == null) {
                    retries = new RecordHeader("retries", new byte[] { 1 });
                    rec.headers().add(retries);
                }
                else {
                    retries.value()[0]++;
                }
                return retries.value()[0] > 5
                        ? new TopicPartition("topic.DLT", rec.partition())
                        : new TopicPartition("topic", rec.partition());
            }), new FixedBackOff(0L, 0L));
}

恢复器检查目标解析器选择的分区是否确实存在。如果分区不存在,ProducerRecord中的分区将设置为null,从而允许KafkaProducer选择分区。你可以通过将verifyPartition属性设置为false来禁用此检查。

十七、管理死信记录头Managing Dead Letter Record Headers

参考上面的发布死信记录,DeadLetterPublishingRecoverer有两个属性,用于在header已经存在时(例如在重新处理失败的死信记录时,包括在使用非阻塞重试时)管理header。

  • appendOriginalHeaders(默认为true)
  • stripPreviousExceptionHeaders(默认为true)
    Apache Kafka支持多个具有相同名称的header;要获取“最新”值,可以使用headers.lastHeader(headerName);要在多个header上获取迭代器,请使用headers.headers(headerName).iterator()。
    当重复重新发布失败的记录时,这些headers可能会增长(并最终导致发布因RecordTooLargeException而失败);对于异常header和堆栈跟踪header尤其如此。
    使用这两个属性的原因是,虽然你可能希望只保留最后一个异常信息,但你可能希望保留每次失败时记录通过了哪个主题的历史记录。
    appendOriginalHeaders应用于所有名为ORIGINAL的header,而stripPreviousExceptionHeaders应用至所有名为EXCEPTION的header。
    你现在可以控制将哪些标准header添加到输出记录中。有关默认添加的(当前)10个标准header的通用名称,请参阅enum HeadersToAdd。这些不是实际header名称,只是一个抽象;实际header名称是由子类可以覆盖的getHeaderNames()方法设置的。
    若要排除header,请使用excludeHeaders()方法;例如,要禁止在header中添加异常堆栈跟踪,请使用:
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template);
recoverer.excludeHeaders(HeaderNames.HeadersToAdd.EX_STACKTRACE);

此外,您还可以通过添加ExceptionHeadersCreator来完全自定义异常标头的添加;这也会禁用所有标准异常标头。

DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template);
recoverer.setExceptionHeadersCreator((kafkaHeaders, exception, isKey, headerNames) -> {
    kafkaHeaders.add(new RecordHeader(..., ...));
});

你现在可以通过addHeadersFunction方法提供多个header函数。这允许应用其他function,即使已经注册了另一个function,例如在使用非阻塞重试时。
另请参阅非阻塞重试的失败报头管理。

十八、ExponentialBackOffWithMaxRetries Implementation

Spring Framework提供了许多BackOff实现。默认情况下,ExponentialBackOff将无限期重试;要在一定次数的重试尝试后放弃,需要计算maxElapsedTime。Spring for Apache Kafka提供了ExponentialBackOffWithMaxRetries,这是一个接收maxRetries属性并自动计算maxElapsedTime的子类,这稍微方便一些。

@Bean
DefaultErrorHandler handler() {
    ExponentialBackOffWithMaxRetries bo = new ExponentialBackOffWithMaxRetries(6);
    bo.setInitialInterval(1_000L);
    bo.setMultiplier(2.0);
    bo.setMaxInterval(10_000L);
    return new DefaultErrorHandler(myRecoverer, bo);
}

在调用恢复程序之前,将在1、2、4、8、10、10秒后重试。

你可能感兴趣的:(spring,kafka,java)