Desmistificando as Semânticas de Exatamente Uma Vez do Kafka: Um Guia Abrangente

Entenda a semântica exatamente-uma vez do Kafka com produtores idempotentes, transações, consumidores read_committed e commits de offset.

Desmistificando a Semântica Exatamente-Uma Vez do Kafka: Um Guia Abrangente

A semântica exatamente-uma vez do Kafka pode proteger um pipeline de processamento de fluxo contra registros de saída duplicados quando produtores tentam novamente, brokers falham ou uma aplicação reinicia. A garantia é poderosa, mas é mais restrita do que o termo sugere: o Kafka pode tornar transacionais as escritas no Kafka e os offsets consumidos. Ele não pode tornar automaticamente seu banco de dados externo, gateway de pagamento ou API HTTP exatamente-uma vez.

Use a semântica exatamente-uma vez quando a saída duplicada seria cara ou difícil de limpar, como ajustes de inventário, eventos de saldo de conta ou tópicos de estado derivado consumidos por outros serviços.

Garantias de Entrega em Linguagem Simples

As aplicações Kafka geralmente falam sobre três modelos de entrega.

  • No máximo uma vez: Sua aplicação pode perder registros, mas não deve processar o mesmo registro duas vezes. Isso pode acontecer quando os offsets são confirmados antes do processamento terminar.
  • Pelo menos uma vez: Sua aplicação não deve perder registros, mas pode processar um registro mais de uma vez após uma nova tentativa ou reinicialização.
  • Exatamente uma vez: Um loop de leitura-processamento-escrita do Kafka confirma seus registros de saída e seus offsets consumidos como uma única transação.

O último ponto é a chave. A semântica exatamente-uma vez é mais forte quando a aplicação lê do Kafka, escreve resultados de volta no Kafka e confirma offsets dentro da mesma transação.

Produtores Idempotentes

Um produtor idempotente evita escritas duplicadas causadas por novas tentativas do produtor. O Kafka atribui ao produtor um ID e rastreia números de sequência para cada produtor e partição. Se o broker já aceitou um lote e então recebe a nova tentativa, ele pode rejeitar a duplicata em vez de anexá-la novamente.

Para clientes Kafka atuais, a idempotência é ativada por padrão quando você não configura configurações conflitantes do produtor. Você ainda pode defini-la explicitamente:

enable.idempotence=true
acks=all

acks=all significa que o líder espera por todas as réplicas em sincronia antes de confirmar a escrita. A idempotência também depende de configurações compatíveis de novas tentativas e requisições em andamento, portanto, evite substituir as configurações de confiabilidade do produtor, a menos que você saiba o efeito na sua versão do cliente.

A idempotência protege as novas tentativas do produtor, mas não torna atômico um fluxo de trabalho de processamento completo. Se sua aplicação consome de um tópico e produz para outro, você precisa de transações para unir a saída e a confirmação do offset.

Transações no Kafka

As transações permitem que um produtor agrupe várias escritas em uma unidade atômica. O produtor precisa de um transactional.id estável.

transactional.id=inventory-adjuster-0
enable.idempotence=true
acks=all

Um fluxo de transação típico é:

  1. Inicializar transações quando a aplicação inicia.
  2. Iniciar uma transação.
  3. Consumir registros do tópico de entrada.
  4. Produzir registros de saída.
  5. Enviar os offsets consumidos para a transação.
  6. Confirmar a transação ou abortá-la em caso de falha.

Se o processo falhar antes da confirmação, o Kafka não expõe a saída não confirmada para consumidores read_committed. Na reinicialização, a aplicação pode ler os mesmos registros de entrada novamente e produzir um resultado confirmado.

Configurações do Consumidor que Importam

Os consumidores que leem saída transacional devem usar:

isolation.level=read_committed
enable.auto.commit=false

read_committed oculta registros de transações abortadas. enable.auto.commit=false impede que o consumidor confirme offsets fora da transação.

O nome da propriedade é importante. A configuração do consumidor Kafka é enable.auto.commit, não auto.commit.enable.

Para uma aplicação manual de consumidor-produtor, a confirmação do offset deve fazer parte da transação do produtor. No cliente Java, isso significa usar as APIs do produtor transacional, incluindo o envio de offsets para a transação antes de confirmá-la.

Um Cenário Concreto

Imagine um tópico orders e um tópico de saída inventory-events. Seu serviço lê um pedido, verifica o SKU e escreve um evento de dedução de inventário.

Sem transações, uma falha após escrever a saída, mas antes de confirmar o offset de entrada, pode criar uma dedução duplicada após a reinicialização. Com transações, o evento de saída e a confirmação do offset de entrada são bem-sucedidos ou falham juntos. Uma reinicialização pode reler o pedido, mas apenas um evento de inventário confirmado se torna visível para consumidores downstream read_committed.

Limites a Considerar

A semântica exatamente-uma vez do Kafka não cobre efeitos colaterais fora do Kafka, a menos que você os projete para isso. Se o mesmo serviço também escreve no PostgreSQL ou chama uma API de faturamento, esse efeito colateral externo precisa de sua própria chave de idempotência, restrição única, estratégia de transação ou padrão outbox.

As transações também adicionam sobrecarga de coordenação. Para ingestão simples de logs onde duplicatas são aceitáveis, produtores idempotentes mais consumidores pelo menos uma vez podem ser suficientes.

Lista de Verificação Prática

Use um transactional.id estável por instância de aplicação ou tarefa. Não permita que dois produtores ativos usem o mesmo transactional.id ao mesmo tempo.

Configure consumidores de saída transacional para read_committed. Desative confirmações automáticas de offset em loops de processamento transacional.

Mantenha as transações curtas. Transações grandes podem aumentar a latência e tornar a recuperação mais lenta.

Trate sistemas externos separadamente. O Kafka pode proteger o estado do Kafka, mas suas escritas no banco de dados ainda precisam de um design idempotente.

A conclusão útil: a semântica exatamente-uma vez não é um interruptor mágico. São um conjunto de escolhas de produtor, consumidor e transação que funcionam melhor para processamento de fluxo Kafka-para-Kafka.