Я пытаюсь проверить свою конфигурацию ровно один раз, чтобы убедиться, что все установленные мной конфигурации верны и поведение такое, как я ожидал.

Кажется, у меня возникла проблема с дублированием отправки

    public static void main(String[] args) {
    MessageProducer producer = new ProducerBuilder()
            .setBootstrapServers("kafka:9992")
            .setKeySerializerClass(StringSerializer.class)
            .setValueSerializerClass(StringSerializer.class)
            .setProducerEnableIdempotence(true).build();
    MessageConsumer consumer = new ConsumerBuilder()
            .setBootstrapServers("kafka:9992")
            .setIsolationLevel("read_committed")
            .setTopics("someTopic2")
            .setGroupId("bla")
            .setKeyDeserializerClass(StringDeserializer.class)
            .setValueDeserializerClass(MapDeserializer.class)
            .setConsumerMessageLogic(new ConsumerMessageLogic() {
                @Override
                public void onMessage(ConsumerRecord cr, Acknowledgment acknowledgment) {
                    producer.sendMessage(new TopicPartition("someTopic2", cr.partition()),
                            new OffsetAndMetadata(cr.offset() + 1),"something1", "im in transaction", cr.key());
                    acknowledgment.acknowledge();
                }
            }).build();
    consumer.start();
}

Это мой «тест», вы можете предположить, что строитель выставляет правильную конфигурацию.

ConsumerMessageLogic - это класс, который обрабатывает "процессную" часть чтения-процесса-записи, которую поддерживает семантика с точностью до одного раза.

Внутри класса производителя у меня есть метод отправки сообщения, например:

    public void sendMessage(TopicPartition topicPartition, OffsetAndMetadata offsetAndMetadata,String sendToTopic, V message, PK partitionKey) {
    try {
        KafkaRecord<PK, V> partitionAndMessagePair = producerMessageLogic.prepareMessage(topicPartition.topic(), partitionKey, message);
        if(kafkaTemplate.getProducerFactory().transactionCapable()){
            kafkaTemplate.executeInTransaction(operations -> {
                sendMessage(message, partitionKey, sendToTopic, partitionAndMessagePair, operations);
                operations.sendOffsetsToTransaction(
                        Map.of(topicPartition, offsetAndMetadata),"bla");
                return true;
            });

        }else{
            sendMessage(message, partitionKey, topicPartition.topic(), partitionAndMessagePair, kafkaTemplate);
        }
    }catch (Exception e){
        failureHandler.onFailure(partitionKey, message, e);
    }
}

Я создаю своего потребителя так:

    /**
 * Start the message consumer
 * The record event will be delegate on the onMessage()
 */
public void start() {
    initConsumerMessageListenerContainer();
    container.start();
}

/**
 * Initialize the kafka message listener
 */
private void initConsumerMessageListenerContainer() {
    // start a acknowledge message listener to allow the manual commit
    messageListener = consumerMessageLogic::onMessage;

    // start and initialize the consumer container
    container = initContainer(messageListener);

    // sets the number of consumers, the topic partitions will be divided by the consumers
    container.setConcurrency(springConcurrency);
    springContainerPollTimeoutOpt.ifPresent(p -> container.getContainerProperties().setPollTimeout(p));
    if (springAckMode != null) {
        container.getContainerProperties().setAckMode(springAckMode);
    }
}

private ConcurrentMessageListenerContainer<PK, V> initContainer(AcknowledgingMessageListener<PK, V> messageListener) {
    return new ConcurrentMessageListenerContainer<>(
            consumerFactory(props),
            containerProperties(messageListener));
}

Когда я создаю своего производителя, я создаю его с UUID в качестве префикса транзакции, например

public ProducerFactory<PK, V> producerFactory(boolean isTransactional) {
    ProducerFactory<PK, V> res = new DefaultKafkaProducerFactory<>(props);
    if(isTransactional){
        ((DefaultKafkaProducerFactory<PK, V>) res).setTransactionIdPrefix(UUID.randomUUID().toString());
        ((DefaultKafkaProducerFactory<PK, V>) res).setProducerPerConsumerPartition(true);
    }
    return res;
}

Теперь, когда все настроено, я поднимаю 2 экземпляра по теме с 2 разделами, каждый экземпляр получает 1 раздел из потребляемой темы.

Я отправляю сообщение и жду отладки для тайм-аута транзакции (для имитации потери соединения) в экземпляре A, как только тайм-аут проходит, другой экземпляр (экземпляр B) автоматически обрабатывает запись и отправляет ее в целевую тему, вызывая повторный баланс произошел

