【Spring连载】使用Spring访问 Apache Kafka(十八)----非阻塞重试Non-Blocking Retries
- 一、这种模式是如何运作的How The Pattern Works
- 二、重试等待策略延迟精度Back Off Delay Precision
- 三、配置Configuration
- 四、程序化构建Programmatic Construction
- 五、特性Features
- 六、合并阻塞和非阻塞重试Combining Blocking and Non-Blocking Retries
- 七、访问投递尝试次数Accessing Delivery Attempts
- 八、主题命名Topic Naming
- 九、多监听器,相同主题Multiple Listeners, Same Topic(s)
- 十、死信主题策略Dlt Strategies
- 十一、指定监听器容器工厂Specifying a ListenerContainerFactory
- 十二、在运行时访问主题信息Accessing Topics' Information at Runtime
- 十三、修改KafkaBackOffException日志级别Changing KafkaBackOffException Logging Level
版本2.9将机制更改为bootstrap基础设施bean;请参阅 配置,了解引导此功能所需的两种机制。
使用Kafka实现无阻塞重试/dlt功能通常需要设置额外的主题并创建和配置相应的监听器。由于2.7 Spring for Apache Kafka通过@RetryableTopic注解和RetryTopicConfiguration类提供了对它的支持,以简化bootstrap。
batch监听器不支持非阻塞重试。
一、这种模式是如何运作的How The Pattern Works
如果消息处理失败,则会将消息转发到具有back off时间戳的重试主题。重试主题consumer检查时间戳,如果不到期,则暂停该主题分区的消费。到期时,将恢复分区消费,消息再次被消费。如果消息处理再次失败,则消息将转发到下一个重试主题,并重复该模式,直到成功处理或尝试次数用完,并将消息发送到死信主题Dead Letter Topic(如果已配置)。
例如,如果你有一个"main-topic"主题,并且希望设置非阻塞重试,其指数退避(exponential backoff)为1000ms,乘数为2,最大尝试次数4次,则它将创建 main-topic-retry-1000, main-topic-retry-2000, main-topic-retry-4000 和 main-topic-dlt 主题,并配置相应的consumers。框架还负责创建主题以及设置和配置监听器。
通过使用这种策略,你将失去Kafka对该主题的排序保证。
你可以设置自己喜欢的AckMode模式,但建议使用RECORD。
目前,此功能不支持类级别的@KafkaListener注解。
当使用asyncAcks设置为true的手动AckMode时,DefaultErrorHandler必须配置为seekAfterError设置为false。从2.9.10和3.0.8版本开始,对于此类配置,这将无条件设置为true。对于早期版本,有必要重写RetryConfigurationSupport.configureCustomizers()方法以将属性设置为true。
@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
customizersConfigurer.customizeErrorHandler(eh -> eh.setSeekAfterError(false));
}
此外,在这些版本之前,无论asyncAcks属性如何,使用默认(日志记录)DLT处理程序都与任何类型的手动AckMode不兼容。
二、重试等待策略延迟精度Back Off Delay Precision
概述和保证Overview and Guarantees
所有消息处理和BackOff都由consumer线程处理,因此,在尽最大努力的基础上保证了延迟精度。如果一条消息的处理时间比该消费者的下一条消息back off时间长,则下一条信息的延迟将高于预期。此外,对于短延迟(约1秒或更短),线程必须做的维护工作,如提交偏移量,可能会延迟消息处理的执行。如果重试主题的consumer正在处理多个分区,则精度也会受到影响,因为我们依赖于从轮询中唤醒consumer,并使用完整的pollTimeouts来进行时间调整。
也就是说,对于处理单个分区的consumers来说,在大多数情况下,消息的处理应该大致在其确切的到期时间进行。
消息在到期时间之前永远不会被处理是保证的。
三、配置Configuration
从2.9版本开始,对于默认配置,@EnableKafkaRetryTopic注释应该在@Configuration注解类中使用。这使该功能能够正确地引导,并允许在运行时注入要查找的一些功能组件。
如果添加此注解,则不必同时添加@EnableKafka,因为@EnableKafkaRetryTopic是用@EnableKafka进行元注释的。
此外,从该版本开始,对于功能组件和全局功能的更高级配置,RetryTopicConfigurationSupport类应在@Configuration类中进行继承,并覆盖适当的方法。有关更多详细信息,请参阅配置全局设置和特性。
默认情况下,重试主题的容器将与主容器具有相同的并发性。从3.0版本开始,你可以为重试容器设置不同的并发性(可以在注解上,也可以在RetryConfigurationBuilder中)。
只能使用上述技术中的一种,并且只有一个@Configuration类可以扩展RetryTopicConfigurationSupport。
3.1 使用@RetryableTopic注解Using the @RetryableTopic annotation
要为@KafkaListener注解的方法配置重试主题和dlt,只需向其中添加@RetryableTopic注解,Spring for Apache Kafka将使用默认配置引导所有必要的topic和consumer。
@RetryableTopic(kafkaTemplate = "myRetryableTopicKafkaTemplate")
@KafkaListener(topics = "my-annotated-topic", groupId = "myGroupId")
public void processMessage(MyPojo message) {
// ... message processing
}
你可以在同一个类中指定一个方法来处理dlt消息,方法是对其使用@DltHandler注解。如果没有提供DltHandler方法,则会创建一个只记录消费的默认consumer。
@DltHandler
public void processMessage(MyPojo message) {
// ... message processing, persistence, etc
}
如果你没有指定kafkaTemplate名称,将查找名称为defaultRetryTopicKafkaTemplate的bean。如果没有找到bean,则抛出异常。
从3.0版本开始,@RetryableTopic注解可以用作自定义注解上的元注释;例如:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@RetryableTopic
static @interface MetaAnnotatedRetryableTopic {
@AliasFor(attribute = "concurrency", annotation = RetryableTopic.class)
String parallelism() default "3";
}
3.2 使用 RetryTopicConfiguration beans
你还可以通过在@Configuration注解类中创建RetryTopicConfiguration bean来配置非阻塞重试支持。
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, Object> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.create(template);
}
这将使用默认配置为带有“@KafkaListener”注解的方法中的所有主题创建重试主题和dlt,以及相应的consumer。消息转发需要KafkaTemplate实例。
为了对如何处理每个主题的非阻塞重试实现更细粒度的控制,可以提供多个RetryTopicConfiguration bean。
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.fixedBackOff(3000)
.maxAttempts(5)
.concurrency(1)
.includeTopics("my-topic", "my-other-topic")
.create(template);
}
@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<String, MyOtherPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.exponentialBackoff(1000, 2, 5000)
.maxAttempts(4)
.excludeTopics("my-topic", "my-other-topic")
.retryOn(MyException.class)
.create(template);
}
重试主题和dlt(dead-letter topic)的consumer将被分配到一个consumer组,该consumer组的组id是您在@KafkaListener注解的groupId参数中提供的带有主题后缀的组id的组合。如果你不提供任何内容,它们都将属于同一组,并且在重试主题上再平衡(rebalance)将导致在main topic上进行不必要的再平衡。
如果consumer使用者配置了ErrorHandlingDeserializer,以处理反序列化异常,那么为KafkaTemplate及其生产者配置一个serializer是很重要的,该serializer可以处理正常对象以及反序列化异常产生的原始byte[]值。template的泛型值类型应为Object。一种技术是使用DelegatingByTypeSerializer;示例如下:
@Bean
public ProducerFactory<String, Object> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfiguration(), new StringSerializer(),
new DelegatingByTypeSerializer(Map.of(byte[].class, new ByteArraySerializer(),
MyNormalObject.class, new JsonSerializer<Object>())));
}
@Bean
public KafkaTemplate<String, Object> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
多个@KafkaListener注解可以用于同一主题,无论是否手动分配分区以及非阻塞重试,但给定主题只能使用一个配置。最好使用单个RetryTopicConfiguration bean来配置此类主题;如果多个@RetryableTopic注解用于同一主题,则所有注解都应具有相同的值,否则其中一个注解将应用于该主题的所有监听器,而其他注解的值将被忽略。
3.3 配置全局设置和特性Configuring Global Settings and Features
自2.9版本以来,以前用于配置组件的bean覆盖方法已被删除(由于API的上述实验性质,没有弃用)。这不会更改RetryTopicConfiguration beans方法,只更改infrastructure组件的配置。现在,RetryTopicConfigurationSupport类应该在一个(单个)@Configuration类中进行继承,并覆盖正确的方法。示例如下:
@EnableKafka
@Configuration
public class MyRetryTopicConfiguration extends RetryTopicConfigurationSupport {
@Override
protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
blockingRetries
.retryOn(MyBlockingRetriesException.class, MyOtherBlockingRetriesException.class)
.backOff(new FixedBackOff(3000, 3));
}
@Override
protected void manageNonBlockingFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
nonBlockingFatalExceptions.add(MyNonBlockingException.class);
}
@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
// Use the new 2.9 mechanism to avoid re-fetching the same records after a pause
customizersConfigurer.customizeErrorHandler(eh -> {
eh.setSeekAfterError(false);
});
}
}
使用这种配置方法时,不应使用@EnableKafkaRetryTopic注解来防止由于重复的bean而导致上下文无法启动。请改用简单的@EnableKafka注解。
当autoCreateTopics为true时,将使用指定的分区数和replication因子创建main topic和retry topic。从3.0版本开始,默认的replication因子是-1,这意味着使用broker默认值。如果你的broker版本早于2.4,则需要设置一个显式值。要覆盖特定主题(例如main主题或DLT)的这些值,只需添加一个具有所需属性的NewTopic @Bean;将覆盖自动创建属性。
默认情况下,发布记录到重试主题将使用接收到的记录的原始分区。如果重试主题的分区少于main主题的分区,则应适当配置框架;下面是一个例子。
@EnableKafka
@Configuration
public class Config extends RetryTopicConfigurationSupport {
@Override
protected Consumer<DeadLetterPublishingRecovererFactory> configureDeadLetterPublishingContainerFactory() {
return dlprf -> dlprf.setPartitionResolver((cr, nextTopic) -> null);
}
...
}
函数的参数是consumer record和下一个topic的名称。你可以返回一个特定的分区编号,也可以返回null以指示KafkaProducer应该确定分区。
默认情况下,当记录通过重试主题转换时,会保留重试header的所有值(尝试次数、时间戳)。从2.9.6版本开始,如果你只想保留这些标头的最后一个值,请使用上面显示的configureDeadLetterPublishingContainerFactory()方法将工厂的retainAllRetryHeaderValues属性设置为false。
四、程序化构建Programmatic Construction
该功能旨在与@KafkaListener一起使用;但是,一些用户请求提供有关如何以编程方式配置非阻塞重试的信息。下面的Spring Boot应用程序提供了一个如何执行此操作的示例。
@SpringBootApplication
public class Application extends RetryTopicConfigurationSupport {
public static void main(String[] args) {
SpringApplication.run(2Application.class, args);
}
@Bean
RetryTopicConfiguration retryConfig(KafkaTemplate<String, String> template) {
return RetryTopicConfigurationBuilder.newInstance()
.maxAttempts(4)
.autoCreateTopicsWith(2, (short) 1)
.create(template);
}
@Bean
TaskScheduler scheduler() {
return new ThreadPoolTaskScheduler();
}
@Bean
@Order(0)
SmartInitializingSingleton dynamicRetry(RetryTopicConfigurer configurer, RetryTopicConfiguration config,
KafkaListenerAnnotationBeanPostProcessor<?, ?> bpp, KafkaListenerContainerFactory<?> factory,
Listener listener, KafkaListenerEndpointRegistry registry) {
return () -> {
KafkaListenerEndpointRegistrar registrar = bpp.getEndpointRegistrar();
MethodKafkaListenerEndpoint<String, String> mainEndpoint = new MethodKafkaListenerEndpoint<>();
EndpointProcessor endpointProcessor = endpoint -> {
// customize as needed (e.g. apply attributes to retry endpoints).
if (!endpoint.equals(mainEndpoint)) {
endpoint.setConcurrency(1);
}
// these are required
endpoint.setMessageHandlerMethodFactory(bpp.getMessageHandlerMethodFactory());
endpoint.setTopics("topic");
endpoint.setId("id");
endpoint.setGroupId("group");
};
mainEndpoint.setBean(listener);
try {
mainEndpoint.setMethod(Listener.class.getDeclaredMethod("onMessage", ConsumerRecord.class));
}
catch (NoSuchMethodException | SecurityException ex) {
throw new IllegalStateException(ex);
}
mainEndpoint.setConcurrency(2);
mainEndpoint.setTopics("topic");
mainEndpoint.setId("id");
mainEndpoint.setGroupId("group");
configurer.processMainAndRetryListeners(endpointProcessor, mainEndpoint, config, registrar, factory,
"kafkaListenerContainerFactory");
};
}
@Bean
ApplicationRunner runner(KafkaTemplate<String, String> template) {
return args -> {
template.send("topic", "test");
};
}
}
@Component
class Listener implements MessageListener<String, String> {
@Override
public void onMessage(ConsumerRecord<String, String> record) {
System.out.println(KafkaUtils.format(record));
throw new RuntimeException("test");
}
}
只有在刷新应用程序上下文之前处理了配置,才会自动创建主题,如上面的示例所示。要在运行时配置容器,需要使用其他技术创建主题。
五、特性Features
大多数特性对于@RetryableTopic注释和RetryTopicConfiguration bean都可用。
5.1 重试等待策略配置BackOff Configuration
BackOff配置依赖于Spring Retry项目中的BackOffPolicy接口。
它包括:
- Fixed Back Off
- Exponential Back Off
- Random Exponential Back Off
- Uniform Random Back Off
- No Back Off
- Custom Back Off
@RetryableTopic(attempts = 5,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000))
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.fixedBackoff(3000)
.maxAttempts(4)
.create(template);
}
你也可以提供Spring Retry的SleepingBackOffPolicy接口的自定义实现:
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.customBackOff(new MyCustomBackOffPolicy())
.maxAttempts(5)
.create(template);
}
默认的backoff策略是FixedBackOffPolicy,最多3次尝试,间隔1000ms。
ExponentialBackOffPolicy的默认最大延迟为30秒。如果您的back off策略需要值大于该值的延迟,请相应地调整maxDelay属性。
第一次尝试会计入maxAttempts,因此如果你提供的maxAttempts值为4,则会有原始尝试加上3次重试。
5.2 全局超时Global timeout
你可以设置重试过程的全局超时时间。如果达到该时间,则下一次消费者抛出异常时,消息将直接发送到DLT(dead-letter topic),或者在没有DLT可用的情况下结束处理。
@RetryableTopic(backoff = @Backoff(2000), timeout = 5000)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.fixedBackoff(2000)
.timeoutAfter(5000)
.create(template);
}
默认值是没有超时设置,这也可以通过提供-1作为超时值来实现。
5.3 异常分类器Exception Classifier
你可以指定要对哪些异常重试,哪些不重试。还可以将其设置为遍历原因(traverse the causes)以查找嵌套异常。
@RetryableTopic(include = {MyRetryException.class, MyOtherRetryException.class}, traversingCauses = true)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
throw new RuntimeException(new MyRetryException()); // Will retry
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyOtherPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.notRetryOn(MyDontRetryException.class)
.create(template);
}
默认行为是对所有异常重试,而不是遍历原因。
自2.8.3以来,有一个fatal异常的全局列表,这将导致记录在没有任何重试的情况下被发送到DLT。有关致命异常的默认列表,请参阅DefaultErrorHandler。通过继承RetryTopicConfigurationSupport重写@Configuration类中的configureNonBlockingRetries方法,可以向该列表添加异常或从中删除异常。有关详细信息,请参阅配置全局设置和特性。
@Override
protected void manageNonBlockingFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
nonBlockingFatalExceptions.add(MyNonBlockingException.class);
}
要禁用fatal异常分类,只需清除提供的列表。
5.4 包括和排除主题Include and Exclude Topics
你可以通过. includeTopic(String topic)、. includeTopics(Collection topics)、 .excludeTopic(String topic)和 .excludeTopics(Collection topics) 方法来决定哪些主题将由RetryTopicConfiguration bean处理,哪些不处理。
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.includeTopics(List.of("my-included-topic", "my-other-included-topic"))
.create(template);
}
@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.excludeTopic("my-excluded-topic")
.create(template);
}
默认行为是包含所有主题。
5.5 主题自动创建Topics AutoCreation
除非另有说明,否则框架将使用KafkaAdmin bean的NewTopic bean自动创建所需的主题。你可以指定创建主题时使用的分区数量和复制因子,也可以关闭此功能。从3.0版本开始,默认复制因子为-1,这意味着使用broker默认值。如果您的broker版本早于2.4,则需要设置显式值。
注意,如果你不使用Spring Boot,则必须提供KafkaAdmin bean才能使用此功能。
@RetryableTopic(numPartitions = 2, replicationFactor = 3)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@RetryableTopic(autoCreateTopics = false)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.autoCreateTopicsWith(2, 3)
.create(template);
}
@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.doNotAutoCreateRetryTopics()
.create(template);
}
默认情况下,主题是自动创建的,只有一个分区,复制因子为-1(意味着使用代理默认值)。如果您的代理版本早于2.4,则需要显式设置值。
5.6 失败报头管理Failure Header Management
在考虑如何管理失败header(原始header和异常header)时,框架将委托给DeadLetterPublishingRecover来决定是否附加或替换标头。
默认情况下,它将appendOriginalHeaders显式设置为false,并将stripPreviousExceptionHeaders保留为DeadLetterPublishingRecover使用的默认值。
这意味着只有第一个“原始”的和最后一个异常header与默认配置一起保留。这是为了避免在涉及许多重试步骤时创建过大的消息(例如,由于堆栈跟踪header)。
有关详细信息,请参阅管理死信记录头。
要将框架重新配置为对这些属性使用不同的设置,请通过在一个@Configuration类继承RetryTopicConfigurationSupport,覆盖configureCustomizers方法来配置DeadLetterPublishingRecoverer自定义程序。有关更多详细信息,请参阅配置全局设置和特性。
@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
customizersConfigurer.customizeDeadLetterPublishingRecoverer(dlpr -> {
dlpr.setAppendOriginalHeaders(true);
dlpr.setStripPreviousExceptionHeaders(false);
});
}
从2.8.4版本开始,如果你希望添加自定义标头(除了工厂添加的重试信息标头外,还可以向工厂添加headersFunction,factory.setHeadersFunction((rec, ex) → { … })。
默认情况下,添加的任何头都是累积的——Kafka标头可以包含多个值。从2.9.5版本开始,如果函数返回的Headers包含类型为“DeadLetterPublishingRecover.SingleRecordHeader”的标头,则该标头的任何现有值都将被删除,只保留新的单个值。
5.7 自定义死信发布恢复器Custom DeadLetterPublishingRecoverer
正如在Failure Header Management中看到的那样,你可以自定义框架创建的默认DeadLetterPublishingRecoverer实例。但是,对于某些用例,有创建DeadLetterPublishingRecoverer的子类,例如覆盖createProducerRecord()以修改发送到重试(或死信)主题的内容。从3.0.9版本开始,您可以覆盖RetryConfigurationSupport.configureDeadLetterPublishingContainerFactory()方法以提供DeadLetterPublisherCreator实例,例如:
@Override
protected Consumer<DeadLetterPublishingRecovererFactory>
configureDeadLetterPublishingContainerFactory() {
return (factory) -> factory.setDeadLetterPublisherCreator(
(templateResolver, destinationResolver) ->
new CustomDLPR(templateResolver, destinationResolver));
}
建议您在构造自定义实例时使用提供的解析器。
六、合并阻塞和非阻塞重试Combining Blocking and Non-Blocking Retries
从2.8.4开始,您可以将框架配置为同时使用阻塞和非阻塞重试。例如,可以有一组异常,这些异常也可能会在下一条记录上继续触发错误,例如DatabaseAccessException,因此你可以在将同一条记录发送到重试主题或直接发送到DLT之前重试几次。
要配置阻塞重试,请在一个@Configuration类中继承RetryTopicConfigurationSupport,覆盖configureBlockingRetries方法,并添加要重试的异常以及要使用的BackOff。默认的BackOff是FixedBackOff,它没有延迟和有9次尝试。有关详细信息,请参阅配置全局设置和特性。
@Override
protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
blockingRetries
.retryOn(MyBlockingRetryException.class, MyOtherBlockingRetryException.class)
.backOff(new FixedBackOff(3000, 5));
}
结合全局可重试主题的fatal异常分类,你可以为您想要的任何行为配置框架,例如让一些异常同时触发阻塞和非阻塞重试,只触发一种或另一种,或者直接进入DLT而不进行任何类型的重试。
以下是两种配置协同工作的示例:
@Override
protected void configureBlockingRetries(BlockingRetriesConfigurer blockingRetries) {
blockingRetries
.retryOn(ShouldRetryOnlyBlockingException.class, ShouldRetryViaBothException.class)
.backOff(new FixedBackOff(50, 3));
}
@Override
protected void manageNonBlockingFatalExceptions(List<Class<? extends Throwable>> nonBlockingFatalExceptions) {
nonBlockingFatalExceptions.add(ShouldSkipBothRetriesException.class);
}
在本例中:
- ShouldRetryOnlyBlockingException.class将仅通过阻塞重试,如果所有重试都失败,消息将直接进入DLT。
- ShouldRetryViaBothException.class将通过阻塞重试,如果所有阻塞重试都失败,则将消息转发到下一个重试主题进行另一组尝试。
- ShouldSkipBothRetriesException.class永远不会以任何方式重试,如果第一次处理尝试失败,消息将直接进入DLT。
请注意,阻塞重试行为是allowlist–你可以添加你确实希望以这种方式重试的异常;而非阻塞重试分类是针对FATAL异常的,因此是denylist–你添加了不想进行非阻塞重试的异常,消息会直接发送到DLT。
非阻塞异常分类行为还取决于特定主题的配置。
七、访问投递尝试次数Accessing Delivery Attempts
为了访问阻塞和非阻塞投递尝试次数,将这些头添加到你的@KafkaListener方法签名中:
@Header(KafkaHeaders.DELIVERY_ATTEMPT) int blockingAttempts,
@Header(name = RetryTopicHeaders.DEFAULT_HEADER_ATTEMPTS, required = false) Integer nonBlockingAttempts
只有当您将ContainerProperties.deliveryAttemptHeader设置为true时,才会提供阻塞投递尝试次数。
请注意,对于初始投递,非阻塞尝试将为空。
从3.0.10版本开始,提供了一个方便的KafkaMessageHeaderAccessor,可以更简单地访问这些头;访问器可以作为监听器方法的参数提供:
@RetryableTopic(backoff = @Backoff(...))
@KafkaListener(id = "dh1", topics = "dh1")
void listen(Thing thing, KafkaMessageHeaderAccessor accessor) {
...
}
使用accessor.getBlockingRetryDeliveryAttempt() 和 accessor.getNonBlockingRetryDeliveryAttempt()获取值。如果未启用阻塞重试,访问器将抛出IllegalStateException;对于非阻塞重试,访问器为初始投递返回1。
八、主题命名Topic Naming
重试主题和DLT(dead letter topic)的命名方法是在main topic后面加上一个提供的或默认的值,再加上该主题的延迟或索引。
示例:
“my-topic” → “my-topic-retry-0”, “my-topic-retry-1”, …, “my-topic-dlt”
“my-other-topic” → “my-topic-myRetrySuffix-1000”, “my-topic-myRetrySuffix-2000”, …, “my-topic-myDltSuffix”.
默认行为是为每次尝试创建单独的重试主题,并附加一个索引值:retry-0,retry-1…, 重试。因此,默认情况下,重试主题的数量是配置的maxAttempts减去1。
你可以配置后缀(8.1),选择是追加尝试索引还是延迟(8.2),在使用fixed backoff时使用单个重试主题(8.3),在使用exponential backoffs时对具有maxInterval的尝试使用单个重试主题(8.4)。
8.1 重试主题和Dlt后缀Retry Topics and Dlt Suffixes
你可以指定重试和Dlt主题将使用的后缀。
@RetryableTopic(retryTopicSuffix = "-my-retry-suffix", dltTopicSuffix = "-my-dlt-suffix")
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyOtherPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.retryTopicSuffix("-my-retry-suffix")
.dltTopicSuffix("-my-dlt-suffix")
.create(template);
}
默认后缀是“-retry”和“-dlt”,分别用于重试主题和dlt主题。
8.2 追加主题的索引或延迟Appending the Topic’s Index or Delay
你可以将主题的索引值或延迟值追加在后缀之后。
@RetryableTopic(topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.suffixTopicsWithIndexValues()
.create(template);
}
默认行为是使用延迟值的后缀,但具有多个主题的固定延迟配置除外,在这种情况下,topic以自身的索引作为后缀。
8.3 固定延迟重试的单个主题Single Topic for Fixed Delay Retries
如果你使用固定延迟策略,如FixedBackOffPolicy或NoBackOffPolicy,则可以使用单个主题来完成非阻塞重试。该主题将以提供的或默认的后缀作为后缀,并且不会追加索引值或延迟值。
以前的FixedDelayStrategy现在已弃用,可以用SameIntervalTopicReuseStrategy代替。
@RetryableTopic(backoff = @Backoff(2000), fixedDelayTopicStrategy = FixedDelayStrategy.SINGLE_TOPIC)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.fixedBackoff(3000)
.maxAttempts(5)
.useSingleTopicForFixedDelays()
.create(template);
}
默认行为是为每次尝试创建单独的重试主题,并附加其索引值:retry-0, retry-1,…
8.4 maxInterval指数延迟的单个主题Single Topic for maxInterval Exponential Delay
如果使用指数退避策略(ExponentialBackOffPolicy),则可以使用单个重试主题来完成非阻塞重试,其中尝试的延迟为maxInterval。
这个“final”重试主题将以提供的或默认的后缀作为后缀,并将追加索引或maxInterval值。
通过选择使用单个主题进行maxInterval延迟的重试,配置长时间保持重试的指数重试策略(exponential retry policy)可能会变得更可行,因为在这种方法中,你不需要大量的主题。
默认行为是使重试主题的数量等于配置的maxAttempts减1,并且当使用exponential backoff时,重试主题以延迟值为后缀,最后一个重试主题(对应于maxInterval延迟)以追加索引为后缀。
例如,当配置initialInterval=1000、multiplier=2和maxInterval=16000的exponential backoff时,为了持续尝试一个小时,需要将maxAttempts配置为229,默认情况下,所需的重试主题为:
- retry-1000
- retry-2000
- retry-4000
- retry-8000
- retry-16000-0
- retry-16000-1
- retry-16000-2
… - retry-16000-224
当使用在相同间隔重复使用重试主题的策略时,在上述相同配置中,所需的重试主题为:
- retry-1000
- retry-2000
- retry-4000
- retry-8000
- retry-16000
这将是未来版本中的默认设置。
@RetryableTopic(attempts = 230,
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 16000),
sameIntervalTopicReuseStrategy = SameIntervalTopicReuseStrategy.SINGLE_TOPIC)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<String, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.exponentialBackoff(1000, 2, 16000)
.maxAttempts(230)
.useSingleTopicForSameIntervals()
.create(template);
}
8.5 自定义命名策略Custom naming strategies
更复杂的命名策略可以通过注册一个实现RetryTopicNamesProviderFactory的bean来完成。默认实现是SuffixingRetryTopicNamesProviderFactory,可以通过以下方式注册不同的实现:
@Override
protected RetryTopicComponentFactory createComponentFactory() {
return new RetryTopicComponentFactory() {
@Override
public RetryTopicNamesProviderFactory retryTopicNamesProviderFactory() {
return new CustomRetryTopicNamesProviderFactory();
}
};
}
作为一个例子,下面的实现,除了标准后缀,添加一个前缀到重试主题和dlt的名称:
public class CustomRetryTopicNamesProviderFactory implements RetryTopicNamesProviderFactory {
@Override
public RetryTopicNamesProvider createRetryTopicNamesProvider(
DestinationTopic.Properties properties) {
if(properties.isMainEndpoint()) {
return new SuffixingRetryTopicNamesProvider(properties);
}
else {
return new SuffixingRetryTopicNamesProvider(properties) {
@Override
public String getTopicName(String topic) {
return "my-prefix-" + super.getTopicName(topic);
}
};
}
}
}
九、多监听器,相同主题Multiple Listeners, Same Topic(s)
从3.0版本开始,现在可以在同一主题上配置多个侦听器。为此,必须使用自定义主题命名将重试主题彼此隔离开来。用一个例子最好地说明了这一点:
@RetryableTopic(...
retryTopicSuffix = "-listener1", dltTopicSuffix = "-listener1-dlt",
topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
@KafkaListener(id = "listener1", groupId = "group1", topics = TWO_LISTENERS_TOPIC, ...)
void listen1(String message, @Header(KafkaHeaders.RECEIVED_TOPIC) String receivedTopic) {
...
}
@RetryableTopic(...
retryTopicSuffix = "-listener2", dltTopicSuffix = "-listener2-dlt",
topicSuffixingStrategy = TopicSuffixingStrategy.SUFFIX_WITH_INDEX_VALUE)
@KafkaListener(id = "listener2", groupId = "group2", topics = TWO_LISTENERS_TOPIC, ...)
void listen2(String message, @Header(KafkaHeaders.RECEIVED_TOPIC) String receivedTopic) {
...
}
topicSuffixingStrategy是可选的。框架将为每个监听器配置和使用一组单独的重试主题。
十、死信主题策略Dlt Strategies
框架为使用DLT提供了一些策略。你可以为DLT处理提供一种方法,使用默认的记录日志方法,或者根本不使用DLT。你还可以选择如果DLT处理失败会发生什么。
10.1 死信主题处理方法Dlt Processing Method
你可以指定用于处理DLT的方法,以及处理失败时的行为。为此,可以在带有@RetryableTopic注释的类的方法中使用@DltHandler注解。请注意,相同的方法将用于该类中的所有带@RetryableTopic注解的方法。
@RetryableTopic
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@DltHandler
public void processMessage(MyPojo message) {
// ... message processing, persistence, etc
}
DLT handler方法也可以通过RetryTopicConfigurationBuilder.dltHandlerMethod(String, String)方法提供,将应该处理DLT消息的bean的名称和方法名称作为参数传递。
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.dltHandlerMethod("myCustomDltProcessor", "processDltMessage")
.create(template);
}
@Component
public class MyCustomDltProcessor {
private final MyDependency myDependency;
public MyCustomDltProcessor(MyDependency myDependency) {
this.myDependency = myDependency;
}
public void processDltMessage(MyPojo message) {
// ... message processing, persistence, etc
}
}
如果未提供DLT处理程序,则使用默认的RetryTopicConfigurer.LoggingDltListenerHandlerMethod 被使用。
从2.8版本开始,如果你根本不想从该应用程序中的DLT消费,包括通过默认处理程序消费(或者您希望推迟消费),你可以控制DLT容器是否启动,而不依赖于容器工厂的autoStartup属性。
使用@RetryableTopic注释时,请将autoStartDltHandler属性设置为false;使用配置生成器时,请使用autoStartDltHandler(false)。
你稍后可以通过KafkaListenerEndpointRegistry启动DLT处理程序。
10.2 死信主题失败时的行为DLT Failure Behavior
如果DLT处理失败,有两种可能的行为:ALWAYS_RETRY_ON_ERROR 和 FAIL_ON_ERROR。在前者中,记录被转发回DLT主题,因此它不会阻塞其他DLT记录的处理。在后一种情况下,consumer在不转发消息的情况下结束执行。
@RetryableTopic(dltProcessingFailureStrategy =
DltStrategy.FAIL_ON_ERROR)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.dltHandlerMethod("myCustomDltProcessor", "processDltMessage")
.doNotRetryOnDltFailure()
.create(template);
}
默认行为为ALWAYS_RETRY_ON_ERROR。
从2.8.3版本开始,如果记录导致引发fatal异常(如反序列化异常),ALWAYS_RETRY_ON_ERROR将不会将该记录路由回DLT,因为通常情况下,总是会引发此类异常。
被视为fatal的异常有:
- DeserializationException
- MessageConversionException
- ConversionException
- MethodArgumentResolutionException
- NoSuchMethodException
- ClassCastException
你可以使用DestinationTopicResolver bean上的方法向该列表添加异常和从中删除异常。
有关详细信息,请参阅异常分类器。
10.3 配置不使用死信主题Configuring No DLT
该框架还提供了不为主题配置DLT的可能性。在这种情况下,重试结束后,处理就结束了。
@RetryableTopic(dltProcessingFailureStrategy =
DltStrategy.NO_DLT)
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.doNotConfigureDlt()
.create(template);
}
十一、指定监听器容器工厂Specifying a ListenerContainerFactory
默认情况下,RetryTopic配置将使用@KafkaListener注解提供的工厂,但是你可以指定一个不同的工厂来创建重试主题和dlt监听器容器。对于@RetryableTopic注解,你可以提供工厂的bean名称,对于RetryTopicConfiguration bean,你可以提供bean名称或实例本身。
@RetryableTopic(listenerContainerFactory = "my-retry-topic-factory")
@KafkaListener(topics = "my-annotated-topic")
public void processMessage(MyPojo message) {
// ... message processing
}
@Bean
public RetryTopicConfiguration myRetryTopic(KafkaTemplate<Integer, MyPojo> template,
ConcurrentKafkaListenerContainerFactory<Integer, MyPojo> factory) {
return RetryTopicConfigurationBuilder
.newInstance()
.listenerFactory(factory)
.create(template);
}
@Bean
public RetryTopicConfiguration myOtherRetryTopic(KafkaTemplate<Integer, MyPojo> template) {
return RetryTopicConfigurationBuilder
.newInstance()
.listenerFactory("my-retry-topic-factory")
.create(template);
}
自2.8.3版本以来,你可以将同一工厂用于可重试和不可重试的主题。
如果你需要将工厂配置行为恢复到2.8.3之前的版本,你可以在一个@Configuration类继承RetryTopicConfigurationSupport,覆盖其中的configureRetryTopicConfigurer方法,如配置全局设置和特性中所述,并将useLegacyFactoryConfigurer设置为true,例如:
@Override
protected Consumer<RetryTopicConfigurer> configureRetryTopicConfigurer() {
return rtc -> rtc.useLegacyFactoryConfigurer(true);
}
十二、在运行时访问主题信息Accessing Topics’ Information at Runtime
自2.9版本以来,你可以通过注入提供的DestinationTopicContainer bean在运行时访问有关主题链(topic chain)的信息。此接口提供了为主题查找链中下一个主题的方法,或者在DLT中查找(如果已配置),以及有用的属性,如主题的名称、延迟和类型。
作为真实世界的用例示例,你可以使用这些信息,以便console应用程序可以在处理失败(例如bug / inconsistent state)得到解决后,将记录从DLT重新发送到链中的第一个重试主题。
DestinationTopicContainer#getNextDestinationTopicFor()方法提供的DestinationTopic对应于输入主题链中注册的下一个主题。由于不同的因素,如异常分类、尝试次数或single-topic 固定延迟策略,消息将转发到的实际主题可能会有所不同。如果需要权衡这些因素,请使用DestinationTopicResolver接口。
十三、修改KafkaBackOffException日志级别Changing KafkaBackOffException Logging Level
当重试主题中的消息未到期消费时,将引发KafkaBackOffException。默认情况下,此类异常记录在DEBUG级别,但你可以通过在@Configuration类的ListenerContainerFactoryConfigurer中设置错误处理自定义程序来更改此行为。
例如,要将日志记录级别更改为WARN,你可以添加:
@Override
protected void configureCustomizers(CustomizersConfigurer customizersConfigurer) {
customizersConfigurer.customizeErrorHandler(defaultErrorHandler ->
defaultErrorHandler.setLogLevel(KafkaException.Level.WARN))
}