簡體   English   中英

Spring Kafka 事務導致生產者每條消息的偏移量增加了兩個

[英]Spring Kafka transaction causes producer per message offset increased by two

我在使用 Spring(boot) Kafka 的微服務中有一個消費-轉換-生產工作流。 我需要實現 Kafka 事務提供的恰好一次語義。 這是下面的代碼片段:

配置

@Bean
public ProducerFactory<String, String> producerFactory() {
    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.ENABLE_IDEMPOTENCE_CONFIG, true);
    props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, 1024 * 1024);
    DefaultKafkaProducerFactory<String, String> defaultKafkaProducerFactory = new DefaultKafkaProducerFactory<>(props);
    defaultKafkaProducerFactory.setTransactionIdPrefix("kafka-trx-");
    return defaultKafkaProducerFactory;
}

@Bean
public ConsumerFactory<String, String> consumerFactory() {
    Map<String, Object> props = new HashMap<>();
    props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
    props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
    props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 5000);
    props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
    props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
    props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
    return new DefaultKafkaConsumerFactory<>(props);
}

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

@Bean
public KafkaTransactionManager<String, String> kafkaTransactionManager() {
    return new KafkaTransactionManager<>(producerFactory());
}

@Bean
@Qualifier("chainedKafkaTransactionManager")
public ChainedKafkaTransactionManager<String, Object> chainedKafkaTransactionManager(KafkaTransactionManager<String, String> kafkaTransactionManager) {
    return new ChainedKafkaTransactionManager<>(kafkaTransactionManager);
}

@Bean
public ConcurrentKafkaListenerContainerFactory<?, ?> concurrentKafkaListenerContainerFactory(ChainedKafkaTransactionManager<String, Object> chainedKafkaTransactionManager) {
    ConcurrentKafkaListenerContainerFactory<String, String> concurrentKafkaListenerContainerFactory = new ConcurrentKafkaListenerContainerFactory<>();
    concurrentKafkaListenerContainerFactory.setConsumerFactory(consumerFactory());
    concurrentKafkaListenerContainerFactory.setBatchListener(true);
    concurrentKafkaListenerContainerFactory.setConcurrency(nexusConsumerConcurrency);
    //concurrentKafkaListenerContainerFactory.setReplyTemplate(kafkaTemplate());
    concurrentKafkaListenerContainerFactory.getContainerProperties().setAckMode(AbstractMessageListenerContainer.AckMode.BATCH);
    concurrentKafkaListenerContainerFactory.getContainerProperties().setTransactionManager(chainedKafkaTransactionManager);
    return concurrentKafkaListenerContainerFactory;
}

聽眾

@KafkaListener(topics = "${kafka.xxx.consumerTopic}", groupId = "${kafka.xxx.consumerGroup}", containerFactory = "concurrentKafkaListenerContainerFactory")
public void listen(@Payload List<String> msgs, @Header(KafkaHeaders.RECEIVED_PARTITION_ID) List<Integer> partitions, @Header(KafkaHeaders.OFFSET) List<Integer> offsets) {

    int i = -1;
    for (String msg : msgs) {
        ++i;
        LOGGER.debug("partition={}; offset={}; msg={}", partitions.get(i), offsets.get(i), msg);
        String json = transform(msg);
        kafkaTemplate.executeInTransaction(kt -> kt.send(producerTopic, json));
    }
}

但是在產品環境中,我遇到了一個奇怪的問題。 生產者發送的每條消息的偏移量增加兩個,消費者不提交消費偏移量。

來自 topic1 的消費者抵消topic1 的消費者抵消

Topic1 消費者詳情

Topic1 消費者詳情

生產到topic2

生產到topic2

然而,生產者發送的消息數量與消費的數量相同。 生產者的下游可以持續接收來自 topic2 的消息。 日志中沒有發現錯誤或異常。

我想知道為什么消費-轉換-生產工作流程看起來沒問題(也保證了恰好一次的語義),但是消費的偏移量沒有提交,並且產生的 msg 偏移量增量是 2,而不是每個單個 msg 的 1。