Все идет нормально. Теперь, когда я освобождаю точку останова на экземпляре A, он говорит о перебалансировке и не может выполнить фиксацию, но я все еще вижу другую выходную запись в моей целевой теме.

Я ожидал, что экземпляр A не продолжит свою работу, как только я отпущу точку останова, поскольку запись уже была обработана.

Я делаю что-то неправильно? Возможен ли этот сценарий?

Редактировать 2:

После того, как garys отмечает выполнение в транзакции, я получаю дублирующую запись, если я замораживаю один из экземпляров до тайм-аута и освобождаю его после того, как другой экземпляр обработал запись, затем замороженный экземпляр обрабатывает и создает ту же запись в выходной теме ...

 public static void main(String[] args) {
    MessageProducer producer = new ProducerBuilder()
            .setBootstrapServers("kafka:9992")
            .setKeySerializerClass(StringSerializer.class)
            .setValueSerializerClass(StringSerializer.class)
            .setProducerEnableIdempotence(true).build();


        MessageConsumer consumer = new ConsumerBuilder()
                .setBootstrapServers("kafka:9992")
                .setIsolationLevel("read_committed")
                .setTopics("someTopic2")
                .setGroupId("bla")
                .setKeyDeserializerClass(StringDeserializer.class)
                .setValueDeserializerClass(MapDeserializer.class)
                .setConsumerMessageLogic(new ConsumerMessageLogic() {
                    @Override
                    public void onMessage(ConsumerRecord cr, Acknowledgment acknowledgment) {
                        producer.sendMessage("something1", "im in transaction");
                    }
                }).build();
        consumer.start(producer.getProducerFactory());
}

Новый метод sendMessage в производителе без executeInTransaction

public void sendMessage(V message, PK partitionKey, String topicName) {

    try {
        KafkaRecord<PK, V> partitionAndMessagePair = producerMessageLogic.prepareMessage(topicName, partitionKey, message);
        sendMessage(message, partitionKey, topicName, partitionAndMessagePair, kafkaTemplate);
    }catch (Exception e){
        failureHandler.onFailure(partitionKey, message, e);
    }
}

А также я изменил создание контейнера потребителя, чтобы иметь диспетчер транзакций с тем же производителем, что и предложенный

/**
 * Initialize the kafka message listener
 */
private void initConsumerMessageListenerContainer(ProducerFactory<PK,V> producerFactory) {
    // start a acknowledge message listener to allow the manual commit
    acknowledgingMessageListener = consumerMessageLogic::onMessage;

    // start and initialize the consumer container
    container = initContainer(acknowledgingMessageListener, producerFactory);

    // sets the number of consumers, the topic partitions will be divided by the consumers
    container.setConcurrency(springConcurrency);
    springContainerPollTimeoutOpt.ifPresent(p -> container.getContainerProperties().setPollTimeout(p));
    if (springAckMode != null) {
        container.getContainerProperties().setAckMode(springAckMode);
    }
}

private ConcurrentMessageListenerContainer<PK, V> initContainer(AcknowledgingMessageListener<PK, V> messageListener, ProducerFactory<PK,V> producerFactory) {
    return new ConcurrentMessageListenerContainer<>(
            consumerFactory(props),
            containerProperties(messageListener, producerFactory));
}

 @NonNull
private ContainerProperties containerProperties(MessageListener<PK, V> messageListener, ProducerFactory<PK,V> producerFactory) {
    ContainerProperties containerProperties = new ContainerProperties(topics);
    containerProperties.setMessageListener(messageListener);
    containerProperties.setTransactionManager(new KafkaTransactionManager<>(producerFactory));
    return containerProperties;
}

Я ожидаю, что брокер, получив обработанную запись из замороженного экземпляра, будет знать, что эта запись уже обрабатывалась другим экземпляром, поскольку она содержит точно такие же метаданные (или это? Я имею в виду, что PID будет другим , а должно ли быть иначе?)

Может быть, сценарий, который я ищу, даже не поддерживается в текущем, когда поддержка kafka и spring предоставляет ...

Если у меня есть 2 экземпляра чтения-процесса-записи - это означает, что у меня есть 2 производителя с 2 разными PID.

Теперь, когда я замораживаю один из экземпляров, когда размороженный экземпляр получает ответственность за процесс записи из-за перебалансировки, он отправляет запись со своим собственным PID и последовательностью в метаданных.

Теперь, когда я освобождаю замороженный экземпляр, он отправляет ту же запись, но со своим собственным PID, поэтому брокер никак не узнает его дубликат ...

