Approaching science《the secret of spring kafka》
spring-kafka 在不同环境下的使用方式、相关配置详解、KafkaAutoConfiguration 配置原理、@EnableKafka 实际作用、@KafkaListener 注解解析、核心组件及实际关系、启动流程、消息监听、毒丸消息及解决方式、@KafkaHandler 使用技巧、@RetryableTopic 工作原理及使用示例等。
版本
- jdk: 17
- spring boot: 3.2.2
- spring-kafka: 3.1.2
- 注:本文只分析 spring-kafka 组件,若殿下想了解 apache kafka 则请移驾 [走近科学之《apache kafka 的秘密》](走近科学之《apache kafka 的秘密》_关于kafka的文献-CSDN博客) 或 [Approaching science《the secret of apache kafka》](人世间子 (xgllhz.top)),且本文中出现的有关名词或专业术语也可在这两篇文章中找到相应说明。
1 如何使用
spring-kafka 最常用的两种使用环境则是 spring boot 项目和非 spring boot 项目(spring 项目),在这两种环境下使用 spring-kafka 时会有所不同。具体体现在使用方式上。
1.1 spring boot 环境下
在 spring boot 环境下使用 spring-kafka 时,只需要三步即可,即在 pom 文件中添加 spring-kafka 依赖、在 yml 文件中添加 spring-kafka 相关配置、编写消息监听器组件类。
-
1、pom 文件中添加 spring-kafka 依赖 如:
<dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>3.1.2</version> </dependency>
-
2、yml 文件中添加 spring-kafka 相关配置(详细配置后文会说明) 如:
spring: kafka: bootstrap-servers: localhost:9092 producer: acks: 1 retries: 3 key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer consumer: enable-auto-commit: true auto-commit-interval: 2000 auto-offset-reset: latest max-poll-records: 500 key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
-
3、编写消息监听器组件 如:
@Component public class TestListener { @KafkaListener(topics = { "test-momo" }, groupId = "test-momo-group") public void zedListener(List<ConsumerRecord<?, ?>> records) { for (ConsumerRecord<?, ?> record : records) { System.out.println(record.value()); // 消费消息 } } }
1.2 非 spring boot 环境下
在非 spring boot 环境下使用 spring-kafka 时,也需要三步,即在 pom 文件中添加 spring-kafka 依赖、编写 spring-kafka 相关配置类、编写消息监听器组件类。相较于在 spring boot 环境下,在非 spring boot 环境下我们需要自行编写 spring-kafka 相关配置类来配置 spring-kafka。
-
1、pom 文件中添加 spring-kafka 依赖 如:
<dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>3.1.2</version> </dependency>
-
2、编写 spring-kafka 相关配置类 如:
@Component @EnableKafka public class SpringKafkaConfig { private static final String BOOTSTRAP_SERVERS = "localhost:9092"; // 向 spring ioc 容器注入 kafkaTemplate bean 该 bean 主要用来发送消息 // 注:若只需要消费消息 则不需要注册该 bean @Bean public KafkaTemplate<String, Object> kafkaTemplate() { return new KafkaTemplate<>(producerFactory()); } // 向 spring ioc 容器注入 ConcurrentKafkaListenerContainerFactory bean // 该 bean 主要用来构建 ConcurrentMessageListenerContainer // ConcurrentMessageListenerContainer 主要用来管理消息监听器 // 注:若只需要生产消息 则不需要注册该 bean @Bean public ConcurrentKafkaListenerContainerFactory<String, Object> containerFactory() { ConcurrentKafkaListenerContainerFactory<String, Object> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory()); return factory; } // 生产者工厂 public ProducerFactory<String, Object> producerFactory() { return new DefaultKafkaProducerFactory<>(buildProducerConfig()); } // 消费者工厂 private ConsumerFactory<String, Object> consumerFactory() { return new DefaultKafkaConsumerFactory<>(buildConsumerConfig()); } // 生产者配置 private Map<String, Object> buildProducerConfig() { Map<String, Object> map = new HashMap<>(); map.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); map.put(ProducerConfig.ACKS_CONFIG, "1"); map.put(ProducerConfig.RETRIES_CONFIG, "3"); map.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); map.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); return map; } // 消费者配置 private Map<String, Object> buildConsumerConfig() { Map<String, Object> map = new HashMap<>(); map.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS); map.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true); map.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, 2000); map.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); map.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500); map.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); map.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); return map; } }
-
3、编写消息监听器组件 如:
@Component public class TestListener { @KafkaListener(topics = { "test-momo" }, groupId = "test-momo-group") public void zedListener(List<ConsumerRecord<?, ?>> records) { for (ConsumerRecord<?, ?> record : records) { System.out.println(record.value()); // 消费消息 } } }
1.3 配置详解
为了方便用户更加灵活的使用 spring-kafka,其对外提供了许多配置。spring boot 项目的 yml 配置中的 spring.kafka.producer 和 spring.kafka.consumer key 分别对应非 spring boot 项目中的 ProducerConfig 和 ConsumerConfig 配置。二者分别用来配置生产者和消费者。下面以 spring boot 环境下的配置进行简单说明。
spring:
kafka:
bootstrap-servers: localhost:9092 # kafka 集群地址
producer: # 生产者配置
acks: 1 # 应答级别 即集群(broker)接收到生产者发送的消息后 会向生产者回复一条确认消息
# 0: broker 收到消息后立即回复 producer
# 1: broker 收到消息 且 leader(主分区)落盘后回复 producer
# -1(all): broker 收到消息 且 leader(主分区)落盘 且 leader 同步到 follower(副本分区)且 follower 落盘后回复 producer
retries: 3 # 生产者发送消息失败后的重试次数 默认为 int 最大值
# 若设置了重试且希望保证消息的有序性 则需要设置 max.in.flight.requests.per.connection = 1
batch-size: 16384 # 批量大小(即 RecordAccumulator 缓冲区每一批消息的最大值 当生产的消息积压达到整个阈值后 则由 sender 线程将其发送到 broker 的同一分区)
# 默认 16k 适当增大可提高生产者吞吐量 但过大会导致数据延迟
buffer-memory: 33554432 # RecordAccumulator 缓冲区大小 默认 32M
transaction-id-prefix: ti # 生产者事务前缀
key-serializer: org.apache.kafka.common.serialization.StringSerializer # 消息 key 序列化器
value-serializer: org.apache.kafka.common.serialization.StringSerializer # 消息 value 序列化器
properties: max.in.flight.requests.per.connection: 1 # sender 线程缓存没有收到 broker ack 的请求数 默认为 5 若开启幂等性 则值应该在 1 ~ 5 之间
linger.ms: 0 # sender 线程从 RecordAccumulator 缓冲区拉取消息的最大等待时间 默认 0ms 生产建议 5 ~ 100ms
# 当为 0ms 时表示消息会被立即拉取走 然后发送到 broker 即不会等到消息积压到 batch-size 的大小再拉取 即意味着 batch-size 将失效
consumer: # 消费者配置
enable-auto-commit: true # 是否自动提交 offset 开关 默认为 true 开启后消费者会周期性的向 broker 提交 offset
auto-commit-interval: 2000 # 消费者自动提交 offset 的频率 默认为 5s 只有 enable-auto-commit 为 true 时该设置才会生效
auto-offset-reset: latest # 当 kafka 中存储的 offset 丢失 消费者消费数据时的处理方式 默认为 latest
# earliest: 自动重置 offset 为最早的 offset
# latest: 自动重置 offset 为最新的 offset
# none: 若消费者组原来的(previous)的 offset 不存在 则向消费者抛出异常
# anything: 向消费者抛出异常
max-poll-records: 500 # 消费每次拉取消息数量的最大值 默认 500 条
group-id: test-group # 消费者所属的消费者组 id
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer # 消息 key 反序列化器
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer # 消息 value 反序列化器
listener:
type: batch # 消费方式 可选 batch、single 即批量或单条
concurrency: 3 # 消费者并发数 设置后消费者将以并发的方式消费数据
missing-topics-fatal: false # 当消费监听的 topic 不存在时 启动项目时是否异常 关掉
ack-mode: manual # 消费者 offset 提交模式 只有 enable-auto-commit 为 false 时才生效
# record: 当消费完一条数据后提交
# batch: 当每一批拉取的消息都消费完后提交
# time: 当每一批拉取的消息都消费完 且距离上次提交时间大于 time 时提交
# count: 在处理每一批拉取的消息时 若已处理的消息数量大于 count 则提交
# count_time: count 或 time 满足其中一个时提交
# manual: 处理完每一批拉取的消息后 手动调用 Acknowledgment.acknowledge() 将 offset 缓存到本地 在下一次拉取消息时提交
# manual_immediate: 处理完每一批拉取的消息后 手动调用 Acknowledgment.acknowledge() 立即提交
2 KafkaAutoConfiguration 与 @EnableKafka
2.1 KafkaAutoConfiguration
通过上文 如何使用 示例,可以看到 spring-kafka 在 spring boot 环境下和非 spring boot 环境下的使用方式并不一样,这是因为 spring boot 通过自动配置类 KafkaAutoConfiguration 帮我们配置了 spring-kafka,这才有了开箱即用的效果。而 spring boot 在自动配置 spring-kafka 时采用了 约定大于配置 的规则。
以下为 KafkaAutoConfiguration 配置类核心源码(篇幅原因 并未全部贴出)。通过源码可知,该配置类自行向 spring ioc 容器中注册了 spring-kafka 使用时的相关 bean,提供了类似于非 spring boot 环境下使用时 SpringKafkaConfig 配置类的功能(当然比示例中的配置类功能要更加丰富)。
@AutoConfiguration // 将该类标注为自动配置类 其作用是被标注的类在 spring boot 启动时会主动处理该配置类
@ConditionalOnClass(KafkaTemplate.class) // 条件注解 意为当类路径下存在 KafkaTempalte 类时该配置类才生效(即当引入了 spring-kafka 依赖时生效)
@EnableConfigurationProperties(KafkaProperties.class) // 开启配置属性 将 yml 文件中关于 spring-kafka 的配置映射到 KafkaProperties 类上
// 导入两个指定的配置类
@Import({ KafkaAnnotationDrivenConfiguration.class, KafkaStreamsAnnotationDrivenConfiguration.class })
public class KafkaAutoConfiguration {
// 通过构造注入的方式注入 KafkaProperties 且 若未配置 bootstrap servers 则默认为 localhost:9092
private final KafkaProperties properties;
KafkaAutoConfiguration(KafkaProperties properties) {
this.properties = properties;
}
@Bean // 向容器中注册 kafka 连接信息的辅助类 bean
@ConditionalOnMissingBean(KafkaConnectionDetails.class) // 当容器中不存在 KafkaConnectionDetails 类型的 bean 时才生效
PropertiesKafkaConnectionDetails kafkaConnectionDetails(KafkaProperties properties) {...}
// 该 bean 的主要作用是用来发送消息
@Bean // 向容器中注册 KafkaTemplate bean
@ConditionalOnMissingBean(KafkaTemplate.class) // 当容器中不存在 KafkaTemplate 类型的 bean 时才生效
public KafkaTemplate<?, ?> kafkaTemplate(ProducerFactory<Object, Object> kafkaProducerFactory,
ProducerListener<Object, Object> kafkaProducerListener,
ObjectProvider<RecordMessageConverter> messageConverter) {...}
// 该 bean 的主要作用是在生产者发送完消息后提供一些回调操作 如需要在发送完消息后进行回调处理 则可自定义注册该 bean
@Bean // 向容器中注册生产者监听器 ProducerListener bean
@ConditionalOnMissingBean(ProducerListener.class) // 当容器中不存在 ProducerListener 类型的 bean 时才生效
public LoggingProducerListener<Object, Object> kafkaProducerListener() {...}
@Bean // 向容器中注册消费者工厂 ConsumerFactory bean 其主要作用是为消费者消费数据提供辅助
@ConditionalOnMissingBean(ConsumerFactory.class) // 当容器中不存在 ConsumerFactory 类型的 bean 时才生效
public DefaultKafkaConsumerFactory<?, ?> kafkaConsumerFactory(KafkaConnectionDetails connectionDetails,
ObjectProvider<DefaultKafkaConsumerFactoryCustomizer> customizers, ObjectProvider<SslBundles> sslBundles) {...}
@Bean // 向容器中注册生产者工厂 ProducerFactory bean 其主要作用是为生产者生产消息提供辅助 且创建 KafkaTempalte bean 时需要
@ConditionalOnMissingBean(ProducerFactory.class) // 当容器中不存在 ProducerFactory 类型的 bean 时才生效
public DefaultKafkaProducerFactory<?, ?> kafkaProducerFactory(KafkaConnectionDetails connectionDetails,
ObjectProvider<DefaultKafkaProducerFactoryCustomizer> customizers, ObjectProvider<SslBundles> sslBundles) {...}
@Bean // 向容器中注册 kafka 事务管理器 bean
@ConditionalOnProperty(name = "spring.kafka.producer.transaction-id-prefix")
@ConditionalOnMissingBean // 当 transaction-id-prefix 配置存在且容器中不存在 KafkaTransactionManager 类型的 bean 时才生效
public KafkaTransactionManager<?, ?> kafkaTransactionManager(ProducerFactory<?, ?> producerFactory) {...}
@Bean // 注册 jaas 初始化相关的 bean 其主要作用是提供对 jaas(java 认证授权服务) 的支持
@ConditionalOnProperty(name = "spring.kafka.jaas.enabled")
@ConditionalOnMissingBean
public KafkaJaasLoginModuleInitializer kafkaJaasInitializer() throws IOException {...}
@Bean // 注册 KafkaAdmin bean KafkaAmin 封装了对 kafka 操作的相关 API
@ConditionalOnMissingBean
public KafkaAdmin kafkaAdmin(KafkaConnectionDetails connectionDetails, ObjectProvider<SslBundles> sslBundles) {...}
@Bean // 注册 kafka 重试 topic 配置 bean 其作用主要是配置 spring-kafka 的 retry topic 功能
@ConditionalOnProperty(name = "spring.kafka.retry.topic.enabled")
@ConditionalOnSingleCandidate(KafkaTemplate.class) // 当 enabled 属性开启且容器中存在 KafkaTemplate bean 时才生效
public RetryTopicConfiguration kafkaRetryTopicConfiguration(KafkaTemplate<?, ?> kafkaTemplate) {...}
}
通过 @Import 注解导入的两个配置类分别提供了以下功能:
- KafkaAnnotationDrivenConfiguration kafka 注解监听器(@KafkaListener)驱动配置类 该配置类主要i提供了一下三个功能:
- 注册 ConcurrentKafkaListenerContainerFactoryConfigurer bean,该 bean 的主要作用是用来配置 ConcurrentKafkaListenerContainerFactory bean(该 bean 的主要作用在 核心组件 章节会讲解)。且提供了两种环境下的 ConcurrentKafkaListenerContainerFactoryConfigurer bean,一种是 platform(平台线程,若虚拟线程未激活则激活平台线程),一种是 virtual(虚拟线程,若 spring.threads.virtual.enabled 为 true 且运行在 java 21 或更高版本的环境下则激活虚拟线程)。
- 注册 ConcurrentKafkaListenerContainerFactory bean,且是通过 ConcurrentKafkaListenerContainerFactoryConfigurer 配置后的。
- 开启 spring-kafka,即使用 @EnableKafka,且只有在容器中不存在名称为 org.springframework.kafka.config.internalKafkaListenerAnnotationProcessor 的 bean 时才生效(该 bean 实际为 spring-kafka 提供的 BPP,用来解析 @KafkaListener 注解,后文会详细讲解s)。
- KafkaStreamsAnnotationDrivenConfiguration kafka 流式 api 注解驱动配置类
- 该配置类主要提供了配置 kafka stream 功能相关的 bean。
- kafka stream 是 kafka 提供的大数据处理框架,其主要功能是对存储在 kafka 中的数据进行流式处理和分析。常见的大数据处理框架有 spark、storm 等。
- 本文不涉及 kafka stream 相关的内容。
2.2 @EnableKafka
在非 spring boot 环境下使用 spring-kafka 的配置类上,以及 spring boot 提供的 KafakAutoConfiguration kafka 自动配置类中都使用了 @EnableKafka 注解,由此可见该注解为 spring-kafka 的核心。
@EnableKafka 的主要作用是通过 @Import 注解注入了 KafkaBootstrapConfiguration 配置类,而 KafkaBootstrapConfiguration 配置类的主要作用是向容器中注册了两个 bean,分别是 KafkaListenerAnnotationBeanPostProcessor 和 KafkaListenerEndpointRegistry,且二者名称分别是 org.springframework.kafka.config.internalKafkaListenerAnnotationProcessor 和 org.springframework.kafka.config.internalKafkaListenerEndpointRegistry。关于这两个 bean 的主要作用,下文会讲解。
3 核心组件
3.1 核心组件
由源码可知,spring-kafka 与 apache-kafka-clients 各核心组件关系图如上图所示(由于图片过大会造成字体模糊,故部分中间组件未体现在图中)。其各主要功能如下:
- Admin: kafka-clients 提供的 kafka 管理接口,主要用来管理和检查 topic、broker、config 和 ACL 等。AdminClient 为其扩展类,增加了获取客户端实例的接口。KafkaAdminClient 为其具体实现类,且依赖于配置类 AdminClientConfig。
- Producer: kafka-clients 提供的生产者接口,主要用来发送消息、提交事务等。KafkaProducer 是其默认实现类,且依赖于配置类 ProducerConfig。
- Consumer: kafka-clients 提供的消费者接口,主要用来消费消息(拉取)、提交偏移量等。KafkaConsumer 是其默认实现类,且依赖于配置类 ConsumerConfig。
- KafkaAdminOperations: spring-kafka 提供的 kafka 管理接口。KafkaAdmin 是其默认实现类,该类委托 kafka-clients 提供的 AdminClient 类来管理 kafka。
- ProducerFactory: spring-kafka 提供的生产者工厂。其在创建生产者功能的基础上额外提供了获取生产者配置、管理序列化器等功能。DefaultKafkaProducerFactory 是其默认实现类,其以策略的方式生成 Producer 实例。
- ConsumerFactory: spring-kafka 提供的消费者工厂。其在创建消费者功能的基础上额外提供了获取消费者配置、管理反序列化器等功能。DefaultKafkaConsumerFactory 是其默认实现类,其以策略的方式生成 Consumer 实例。
- KafkaOperations: spring-kafka 提供的 kafka 消息操作接口,提供生产(发送)消息、消费(接收)消息等功能。KafkaTemplate 是其默认实现类,该类聚合了 ProducerFactory 和 ConsumerFactory 类,用二者创建的 Producer 和 Consumer 实例来发送和接收消息。
- KafkaMessageListenerContainer: spring-kafka 提供的支持自动分区分配和指定分区分配的单线程异步消息监听器容器。首先它是一个监听器容器,故它内置了创建消费者的功能(其聚合了 ConsumerFactory 类,并用内部类 Listener 包装了 Consumer,实际使用 ConsumerFactory 来实例化 Consumer 实例);然后它支持自动分区分配和指定分区分配;最后其借助 AsyncTaskExecutor 异步任务执行器来实现单线程异步消费消息。
- ConcurrentMessageListenerContainer: spring-kafka 提供的并发消息监听器容器。其内部维护了一个类型为 KafkaMessageListenerContainer 的容器实例集合 containers,这些容器实例会并发的消费消息,且所有容器实例会均匀的消费 topic 所对应的 partition。其并发度由 spring.kafka.listener.concurrency 配置决定,即集合 containers 中实例的数量由并发度决定。同时提供了 KafkaListenerContainerFactory 工厂类来创建该对象。
- KafkaListenerEndpointRegistry: spring-kafka 提供的消息监听器管理器,其用来管理监听器,并维护监听器的生命周期(如启动、销毁等)。其内部维护了一个 listenerContainers map,key 消息监听器容器唯一标识(自动生成),value 为消息监听器容器 ConcurrentMessageListenerContainer 实例。即其实际管理消息监听器为 ConcurrentMessageListenerContainer 实例(监听器容器内就是监听器)。
- KafkaListenerEndpointRegistrar: spring-kafka 提供的监听器注册辅助类,即其辅助 KafkaListenerEndpointRegistry 来将消息监听器注册到 KafkaListenerEndpointRegistry 中。其内部维护了一个 类型为 KafkaListenerEndpointDescriptor 的集合 endpointDescriptors,@KafkaListener 注解标注的方法解析后被包装成 KafkaListenerEndpointDescriptor 对象,然后放入 endpointDescriptors 中。
- @KafkaListener: spring-kafka 提供的消息监听器注解,用来标注一个类或方法为消息监听器。且可指定要监听的主题、分区、所处的消费者组、并发度、所处的监听器并发容器(即其对应的 ConcurrentMessageListenerContainer 实例,通过指定 KafkaListenerContainerFactory 来指定)。且可通过 @TopicPartition、@PartitionOffset 等注解来配置相应参数。
- KafkaListenerAnnotationBeanPostProcessor: spring-kafka 提供的 BPP(bean 后置处理器),用来解析 @KafkaListener 注解,并聚合了 KafkaListenerEndpointRegistrar 实例,以便将解析后的监听器注册到 KafkaListenerEndpointRegistry 中。
- @EnableKafka: spring-kafka 提供的用来开启 spring-kafka 的注解,其主要作用是向 spring ioc 容器中注册了 KafkaListenerEndpointRegistry bean 和 KafkaListenerAnnotationBeanPostProcessor bean。以便在项目启动时能够解析 @KafkaListener 注解并维护到 KafkaListenerEndpointRegistry 管理器中。
- @KafkaHandler: spring-kafka 提供的消息处理器,其可通过搭配 @KafkaListener 注解来指定消费 topic 中特定格式的消息体,后文会有具体使用方式。
- @RetryableTopic: spring-kafka 提供的可重试 topic 注解,其作用是在不暂停正常消息消费的前提下处理异常消息。其实现逻辑是自动创建一个带有重试机制(参考 spring-retry 组件,可自定义设置多种重试策略,如重试次数、重试间隔等等)的 topic,当程序在消费消息并出现异常时,则会将这些消息发往 retry topic,交给 retry topic 的消费者进行处理(这个消费当然得开发者根据业务逻辑自定义开发),经过重试策略后仍未正常消费这些消息,那么其会将消息发往 DLC(死信队列)中,若要继续处理这些消息,则需要通过 @DltHandler 注解来处理。@RetryableTopic 让 kafka 拥有了类似于 rabbit mq 死信队列一样的功能。需要注意的是,使用该功能可能会破坏消息的顺序性。
3.2 实际关系
上图简要描述了 @KafkaListener 注解、topic、KafkaListenerEndpointRegistry 管理器、ConcurrentMessageListenerContainer 实例以及 KafkaMessageListenerContainer 实例之间的关系。由图可知,每个被 @KafkaListener 注解标注的方法在解析后都会为其创建一个 ConcurrentMessageListenerContainer 容器,然后根据并发度配置 concurrency 的大小来为其创建 KafkaMessageListenerContainer 实例数量(即创建并发消费者,若不配置 concurrency 则默认创建一个消费者实例),维护到 containers 中,最后将 ConcurrentMessageListenerContainer 实例维护到 KafkaListenerEndpointRegistory 实例的 listenerContainers 集合中。
4 启动流程
上图为 spring-kafka 启动流程时序图,简单描述了 spring-kafka 的启动过程,其各环节说明如下:
- 1、ConfigurableApplicationContext#refresh():众所周知,spring ico 是 spring 系列作品的核心,而 refresh() 方法则是 spring ioc 的核心及入口,当然在 spring boot 中也不列外,所以,故事的一切,从这里开始。
- 1、AbstractApplicationContext#finishBeanFactoryInitialization():refresh() 方法的第 11 步,这一步的主要作用是在容器初始化完成后,完成剩余非懒加载单例 bean 的初始化。即通过 beanDefinitionMap 中的 bean 定义来初始化 bean,并在这过程中调用 BPP 来增强 bean。在这一步针对 spring-kafka 而言则是生成相关的 bean,并解析 @KafkaListener 注解。
- 1、ConfigurableListableBeanFactory#preInstantiateSingletons():具体是通过调用该方法来初始化非懒加载的单例 bean,且分为一下两个步骤。
- 1、BeanFactory#getBean():通过调用 getBean() 方法来初始化 bean。
- 1、KafkaListenerAnnotationBeanPostProcessor#postProcessAfterInitialization():调用 BPP 来增强 bean。即在创建被 @KafkaListener 注解标注的方法所在的类或被其标注的类的 bean 时,会调用 KafkaListenerAnnotationBeanPostProcessor 所实现的 postProcessAfterInitialization() 方法来增强该 bean。即此处调用该 BPP 的具体作用就是解析 @KafkaListener 注解.。
- 1、KafkaListenerEndpointRegistrar#registerEndpoint():并将解析到的内容添加到 KafkaListenerEndpointRegistrar 所维护的 endpointDescriptors 集合中。
- 1、KafkaListenerAnnotationBeanPostProcessor#postProcessAfterInitialization():调用 BPP 来增强 bean。即在创建被 @KafkaListener 注解标注的方法所在的类或被其标注的类的 bean 时,会调用 KafkaListenerAnnotationBeanPostProcessor 所实现的 postProcessAfterInitialization() 方法来增强该 bean。即此处调用该 BPP 的具体作用就是解析 @KafkaListener 注解.。
- 2、KafkaListenerAnnotationBeanPostProcessor#afterSingletonsInstantiated():回调 bean 的初始化方法。注意,bean 的初始化方法有 @PostConstruct、实现 InitializingBean、实现 SmartInitializingSingleton 接口。区别是前两种是在每个 bean 实例化完成后调用,而第三种则是在所有非懒加载的单例 bean 都实例化完了才调用。KafkaListenerAnnotationBeanPostProcessor 同时也是 SmartInitializingSingleton 接口的一个实现者。这里为什么使用第三种而不是前两种,目的是为了在所有 @KafkaListener 消费者都被解析完之后再进行 kafka 配置、监听器容器创建、启动等操作。
- 1、KafkaListenerConfigurer#configureKafkaListeners():调用所有 configurer 配置 kafka listener。这里的 configurer 默认是妹有实现者的,故需要开发者自定义实现。
- 2、KafkaListenerEndpointRegistrar#afterPropertiesSet():遍历 KafkaListenerEndpointRegistrar 中的 endpointDescriptors,为每个消费者创建监听器容器 ConcurrentMessageListenerContainer,并根据并发度(concurrency)为每个消费者创建对应数量消费者线程(即创建KafkaMessageListenerContainer 实例),然后将消费者线程维护进 ConcurrentMessageListenerContainer 实例中的 containers 中,再将 ConcurrentMessageListenerContainer 实例注册到 listenerContainers 中。
- 1、KafkaListenerEndpointRegistory#registListenerContainer():
- 1、BeanFactory#getBean():通过调用 getBean() 方法来初始化 bean。
- 1、ConfigurableListableBeanFactory#preInstantiateSingletons():具体是通过调用该方法来初始化非懒加载的单例 bean,且分为一下两个步骤。
- 2、ConfigurableApplicationContext#finishRefresh():这是 refresh() 的第 12 步,其主要作用是在容器刷新完成后,清楚额外缓存、初始化容器生命周期、发布容器已刷新事件。这一步针对 spring-kafka 而言则是生成消费者实例并启动消费者。
- 1、LifecycleProcessor#onRefresh():第十二步是通过此方法来初始化容器生命周期,且在此过程中会逐个启动 bean。
- 1、KafkaListenerEndpointRegistory#start():启动监听器端点注册中心。若要在 spring ioc 刷新时启动 bean,则该 bean 对应的类需要实现 Lifecycle 接口及其 start() 方法。KafkaListenerEndpointRegistory 实现了该接口,并在 start() 方法中遍历并发消息监听器容器集合 listenerContainers,逐个启动容器。
- 1、ConcurrentMessageListenerContainer#doStart():启动并发监听器容器。KafkaListenerEndpointRegistory 在启动并发容器时调用了 ConcurrentMessageListenerContainer 所实现的 doStart() 方法,该方法内会根据并发度(concurrency)创建对应数量的消费者线程实例(即创建 KafkaMessageListenerContainer 实例),并调用 KafkaMessageListenerContainer 实现的 doStart() 方法启动消费者线程实例, 然后将消费者线程维护进 ConcurrentMessageListenerContainer 实例中的 containers 集合中。
- 1、KafkaMessageListenerContainer#doStart():启动消息监听器容器。实际上是启动消费者。这里会创建消费者(创建 spring-kafka 提供的 ListenerConsumer 类实例,其持有了 kafka-clients 提供的 Consumer 类型属性),ListenerConsumer 实现了 Runable 接口,最终会将消费者实例(实际上是个 task)交给 =异步任务执行器 AsyncTaskExecutor 去执行。
- 1、ConcurrentMessageListenerContainer#doStart():启动并发监听器容器。KafkaListenerEndpointRegistory 在启动并发容器时调用了 ConcurrentMessageListenerContainer 所实现的 doStart() 方法,该方法内会根据并发度(concurrency)创建对应数量的消费者线程实例(即创建 KafkaMessageListenerContainer 实例),并调用 KafkaMessageListenerContainer 实现的 doStart() 方法启动消费者线程实例, 然后将消费者线程维护进 ConcurrentMessageListenerContainer 实例中的 containers 集合中。
- 1、KafkaListenerEndpointRegistory#start():启动监听器端点注册中心。若要在 spring ioc 刷新时启动 bean,则该 bean 对应的类需要实现 Lifecycle 接口及其 start() 方法。KafkaListenerEndpointRegistory 实现了该接口,并在 start() 方法中遍历并发消息监听器容器集合 listenerContainers,逐个启动容器。
- 1、LifecycleProcessor#onRefresh():第十二步是通过此方法来初始化容器生命周期,且在此过程中会逐个启动 bean。
- 1、AbstractApplicationContext#finishBeanFactoryInitialization():refresh() 方法的第 11 步,这一步的主要作用是在容器初始化完成后,完成剩余非懒加载单例 bean 的初始化。即通过 beanDefinitionMap 中的 bean 定义来初始化 bean,并在这过程中调用 BPP 来增强 bean。在这一步针对 spring-kafka 而言则是生成相关的 bean,并解析 @KafkaListener 注解。
5 监听消息
对于 @KafkaListener 所声明的消费者来说,其最终创建的消费者实例是 ListenerConsumer 对象,而该类实现了 Runable 接口,且其以异步的方式消费消息,所以其消费消息的逻辑都在实现的 run() 方法中。其伪代码如下:
while (isRunning()) {
try {
pollAndInvoke();
} catch(Exception e) {
// 异常处理
} finally {
// 关闭各种资源
}
}
由伪代码可见,其以 while true 的方式不断执行拉取和回调操作,其中拉取操作是通过调用其持有的 Consumer 实例的 poll() api 来实现的,即底层调用的是 apache kafka-clients 中 Consumer 提供的 poll() 接口;而回调则是通过反射的方式调用开发者编写的消费逻辑代码。
6 备注
6.1 毒丸消息
kafka 毒丸消息是指在消费者消费消息时,若反序列化失败则消费者线程会一直处在 “反序列化失败-重试-反序列化失败” 的死循环中。这样就会造成线程空转、性能消耗、疯狂打印日志等,还可能直接让服务宕机,所以这玩意儿贼恐怖。
解决方式:kafka 是不支持删除数据的(即没有删数据的 api),但 spring-kafka 提供了反序列化错误处理器 ErrorHandlingDeserializer,它可以处理掉反序列化失败的消息,使得 offset 正常提交。
开发者可以通过配置文件或硬编码的方式使用 ErrorHandlingDeserializer:
# 配置文件方式
# spring-kafka consumer 部分配置
spring:
kafka:
consumer:
# 指定反序列化错误处理器
key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
properties:
spring:
deserializer:
key:
delegate: # 指定 key 反序列化器
class: org.apache.kafka.common.serialization.StringDeserializer
value:
delegate: # 指定 value 反序列化器
class: org.apache.kafka.common.serialization.StringDeserializer
// 硬编码方式
// 注:ConsumerConfig 是 kafka-clients 提供的 ErrorHandlingDeserializer 是 spring-kafka 提供的
map.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
map.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class);
map.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, StringDeserializer.class);
map.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, JsonDeserializer.class);
6.2 @KafkaHandler 示例
在 3 核心组件 中简单说明了 @KafkaHandler 注解的作用,即通过该注解可以消费 topic 中指定格式的消息。如在某个 topic 中存在三种不同格式的 json 消息,其已知的格式对应 Zed 类和 Fizz 类,第三种只知道是个 json,但具体的数据结构不知道,这时候我们就可以使用 @KafkaHandler 注解来消费这个 topic。
// 这个消费者消费 topic test-zed 这个 topic 中包含三种 json 结构
// 注:当消息格式为 json 时 则需要指定反序列化器为 json 相关的
// 注:若当前消费者相关配置(如反序列化器)与项目中其它消费者配置冲突 则可以使用 containerFactory 属性指定其消费者(具体见 如何使用 章节中非 spring boot 环境下小节)
@Component
@KafkaListener(topics = { "test-zed" }, groupId = "test-zed-group", containerFactory = "containerFactory")
public class ZedListener {
@KafkaHandler // 消费 Zed 格式消息
public void consumeZed(@Payload Zed zed) {
System.out.println(zed);
}
@KafkaHandler // 消费 Fizz 格式消息
public void consumeFizz(@Payload Fizz fizz) {
System.out.println(fizz);
}
@KafkaHandler(isDefault = true) // 消费未知格式的 json 格式消息 且将其指定为默认处理器
public void consumeOther(@Payload Map<String, Object> map) {
System.out.println(map);
}
}
spring 系列组件中,默认使用 jackson 作为序列化和反序列化组件,且 spring 为反序列化设置了安全机制,即只有被信任的目标类型才能被反序列化(spring-security 中也有相似的机制)。所以,对于上述代码中反序列化的目标类型 Zed 和 Fizz 需要被 spring 信任(内置数据结构或类默认是被信任的,如 List、Map 等),做法是设置配置 spring.json.trusted.packages 的值为反序列化目标类型所在的包路径。
6.3 @RetryableTopic 示例
@RetryableTopic(backoff = @Backoff(delay = 6000), attempts = "3", sameIntervalTopicReuseStrategy = SameIntervalTopicReuseStrategy.SINGLE_TOPIC, kafkaTemplate = "testKafkaTemplate", concurrency = "4")
@KafkaListener(topics = { "test-fizz" }, groupId = "test-fizz-group")
public void consumer(List<ConsumerRecord<?, ?>> records) {
System.out.println(records);
}
如上述代码所示,该注解需要配合 @KafkaListener 使用,且可以通过各种属性灵活配置 retry topic。如支持通过 spring-retry 组件提供的 @Backoff 注解配置重试策略(间隔多长时间重试多少次等);通过 attempts 设置重试多少次失败后发送到 dlc;通过 sameIntervalTopicReuseStrategy 属性配置重试主题个数,可设置为单例(即只创建一个 retry topic)或并发(默认创建和 attempts 数目一致的 retry topic 数);通过 kafkaTemplate 设置发送到 retry topic 时使用的发送组件;通过 concurrency 设置 retry topic 多对应消费者的并发度,默认和主容器一致,即默认并发度和 test-fizz topic 的当前消费者并发度一致。
注:使用该功能可能会破坏消息的顺序性。
《尘埃落定》- 张敬轩.mp3