一步步解决spring-kafka消息丢失

本文深入探讨Kafka消费不稳定的疑难杂症,从环境差异、版本兼容性、消费组冲突到源码解析,逐步揭秘Kafka消费机制,提供实用的故障定位与解决策略。

kafka的高性能、流式响应、对大数据支持是有目共睹的,在我们内部项目product-search有使用CDC的解决方案,而kafka成为该方案的输出源,此时各个业务方只需要去订阅消费即可。

1.  问题描述

有一天有开发人员找到我:我的consumer端消费kafka为何有时候可以消费到数据,有时候不行?dev环境我自己试了是没有问题的,而test环境就不行,不行的时候系统也没有任何报错信息,why?

2. 分析症状

说实话,每当遇到这样的问题(连个error都没有),内心是相当的囧,能怎么办?只能立刻马上冷静下来,然后去现场寻找蛛丝马迹。尴尬的时刻到了,了解下来故障现场确实没有留下什么迹象!但还是有些可疑点

3. 去伪求真

其实就是排除可疑点,巧合的事情就是最大的可能性。接下来就要按套路来解决问题:

1. kafka server是否真的数据?

很容易确认(借助一些kafka监控),基本可以排除

2. kafka client版本问题,使用方法问题,导致无法消费?

Spring for Apache Kafka Version

Spring Integration for Apache Kafka Version

kafka-clients

2.2.x

3.1.x

2.0.0, 2.1.0

2.1.x

3.0.x

1.0.x, 1.1.x, 2.0.0

2.0.x

3.0.x

0.11.0.x, 1.0.x

1.3.x

2.3.x

0.11.0.x, 1.0.x

1.2.x

2.2.x

0.10.2.x

1.1.x

2.1.x

0.10.0.x, 0.10.1.x

1.0.x

2.0.x

0.9.x.x

N/A*

1.3.x

0.8.2.2

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    <version>1.1.1.RELEASE</version>
</dependency>

application.yml(见下面)
spring:
    profiles: default
    kafka:
      bootstrap-servers: 192.168.1.183:9092,192.168.1.184:9092,192.168.1.182:9092
      consumer:
        group-id: opa-consumer-group
        auto-offset-reset: earliest
        enable-auto-commit: false
        auto-commit-interval: 100

@Component
public class KafkaReceiver {

    private static final String CDC_TOPIC = "BEEHIVE_XKDATA";
    private static final Logger LOGGER = LoggerFactory.getLogger(KafkaReceiver.class);

    @KafkaListener(topics = {CDC_TOPIC})
    public boolean consumer(ConsumerRecord<?, ?> record) {
        Optional<?> kafkaMessage = Optional.ofNullable(record.value());
        if (kafkaMessage.isPresent()) {
            Object message = kafkaMessage.get();
            //todo。。。略
        }
        return true;
    }
}

首先,版本是没有问题的,kafka driver可以直接去官方网站去确认,

用法(配置和代码),也是官方Samples,我擦这么简单的不可能有问题,基本上此项也可以排除。

 3. kafka client可能被别的consumer消费掉?

这个无法识别,需要对源码有一定了解的情况下才可以确认,懂源码不是让你改源码,更多是是让你埋日志(如果它有debug日志就打开,如果不够就把源码拉下来放自己项目中,加日志)。

<logger name="org.springframework.kafka.listener.adapter.RecordMessagingMessageListenerAdapter" level="DEBUG"/> 
<logger name="org.springframework.messaging.handler.invocation.InvocableHandlerMethod" level="trace"/>

基本上已经可以确认有问题!我发现:被bean A消费了,这个bean A(它继承了KafkaReceiver),bean A被别的类库扫描到了spring ioc中去了,那这又会有什么问题呢?后续源码会讲到,它不能这么做

4. 读源码

1. 要对kafka-driver有一个基本的认识,它是poll模型

2. 对spring-boot有一定了解的都知道,入口在spring.factories(spring-boot-autoconfigure),找到启动类org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\