Я ошибся? как я могу избежать этого сценария? я, хотя повторная балансировка останавливает экземпляр и не позволяет ему завершить свой процесс (где он создает дублирующую запись), потому что он больше не несет ответственности за эту запись

Добавление журналов: замороженный экземпляр: вы можете увидеть время зависания в 10:53:34, и я выпустил его в 10:54:02 (время ребалансировки составляет 10 секунд)

2020-06-16 10:53:34,393 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.c.DefaultKafkaProducerFactory.debug:296] 
Created new Producer: CloseSafeProducer 
[delegate=org.apache.kafka.clients.producer.KafkaProducer@5c7f5906]
2020-06-16 10:53:34,394 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.c.DefaultKafkaProducerFactory.debug:296] 
CloseSafeProducer 
[delegate=org.apache.kafka.clients.producer.KafkaProducer@5c7f5906] 
beginTransaction()
2020-06-16 10:53:34,395 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.t.KafkaTransactionManager.doBegin:149] Created 
Kafka transaction on producer [CloseSafeProducer 
[delegate=org.apache.kafka.clients.producer.KafkaProducer@5c7f5906]]
2020-06-16 10:54:02,157 INFO  [${sys:spring.application.name}] [kafka- 
coordinator-heartbeat-thread | bla] 
[o.a.k.c.c.i.AbstractCoordinator.:] [Consumer clientId=consumer-bla-1,      
groupId=bla] Group coordinator X.X.X.X:9992 (id: 2147482646 rack: 
null) is unavailable or invalid, will attempt rediscovery
2020-06-16 10:54:02,181 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] 
[o.s.k.l.KafkaMessageListenerContainer$ListenerConsumer.debug:296] 
Sending offsets to transaction: {someTopic2- 
0=OffsetAndMetadata{offset=23, leaderEpoch=null, metadata=''}}
2020-06-16 10:54:02,189 INFO  [${sys:spring.application.name}] [kafka- 
producer-network-thread | producer-b76e8aba-8149-48f8-857b- 
a19195f5a20abla.someTopic2.0] [i.i.k.s.p.SimpleSuccessHandler.:] Sent 
message=[im in transaction] with offset=[252] to topic something1
2020-06-16 10:54:02,193 INFO  [${sys:spring.application.name}] [kafka- 
producer-network-thread | producer-b76e8aba-8149-48f8-857b- 
a19195f5a20abla.someTopic2.0] [o.a.k.c.p.i.TransactionManager.:] 
[Producer clientId=producer-b76e8aba-8149-48f8-857b- 
a19195f5a20abla.someTopic2.0, transactionalId=b76e8aba-8149-48f8-857b- 
a19195f5a20abla.someTopic2.0] Discovered group coordinator 
X.X.X.X:9992 (id: 1001 rack: null)
2020-06-16 10:54:02,263 INFO  [${sys:spring.application.name}] [kafka- 
coordinator-heartbeat-thread | bla] 
[o.a.k.c.c.i.AbstractCoordinator.:] [Consumer clientId=consumer-bla-1, 
groupId=bla] Discovered group coordinator 192.168.144.1:9992 (id: 
2147482646 rack: null)
2020-06-16 10:54:02,295 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.t.KafkaTransactionManager.processCommit:740] 
Initiating transaction commit
2020-06-16 10:54:02,296 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.c.DefaultKafkaProducerFactory.debug:296] 
CloseSafeProducer 
[delegate=org.apache.kafka.clients.producer.KafkaProducer@5c7f5906] 
commitTransaction()
2020-06-16 10:54:02,299 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] 
[o.s.k.l.KafkaMessageListenerContainer$ListenerConsumer.debug:296] 
Commit list: {}
2020-06-16 10:54:02,301 INFO  [${sys:spring.application.name}] 
[consumer-0-C-1] [o.a.k.c.c.i.AbstractCoordinator.:] [Consumer 
clientId=consumer-bla-1, groupId=bla] Attempt to heartbeat failed for 
since member id consumer-bla-1-b3ad1c09-ad06-4bc4-a891-47a2288a830f is 
not valid.
2020-06-16 10:54:02,302 INFO  [${sys:spring.application.name}] 
[consumer-0-C-1] [o.a.k.c.c.i.ConsumerCoordinator.:] [Consumer 
clientId=consumer-bla-1, groupId=bla] Giving away all assigned 
partitions as lost since generation has been reset,indicating that 
consumer is no longer part of the group
2020-06-16 10:54:02,302 INFO  [${sys:spring.application.name}] 
[consumer-0-C-1] [o.a.k.c.c.i.ConsumerCoordinator.:] [Consumer 
clientId=consumer-bla-1, groupId=bla] Lost previously assigned 
partitions someTopic2-0
2020-06-16 10:54:02,302 INFO  [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.l.ConcurrentMessageListenerContainer.info:279] 
bla: partitions lost: [someTopic2-0]
2020-06-16 10:54:02,303 INFO  [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.l.ConcurrentMessageListenerContainer.info:279] 
bla: partitions revoked: [someTopic2-0]
2020-06-16 10:54:02,303 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] 
[o.s.k.l.KafkaMessageListenerContainer$ListenerConsumer.debug:296] 
Commit list: {}

Обычный экземпляр, который берет на себя раздел и создает запись после перебалансировки

2020-06-16 10:53:46,536 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.c.DefaultKafkaProducerFactory.debug:296] 
Created new Producer: CloseSafeProducer 
[delegate=org.apache.kafka.clients.producer.KafkaProducer@26c76153]
2020-06-16 10:53:46,537 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.c.DefaultKafkaProducerFactory.debug:296] 
CloseSafeProducer 
[delegate=org.apache.kafka.clients.producer.KafkaProducer@26c76153] 
beginTransaction()
2020-06-16 10:53:46,539 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.t.KafkaTransactionManager.doBegin:149] Created 
Kafka transaction on producer [CloseSafeProducer 
[delegate=org.apache.kafka.clients.producer.KafkaProducer@26c76153]]
2020-06-16 10:53:46,556 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] 
[o.s.k.l.KafkaMessageListenerContainer$ListenerConsumer.debug:296] 
Sending offsets to transaction: {someTopic2- 
0=OffsetAndMetadata{offset=23, leaderEpoch=null, metadata=''}}
2020-06-16 10:53:46,563 INFO  [${sys:spring.application.name}] [kafka- 
producer-network-thread | producer-1d8e74d3-8986-4458-89b7- 
6d3e5756e213bla.someTopic2.0] [i.i.k.s.p.SimpleSuccessHandler.:] Sent 
message=[im in transaction] with offset=[250] to topic something1
2020-06-16 10:53:46,566 INFO  [${sys:spring.application.name}] [kafka-        
producer-network-thread | producer-1d8e74d3-8986-4458-89b7- 
6d3e5756e213bla.someTopic2.0] [o.a.k.c.p.i.TransactionManager.:] 
[Producer clientId=producer-1d8e74d3-8986-4458-89b7- 
6d3e5756e213bla.someTopic2.0, transactionalId=1d8e74d3-8986-4458-89b7- 
6d3e5756e213bla.someTopic2.0] Discovered group coordinator 
X.X.X.X:9992 (id: 1001 rack: null)
2020-06-16 10:53:46,668 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.t.KafkaTransactionManager.processCommit:740] 
Initiating transaction commit
2020-06-16 10:53:46,669 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] [o.s.k.c.DefaultKafkaProducerFactory.debug:296] 
CloseSafeProducer 
[delegate=org.apache.kafka.clients.producer.KafkaProducer@26c76153] 
commitTransaction()
2020-06-16 10:53:46,672 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] 
[o.s.k.l.KafkaMessageListenerContainer$ListenerConsumer.debug:296] 
Commit list: {}
2020-06-16 10:53:51,673 DEBUG [${sys:spring.application.name}] 
[consumer-0-C-1] 
[o.s.k.l.KafkaMessageListenerContainer$ListenerConsumer.debug:296] 
Received: 0 records

Я заметил, что они оба отмечают одно и то же смещение для фиксации

Sending offsets to transaction: {someTopic2-0=OffsetAndMetadata{offset=23, leaderEpoch=null, metadata=''}}

Я думал, когда они попытаются совершить то же самое, брокер прервет одну из транзакций ...

Я также заметил, что если я уменьшу файл transaction.timeout.ms до 2 секунд, он не прервет транзакцию, независимо от того, как долго я замораживаю экземпляр при отладке ...

Может быть, таймер transaction.timeout.ms запускается только после того, как я отправлю сообщение?

0
sharon gur 15 Июн 2020 в 10:26

1 ответ

Лучший ответ

Вы не должны использовать executeInTransaction вообще - см. Его Javadocs; он используется, когда нет активной транзакции или если вы явно не хотите, чтобы операция участвовала в существующей транзакции.

Вам нужно добавить KafkaTransactionManager в контейнер слушателя; он должен иметь ссылку на тот же ProducerFactory, что и шаблон.

Затем контейнер запустит транзакцию и, в случае успеха, отправит смещение в транзакцию.

0
Gary Russell 15 Июн 2020 в 15:06