Kafka生产Bug记录:CommitFailedException
问题背景
在一次Kafka消费过程中,发现消费端出现了多次同一个消息被推送给后台离线处理平台的情况,而且消息消费速度逐渐变慢。通过查看生产日志,发现存在重复消费的ID,并且这些重复消息的offset完全相同。
初步分析
起初怀疑是后台处理数据失败,导致调度对失败的消息进行重试。然而,如果是重试的话,发送到Kafka的消息offset应该会不同,但日志显示重复消费的消息ID和offset完全一致。查看日志后面的错误信息,发现了如下错误:
CommitFailedException: offset commit cannot be completed since the consumer is not part of an active group.
错误分析
CommitFailedException
是Kafka中的一个异常,通常在消费者提交offset失败时抛出。消费者提交offset是为了记录已经处理的消息,以便在故障或重启后能够从正确的位置继续消费。如果提交offset失败,可能会导致消息被重复消费或未处理的消息被跳过。
当消费者重复提交相同的offset时,Kafka消费者日志可能会记录以下警告信息:
Consumer attempted to commit an offset, but the consumer group had already rebalanced, causing the commit to fail.
这表明消费者尝试提交相同的offset,但由于消费者组已经重新平衡,提交失败。这通常表示消费者在处理消息时花费的时间过长,导致Kafka认为消费者已死亡并重新平衡消费者组。
解决措施
为了应对这一问题,可以考虑以下解决措施:
-
优化消费者端代码以加快消息处理速度
-
增加
max.poll.interval.ms
配置值,以便消费者有更长的时间来处理消息,例如:spring.kafka.consumer.max.poll.interval.ms=500000
这可以根据接口处理业务逻辑的时间来调整。
-
设置
max.poll.records
值,控制每次拉取的消息数量,例如:spring.kafka.consumer.max.poll.records=3
可以根据业务执行时间来设置每次拉取的消息数量。
总结
CommitFailedException
异常通常由以下几个原因引起:
- 超时:如果消费者在规定时间内没有提交offset,就会抛出
CommitFailedException
异常。这通常是因为消费者处理消息时间太长或提交offset频率太低,导致消费者被视为死亡并从消费者组中移除,提交offset超时。 - 重复提交:如果消费者在短时间内多次提交相同的offset,就会抛出
CommitFailedException
异常。这可能是由于消费者代码中的错误或线程同步问题导致的。 - 重启:如果消费者重启,它将从消费者组中删除并重新加入消费者组,此时之前提交的offset将不再有效。
- 资源不足:如果Kafka集群没有足够的资源处理消费者提交的offset,就会抛出
CommitFailedException
异常,例如Kafka集群已达到最大负载。 - Kafka集群故障:如果Kafka集群出现故障,例如领导者节点崩溃或磁盘故障,可能导致提交offset失败并抛出
CommitFailedException
异常。 - 其他消费者重复提交:如果另一个消费者提交了相同的offset,那么自己的消费者就无法提交offset。为了避免消息丢失,Kafka会拒绝重复提交相同的offset。
如果在开发中遇到CommitFailedException
异常,请检查消费者提交offset的频率和代码,确保消费者在规定时间内提交offset,并避免重复提交相同的offset。如果问题仍然存在,请检查Kafka集群的健康状况,确保集群有足够的资源处理消费者提交的offset。可以考虑上面提到的配置文件进行处理。
另外使用手动提交(acknowledgment)机制可以帮助避免 CommitFailedException
,特别是在消息处理时间较长的情况下。手动提交允许你在消息成功处理后再提交位移,而不是依赖于Kafka客户端的自动提交机制。
什么是 CommitFailedException
通过手动提交机制,你可以在每条消息处理成功后手动提交位移,确保只有在成功处理后才提交,从而避免处理失败时的位移提交问题。
手动提交机制
在Spring Kafka中,可以通过设置消费者的 ackMode
和使用 Acknowledgment
对象来实现手动提交。
配置手动提交
- 配置类
在消费者配置类中设置 ackMode
为 MANUAL_IMMEDIATE
或 MANUAL
:
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import java.util.HashMap;
import java.util.Map;
@EnableKafka
@Configuration
public class KafkaConsumerConfig {
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
configProps.put(ConsumerConfig.GROUP_ID_CONFIG, "my-group");
configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
return new DefaultKafkaConsumerFactory<>(configProps);
}
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
}
- 使用
Acknowledgment
在消费者方法中使用 Acknowledgment
对象进行手动提交:
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.stereotype.Service;
@Service
public class KafkaConsumerService {
@KafkaListener(topics = "my-topic", groupId = "my-group")
public void listen(String message, Acknowledgment acknowledgment) {
try {
// 处理消息
System.out.println("Received Message: " + message);
// 手动提交偏移量
acknowledgment.acknowledge();
} catch (Exception e) {
// 处理异常
System.err.println("Error processing message: " + e.getMessage());
}
}
}
总结
通过配置手动提交机制,可以在消息成功处理后手动提交位移,从而避免 CommitFailedException
。这对于处理时间较长的任务特别有用,因为你可以确保只有在消息处理成功后才提交位移,而不是依赖于Kafka客户端的自动提交机制。这种方式可以提高系统的可靠性,确保消息处理过程中的异常不会导致位移提交失败。
本文来自博客园,作者:茄子_2008,转载请注明原文链接:https://www.cnblogs.com/xd502djj/p/18296701