3.看代码

KafkaProperties配置类
KafkaTemplate生产者生产消息用的,主要有send()
ProducerListener生产者监听接口,主要有onSuccess()、onError()。默认实现LoggingProducerListener,它打印错误消息
ConsumerFactory主要是createKafkaConsumer()
ProducerFactory主要是createProducer()
ConcurrentKafkaListenerContainerFactoryConfigurer配置ContainerProperties(包含了一个监听容器的运行时配置信息,主要定义了监听的主题、分区、初始化偏移量,还有消息监听器)
ConcurrentKafkaListenerContainerFactory创建createContainerInstance
KafkaBootstrapConfigurationKafkaListenerAnnotationBeanPostProcessor:处理各种注解@KafkaListener,它是核心,创建都是它驱动的
KafkaListenerEndpointRegistry:它实现SmartLifecycle(start、stop),掌管MessageListenerContainer的start和stop

4.1 MessageListener接口

实现onMessage方法,去消费接收到的消息。两种方案:

  • MessageListener 消费完消息后自动提交offset(enable.auto.commit=true时),可提高效率,存在消费失败但移动了偏移量的风险。
  • AcknowledgingMessageListener 消费完消息后手动提交offset(enable.auto.commit=false时)效率降低,无消费失败但移动偏移量的风险

MessagingMessageListenerAdapter的实现有:RecordMessagingMessageListenerAdapter、BatchMessagingMessageListenerAdapter,他们是由ConcurrentKafkaListenerContainerFactory创建的。

4.2 ConcurrentMessageListenerContainer核心类

它继承自抽象类AbstractMessageListenerContainer,核心方法doStart()可以创建多个consumer(KafkaMessageListenerContainer),实例化方式ConcurrentMessageListenerContainer(ConsumerFactory<K, V> consumerFactory,ContainerProperties containerProperties)。

4.3 执行流程

  1. 容器启动,KafkaListenerAnnotationBeanPostProcessor开始扫描并创建consumer和producter;
  2. kafkaConsumer拉取消息,首先Fetcher(请求获取器)获取请求并存储在unset中,ConsumerNetworkClient网络客户端执行poll(),调用NetWlrikClient的send()方法从unset中获取ClientRequest请求转成RequestSend最终塞进Selector的KafkaChannel通道中,Seletcor.send()从kafka集群拉取待消费数据ConsumerRecords。通过recordsToProcess(LinkedBlockingQueue)实现消息的的存储。
  3. 消费者监听器MessageListener.onMessage()执行用户自定义的实际消费业务逻辑。

4.4 KafkaMessageListenerContainer

上面讲过,每个consumer都有一个这样的实例,核心方法是start(),其实KafkaMessageListenerContainer通过内部类ListenerConsumer和提交监听消费者任务。

1.定义消费者订阅topic或者指定分区
2.设置监听器,支持4种:
  1)BatchAcknowledgingMessageListener批量需确认消息监听器
  2)AcknowledgingMessageListener需确认消息监听器
  3)BatchMessageListener批量消息监听器
  4)MessageListener消息监听器(用的最多,一次消费一条)

提交监听消费者任务(ListenerConsumer),返回Future并赋值。这里我们看一下任务Runnable接口的run方法,分两种情况:
1.如果自定义了分区,没必要再平衡分配分区了,直接回调
2.未指定分区,进入自旋消费

ListenerInvoker.startInvoker()触发onMessage(),

1.如果用户自定义了分区且非自动提交,那么开启异步线程执行ListenerInvoker任务。
2.未指定分区,进入自旋
1)拉取消费记录
ConsumerRecords<K, V> records = this.consumer.poll(this.containerProperties.getPollTimeout());
2)如果设置了自动提交,直接在当前线程执行
invokeListener(records);
3)否则发送消息进缓存队列
sendToListener(records)