如何解決? 謝謝!

這就是它的設計方式。 Kafka 日志是不可變的,因此在事務結束時使用一個額外的“槽”來指示事務是提交還是回滾。 這允許具有read_committed隔離級別的使用者跳過回滾事務。

如果你在一個事務中發布 10 條記錄,你會看到 offset 增加了 11。如果你只發布了一條,它會增加 2。

如果您希望發布參與消費者啟動的事務(僅一次),則不應使用executeInTransaction 這將開始一個新的事務。

/**
 * Execute some arbitrary operation(s) on the operations and return the result.
 * The operations are invoked within a local transaction and do not participate
 * in a global transaction (if present).
 * @param callback the callback.
 * @param <T> the result type.
 * @return the result.
 * @since 1.1
 */
<T> T executeInTransaction(OperationsCallback<K, V, T> callback);

我不明白為什么消費者偏移量不會仍然發送到消費者啟動的交易。 您應該打開 DEBUG 日志記錄以查看發生了什么(如果修復模板代碼后仍然發生)。

編輯

監聽器退出時,監聽器容器將消耗的偏移量(+1)發送給事務; 打開提交日志,你會看到它......

@SpringBootApplication
public class So59152915Application {

    public static void main(String[] args) {
        SpringApplication.run(So59152915Application.class, args);
    }

    @Autowired
    private KafkaTemplate<String, String> template;

    @KafkaListener(id = "foo", topics = "so59152915-1", clientIdPrefix = "so59152915")
    public void listen1(String in, @Header(KafkaHeaders.OFFSET) long offset) throws InterruptedException {
        System.out.println(in + "@" + offset);
        this.template.send("so59152915-2", in.toUpperCase());
        Thread.sleep(2000);
    }

    @KafkaListener(id = "bar", topics = "so59152915-2")
    public void listen2(String in) {
        System.out.println(in);
    }

    @Bean
    public NewTopic topic1() {
        return new NewTopic("so59152915-1", 1, (short) 1);
    }

    @Bean
    public NewTopic topic2() {
        return new NewTopic("so59152915-2", 1, (short) 1);
    }

    @Bean
    public ApplicationRunner runner(KafkaListenerEndpointRegistry registry) {
        return args -> {
            this.template.executeInTransaction(t -> {
                IntStream.range(0, 11).forEach(i -> t.send("so59152915-1", "foo" + i));
                try {
                    System.out.println("Hit enter to commit sends");
                    System.in.read();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            });
        };
    }

}

@Component
class Configurer {

    Configurer(ConcurrentKafkaListenerContainerFactory<?, ?> factory) {
        factory.getContainerProperties().setCommitLogLevel(Level.INFO);
    }

}

spring.kafka.producer.transaction-id-prefix=tx-
spring.kafka.consumer.properties.isolation.level=read_committed
spring.kafka.consumer.auto-offset-reset=earliest

foo0@56
2019-12-04 10:07:18.551  INFO 55430 --- [      foo-0-C-1] essageListenerContainer$ListenerConsumer : Sending offsets to transaction: {so59152915-1-0=OffsetAndMetadata{offset=57, leaderEpoch=null, metadata=''}}
foo1@57
FOO0
2019-12-04 10:07:18.558  INFO 55430 --- [      bar-0-C-1] essageListenerContainer$ListenerConsumer : Sending offsets to transaction: {so59152915-2-0=OffsetAndMetadata{offset=63, leaderEpoch=null, metadata=''}}
2019-12-04 10:07:20.562  INFO 55430 --- [      foo-0-C-1] essageListenerContainer$ListenerConsumer : Sending offsets to transaction: {so59152915-1-0=OffsetAndMetadata{offset=58, leaderEpoch=null, metadata=''}}
foo2@58

請注意您的自動提交設置。 正如我所見,您將其設置為 false:

props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);

因此,在這種情況下,您需要“手動”提交或將自動提交設置為 true。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM