Kafka 生产者消息丢失:ACK、重试与可靠发送

Kafka 生产者消息丢失:ACK、重试与可靠发送

生产者调用 send 方法,并不代表消息已经可靠写入 Kafka。网络抖动、Leader 切换、副本同步不足和错误的 ACK 配置,都可能让应用看到"已发送",最终却找不到消息。本文重点拆解 ACK 机制,并通过 Spring Kafka 配置说明如何提高发送可靠性。

语义纠正:acks=1 表示 Leader 副本写入成功后响应,并不要求其他副本完成同步;acks=all(或 -1)要求当前 ISR 中的副本满足确认条件。生产环境还应结合 enable.idempotence、retries、delivery.timeout.ms 与 min.insync.replicas 一起评估。

ACK 是什么

首先,我们得知道ack到底是啥,上节给大家讲手动提交offset的时候提到了ack,但没有展开讲,下面就给大家好好理一下。

Kafka中,ack(即Acknowledgement)是指消费者在成功消费一条消息后,向Kafka集群确认消息已经被成功消费的方式。Kafka提供了三种ACK级别

  • acks=0:生产者发送消息后,不等待任何确认,直接发送下一条消息。
  • acks=1:生产者发送消息后,等待leader节点成功写入消息后返回确认,然后发送下一条消息。
  • acks=all:生产者发送消息后,等待所有的follower节点和leader节点都成功写入消息后返回确认,然后发送下一条消息。

其中,acks=all级别提供了最高的消息可靠性,但会有较高的延迟。

大致流程如下:

生产者Kafka集群发送一条消息,并指定acks=allKafka集群中有一个leader节点两个follower节点。当leader节点收到消息后,它会将消息写入本地磁盘,并向follower节点发送写入请求。follower节点收到请求后,也会将消息写入本地磁盘并向leader节点返回确认。只有当所有节点都成功写入消息后,leader节点才会向生产者返回确认,表示消息已经被成功写入Kafka集群。生产者收到确认后,才会发送下一条消息。

这种ack机制保证了消息被成功写入Kafka集群后才会被认为是已经被消费,从而保证了消息的可靠性。

代码实践

下面我们就结合springboot代码给大家实际讲解一下,配置参数方式有很多,你可以在yml配置文件里添加,这里给大家介绍在配置类中进行配置的方式

新建一个KafkaProducerConfig

java 复制代码
@Configuration
public class KafkaProducerConfig {

    @Value("${spring.kafka.bootstrap-servers}")
    private String bootstrapServers;

    @Bean
    public Map<String, Object> producerConfigs() {
        Map<String, Object> props = new HashMap<>();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        props.put(ProducerConfig.ACKS_CONFIG, "1"); // 设置ack参数为1
        return props;
    }

    @Bean
    public ProducerFactory<String, String> producerFactory() {
        return new DefaultKafkaProducerFactory<>(producerConfigs());
    }

    @Bean
    public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
    }
}

在上面的代码中,我们创建了一个名为producerConfigsbean,其中包含了bootstrap.servers、key.serializer、value.serializeracks等配置项。然后,我们创建了一个名为producerFactorybean,并将producerConfigs传递给DefaultKafkaProducerFactory的构造方法。最后,我们创建了一个名为kafkaTemplatebean,并将producerFactory传递给它的构造方法。

当我们向Kafka发送消息时,KafkaTemplate将使用producerFactory创建一个KafkaProducer对象,并使用我们在producerConfigs中设置的配置项来配置KafkaProducer。我们将acks参数设置为1,表示生产者只需等待主题的分区副本收到消息即可确认发送成功。

消费者端,我们可以使用enable.auto.commit配置项来控制自动提交消费位移的行为,从而实现对消费消息的确认。

enable.auto.commit配置项为true时,消费者会自动提交消费位移,这意味着消费者无法控制何时确认消费消息。如果消费者在处理消息时发生错误,那么消费位移也会被提交,导致这些消息无法重新消费。因此,建议在需要保证消息可靠性的场景下关闭enable.auto.commit配置项。

enable.auto.commit配置项为false时,消费者需要手动提交消费位移,这样,消费者就可以在成功处理消息后再提交消费位移,从而实现对消费消息的确认。上节有带大家实现过

接着,看消费端的代码,新建一个AckController

java 复制代码
@Slf4j
@RestController
public class AckController {

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    /**
     * 手动提交偏移量
     */
    @GetMapping("/hello")
    public String hello() throws Exception {
        // 发送消息
        for (int i = 0; i < 10; i++) {
            String message = "Message " + i;
            kafkaTemplate.send("topic1", message);
            log.info("Sent message: {}", message);
            Thread.sleep(1000);
        }
        return "hello";
    }

    @KafkaListener(topics = "topic1", groupId = "my-group", containerFactory = "kafkaListenerContainerFactory")
    public void onMessage(ConsumerRecord<String, String> record, Acknowledgment acknowledgment) {
        log.info("listen Received message >>> {}", record);
        acknowledgment.acknowledge();
    }
}
plain 复制代码
2023-03-24 10:22:03.240  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 0
2023-03-24 10:22:03.250  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 87, CreateTime = 1679624523236, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 0)
2023-03-24 10:22:04.241  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 1
2023-03-24 10:22:04.255  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 88, CreateTime = 1679624524241, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 1)
2023-03-24 10:22:05.254  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 2
2023-03-24 10:22:05.258  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 89, CreateTime = 1679624525254, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 2)
2023-03-24 10:22:06.269  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 3
2023-03-24 10:22:06.274  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 90, CreateTime = 1679624526269, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 3)
2023-03-24 10:22:07.277  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 4
2023-03-24 10:22:07.285  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 91, CreateTime = 1679624527277, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 4)
2023-03-24 10:22:08.292  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 5
2023-03-24 10:22:08.296  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 92, CreateTime = 1679624528292, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 5)
2023-03-24 10:22:09.304  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 6
2023-03-24 10:22:09.308  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 93, CreateTime = 1679624529304, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 6)
2023-03-24 10:22:10.317  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 7
2023-03-24 10:22:10.321  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 94, CreateTime = 1679624530317, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 7)
2023-03-24 10:22:11.331  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 8
2023-03-24 10:22:11.335  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 95, CreateTime = 1679624531331, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 8)
2023-03-24 10:22:12.346  INFO 24584 --- [nio-8081-exec-1] c.kafka.study.controller.AckController   : Sent message: Message 9
2023-03-24 10:22:12.349  INFO 24584 --- [ntainer#0-0-C-1] c.kafka.study.controller.AckController   : listen Received message >>> ConsumerRecord(topic = topic1, partition = 0, offset = 96, CreateTime = 1679624532345, serialized key size = -1, serialized value size = 9, headers = RecordHeaders(headers = [], isReadOnly = false), key = null, value = Message 9)

需要注意的是,手动提交消费位移会降低消费者的处理速度,因为每次处理完一条消息后都需要提交位移。因此,在需要高吞吐量的场景下,建议使用批量提交消费位移的方式来提高消费者的处理速度。

批量提交消费位移

其实很简单,首先需要改下KafkaConfig,在kafkaListenerContainerFactory添加如下代码:

java 复制代码
//设置为批量消费,每个批次数量在Kafka配置参数中设置ConsumerConfig.MAX_POLL_RECORDS_CONFIG
factory.setBatchListener(true);
// 设置消息转换器
factory.setMessageConverter(new StringJsonMessageConverter());

在上述代码中,设置了一个消息转换器StringJsonMessageConverter,用于将JSON格式的消息转换为Java对象。这里我们使用了Spring Kafka提供的默认消息转换器

更改消费端:

java 复制代码
/**
     * 批量提交消费位移
     * @param records
     * @param acknowledgment
     */
@KafkaListener(topics = "topic1", groupId = "my-group", containerFactory = "kafkaListenerContainerFactory")
public void onMessage(List<ConsumerRecord<String, String>> records, Acknowledgment acknowledgment) {
    for (ConsumerRecord<String, String> record : records) {
        String message = record.value();
        // 处理消息
        log.info("onMessage: {}", message);
    }
    acknowledgment.acknowledge();
}

需要注意的是,使用批量处理消息时,需要确保在处理完所有消息后再调用acknowledge方法提交消费位移,否则可能会出现一些消息重复消费丢失的情况。

总结

可靠发送不能只依赖某一个参数。ACK 决定确认条件,幂等生产者减少重试导致的重复,副本与最小同步副本数决定 Broker 端的容错底线,发送回调与监控则负责暴露失败。把这些环节组合起来,才能真正降低生产者侧的消息丢失概率。

相关推荐
heimeiyingwang2 小时前
【中台·技术篇】消息队列中台:Kafka/RocketMQ 统一管理与多租户隔离
kafka·rocketmq·中台
程序员天天困21 小时前
Kafka 接入 AI 的三条路线:MCP 提案、会话记忆与实时上下文
大数据·后端·kafka
hey you~2 天前
云客服多渠道统一接入,消息队列技术实现方案
kafka·消息队列·rocketmq·系统集成·云客服·多渠道接入·接口对接
雾隐隐o2 天前
Kafka 消费者消息丢失:Offset 原理与解决方案
kafka
clz13145213 天前
Kafka 日消 10 亿场景:用 ConcurrentLinkedQueue 实现高性能批量消费缓冲
分布式·kafka·linq
(Charon)4 天前
【Kafka】消息队列学习(一):为什么需要Kafka?从消息队列到整体架构
学习·架构·kafka
clz13145214 天前
风险特征系统 EPC 子系统:基于 Kafka 数据源与数据集配置的 Flink 指标清洗加工
分布式·flink·kafka
富士康质检员张全蛋4 天前
Kafka实战 生产阶段 消费阶段的拦截器
kafka