<think>好的,我现在需要解决用户的问题,即在Ubuntu上基于JDK8安装Kafka,并使用Spring Boot 2.1.6、Spring Kafka 2.2.13和kafka-clients 2.2.1,启动一个生产者和同一个消费组的三个消费者(其中一个是通过concurrency=2产生的,另一个是独立进程),生产者循环发送特定消息,消费者批量消费并手动提交位移,最后关闭concurrency=2的消费者。我需要一步步理清步骤,并确保每个环节正确。 首先,用户的问题分为几个部分:安装Kafka、配置Spring Boot项目、设置生产者和消费者、验证批量消费和手动提交位移,最后关闭指定消费者。我需要逐一处理这些部分。 第一步是在Ubuntu上安装JDK8和Kafka。用户已经指定JDK8,所以需要确认Ubuntu上的JDK安装。然后下载Kafka,这里需要注意Kafka的版本是否兼容kafka-clients 2.2.1。Kafka 2.2.1版本应该对应kafka-clients 2.2.1,所以下载Kafka 2.2.1。安装ZooKeeper(Kafka依赖),启动ZooKeeper和Kafka服务。 接下来是Spring Boot项目的配置。Spring Boot 2.1.6对应的Spring Kafka版本需要是2.2.13,同时kafka-clients强制为2.2.1。需要在pom.xml中正确设置依赖,注意排除可能冲突的kafka-clients版本。 然后是生产者的配置。需要配置KafkaTemplate,设置bootstrap-servers,可能需要配置生产者的acks、retries等参数,但用户没有特别要求,所以保持默认或简单配置即可。生产者需要循环发送消息消息内容是“Hello Kafka,I’m W0111267”。 消费者部分需要同一消费组的三个消费者。其中一个消费者通过concurrency=2配置,这意味着该消费者会启动两个实例,属于同一个消费组。另一个消费者是独立进程,即另一个应用实例,同样属于同一消费组。这样总共有2(来自concurrency)+1(独立进程)=3个消费者。需要配置消费者的group-id,批量消费(设置max-poll-records),手动提交位移(设置enable-auto-commit为false,使用Acknowledgment手动提交)。 手动提交位移的实现需要在消费者方法中获取Acknowledgment对象,并调用acknowledge()方法。批量消费需要配置listener的batch-listener为true,这样消费者方法的参数是一个消息列表。 然后需要启动生产者应用,循环发送消息。启动消费者应用,其中一个应用的消费者配置了concurrency=2,另一个应用作为独立进程启动,同样的group-id。此时Kafka的消费组会有三个消费者实例。 最后,关闭concurrency=2的消费者,即停止对应的Spring Boot应用,此时剩下的独立进程消费者会继续消费消息。 需要注意的问题包括:依赖版本的兼容性(Spring Boot 2.1.6、Spring Kafka 2.2.13、kafka-clients 2.2.1是否兼容)、Kafka服务的正确启动(ZooKeeper和Kafka的端口是否正确,防火墙是否开放)、消费者组的配置是否正确(相同的group-id)、批量消费和手动提交的配置是否正确(batch-listener和enable-auto-commit的设置)、以及如何验证消费者实例的数量(通过Kafka命令行工具如kafka-consumer-groups.sh查看消费组的成员)。 现在需要详细步骤,确保每个环节正确。例如,安装JDK8时可能需要使用apt-get安装或手动下载,配置环境变量。安装Kafka时需要下载正确版本,配置server.properties中的zookeeper.connect,启动脚本的使用。Spring Boot项目的pom.xml依赖需要正确排除可能的kafka-clients版本冲突,确保使用2.2.1。生产者的配置类需要@Bean配置KafkaTemplate,消费者的配置需要@KafkaListener注解,设置concurrency、group-id、batch等属性,手动提交需要使用Acknowledgment。 另外,用户要求关闭concurrency=2的消费者,即停止运行该消费者的Spring Boot应用,此时消费组中的两个实例会被移除,剩下的独立进程消费者继续处理消息。需要验证这一点,可能通过发送消息观察消费情况,或者使用kafka-consumer-groups.sh查看活跃消费者数量。 可能遇到的问题包括:依赖冲突导致项目无法启动,需要检查pom.xml的依赖树;Kafka服务未正确启动,需要检查日志;消费者无法连接Kafka,需要检查bootstrap-servers配置;手动提交未正确实现,导致位移未提交,消息重复消费;批量消费未配置,导致每次只消费一条消息。 总结,需要分步骤详细说明安装、配置、代码实现和验证过程,确保用户能够按照步骤操作并达到目标。 </think>### 一、环境准备与 Kafka 安装(Ubuntu) #### 1. 安装 JDK 8 ```bash # 更新包索引 sudo apt update # 安装 OpenJDK 8 sudo apt install openjdk-8-jdk -y # 验证安装 java -version # 输出 "java version "1.8.0_xxx"" ``` #### 2. 安装并启动 Kafka 2.2.1(兼容 kafka-clients 2.2.1) ```bash # 下载 Kafka 2.2.1(Scala 2.12 版本) wget https://archive.apache.org/dist/kafka/2.2.1/kafka_2.12-2.2.1.tgz # 解压 tar -xzf kafka_2.12-2.2.1.tgz mv kafka_2.12-2.2.1 /opt/kafka # 启动 ZooKeeper(Kafka 依赖) cd /opt/kafka bin/zookeeper-server-start.sh -daemon config/zookeeper.properties # 启动 Kafka Broker(修改 config/server.properties 确保 listeners=PLAINTEXT://:9092) bin/kafka-server-start.sh -daemon config/server.properties # 验证服务(等待 10s 后检查进程) jps | grep -E "QuorumPeerMain|Kafka" # 应看到 ZooKeeper 和 Kafka 进程 ``` ### 二、Spring Boot 项目配置(2.1.6.RELEASE) #### 1. `pom.xml` 依赖(关键版本约束) ```xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>kafka-demo</artifactId> <version>1.0.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <spring-kafka.version>2.2.13.RELEASE</spring-kafka.version> <kafka-clients.version>2.2.1</kafka-clients.version> </properties> <dependencies> <!-- Spring Boot Web(用于启动应用) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Kafka(核心) --> <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</artifactId> <version>${spring-kafka.version}</version> </dependency> <!-- 强制指定 kafka-clients 版本 --> <dependency> <groupId>org.apache.kafka</groupId> <artifactId>kafka-clients</artifactId> <version>${kafka-clients.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> ``` ### 三、生产者配置与实现 #### 1. `application.yml` 配置 ```yaml spring: kafka: bootstrap-servers: localhost:9092 # Kafka 服务地址 producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer ``` #### 2. 生产者服务类(循环发送消息) ```java import org.springframework.kafka.core.KafkaTemplate; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component @EnableScheduling // 启用定时任务 public class KafkaProducer { private final KafkaTemplate<String, String> kafkaTemplate; public KafkaProducer(KafkaTemplate<String, String> kafkaTemplate) { this.kafkaTemplate = kafkaTemplate; } // 每 2 秒发送一条消息(工号替换为 W0111267) @Scheduled(fixedRate = 2000) public void sendMessage() { String message = "Hello Kafka,I’m W0111267"; kafkaTemplate.send("test-topic", message); // 发送到 "test-topic" System.out.println("发送消息: " + message); } } ``` ### 四、消费者配置与实现(批量消费+手动提交) #### 1. `application.yml` 消费者配置 ```yaml spring: kafka: consumer: group-id: my-consumer-group # 消费组 ID(关键:三个消费者需相同) enable-auto-commit: false # 禁用自动提交(手动提交) auto-offset-reset: earliest # 无偏移量时从最早消息开始 key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer properties: max.poll.records: 5 # 批量消费最大条数(一次拉取最多 5 条) listener: type: batch # 启用批量消费 concurrency: 2 # 消费者实例数(第一个消费者进程启动 2 个实例) ``` #### 2. 消费者服务类(手动提交位移) ```java import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.OffsetAndMetadata; import org.apache.kafka.common.TopicPartition; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.support.Acknowledgment; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Component public class KafkaConsumer { // 批量消费监听器(处理 "test-topic" 消息) @KafkaListener(topics = "test-topic", groupId = "my-consumer-group") public void batchConsume(List<ConsumerRecord<String, String>> records, Acknowledgment ack) { // 打印批量消息内容 records.forEach(record -> System.out.printf("消费者实例 %s 消费消息: %s%n", Thread.currentThread().getName(), record.value()) ); // 手动提交位移(提交最后一条消息的偏移量) Map<TopicPartition, OffsetAndMetadata> offsets = records.stream() .collect(Collectors.toMap( record -> new TopicPartition(record.topic(), record.partition()), record -> new OffsetAndMetadata(record.offset() + 1) )); ack.acknowledge(); // 提交所有分区的位移 System.out.println("手动提交位移完成,本次消费 " + records.size() + " 条消息"); } } ``` ### 五、启动与验证步骤 #### 1. 创建 Kafka 主题(手动或通过代码) ```bash # 命令行创建 "test-topic"(分区数 3,副本数 1) /opt/kafka/bin/kafka-topics.sh --create \ --zookeeper localhost:2181 \ --replication-factor 1 \ --partitions 3 \ --topic test-topic ``` #### 2. 启动生产者与消费者进程 | 进程类型 | 启动方式 | 说明 | |-------------------------|--------------------------------------------------------------------------|----------------------------------------------------------------------| | 生产者进程 | 运行 Spring Boot 主类(`@SpringBootApplication`) | 循环发送消息到 "test-topic" | | 消费者进程(concurrency=2) | 运行 Spring Boot 主类(默认启动 2 个消费者实例,属于 `my-consumer-group`) | 消费组中新增 2 个实例 | | 独立消费者进程 | 打包项目为 JAR,通过 `java -jar` 启动另一个实例(相同 `my-consumer-group`) | 消费组中新增 1 个实例(总共有 2+1=3 个消费者) | #### 3. 验证消费组状态(Kafka 命令行工具) ```bash # 查看消费组 "my-consumer-group" 的成员 /opt/kafka/bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \ --describe --group my-consumer-group # 预期输出(3 个消费者实例): # GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID # my-consumer-group test-topic 0 10 10 0 consumer-1-xxxx-xxxx /127.0.0.1 consumer-1 # my-consumer-group test-topic 1 10 10 0 consumer-2-xxxx-xxxx /127.0.0.1 consumer-2 # my-consumer-group test-topic 2 10 10 0 consumer-3-xxxx-xxxx /127.0.0.1 consumer-3 ``` #### 4. 关闭 concurrency=2 的消费者进程 直接终止运行该进程的 Spring Boot 应用(如通过 `Ctrl+C` 或关闭终端)。此时: - 消费组 `my-consumer-group` 中会移除 2 个实例(仅保留独立进程的 1 个消费者)。 - 未消费的消息会由剩余的 1 个消费者继续处理(Kafka 会重新分配分区)。 ### 六、关键注意事项 1. **依赖版本兼容**:Spring Kafka 2.2.13 需匹配 Spring Boot 2.1.6(通过 `spring-kafka.version` 显式指定),同时强制 `kafka-clients` 为 2.2.1(避免 Spring Boot 自动引入更高版本)。 2. **批量消费配置**:需同时设置 `spring.kafka.listener.type=batch`(启用批量监听器)和 `max.poll.records`(控制每次拉取的最大消息数)。 3. **手动提交位移**:通过 `Acknowledgment.acknowledge()` 提交位移,确保消息消费完成后再提交(避免消息丢失)。 4. **消费组负载均衡**:3 个消费者会自动分配到 3 个分区(假设主题分区数≥3),关闭 2 个消费者后,剩余 1 个消费者会接管所有分区的消费。
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值