Kafka Producer: acks, retry e idempotência correta
1. Visão geral
Quando um producer envia uma mensagem para o Kafka, há três perguntas que você deve responder: enviar é o suficiente para ser considerado sucesso, se houver erro como fazer retry, e se o retry causa duplicação de mensagens. Este artigo explica acks, retries e enable.idempotence — três configurações que definem a confiabilidade e a correção do producer. Depois de ler, você saberá como configurar um producer “production-ready” e evitar erros comuns que causam perda ou duplicação de dados.
2. Dependências necessárias
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.7.0</version>
</dependency>
Os exemplos neste artigo usam o cliente Kafka puro (KafkaProducer), sem passar por Spring Kafka, para ver claramente como cada configuração afeta o comportamento.
3. acks: quando o producer considera que enviou com sucesso
acks determina quantos brokers o producer deve esperar confirmar antes de considerar a mensagem como enviada.
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.ACKS_CONFIG, "all");
acks=0: o producer não aguarda nenhuma confirmação. Mais rápido, mas perde a mensagem se o broker cair logo após receber — nem mesmo sabendo que foi perdida.acks=1: espera apenas o líder da partição escrever no log local e depois enviar ACK. Se o líder falhar antes do follower sincronizar, a mensagem é perdida.acks=all(ou-1): espera que todas as réplicas no ISR (in-sync replica set) confirmem. Mais seguro, mas com latência maior.
Nota:
acks=allé realmente seguro apenas quando acompanhado demin.insync.replicas >= 2no broker. Semin.insync.replicas=1,acks=alltambém precisa apenas de 1 réplica confirmando — não é diferente deacks=1em termos de segurança.
3.1. Configuração de broker que acompanha
# server.properties ou topic config
min.insync.replicas=2
Com replication factor = 3 e min.insync.replicas=2, o sistema consegue suportar 1 broker morrer e ainda conseguir escrever, enquanto ao mesmo tempo garante que os dados estejam em pelo menos 2 cópias antes do ACK.
4. retries: tratando erros temporários
O producer pode encontrar erros temporários como NotLeaderForPartitionException ou perda de conexão de rede por um curto período. retries permite que o producer resenda automaticamente sem precisar de manipulação manual de código.
props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 100);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);
retries: número máximo de tentativas de retry. A partir do Kafka 2.1, deve-se definir bem alto (ouInteger.MAX_VALUE) e usardelivery.timeout.mspara controlar o tempo total de espera em vez de contar o número de vezes.retry.backoff.ms: tempo de espera entre tentativas de retry, para evitar spam no broker.delivery.timeout.ms: tempo total máximo desde a chamada desend()até receber o resultado (sucesso ou falha final). Deve ser maior querequest.timeout.ms + linger.ms.
Problema: se não for cuidadoso, retry pode fazer a mensagem ser enviada duplicadamente ou ser enviada fora de ordem.
4.1. Por que o retry causa duplicação de mensagens
1. Producer envia um lote contendo a mensagem A
2. Broker escreve A com sucesso, envia ACK de volta
3. ACK é perdido na rede (network timeout)
4. Producer não recebe o ACK -> considera como erro -> faz retry
5. Broker recebe o pedido novamente -> escreve A pela segunda vez
=> Tópico tem 2 cópias da mensagem A
Esta é uma situação clássica: o broker já escreveu com sucesso mas o producer não sabe, então envia novamente. A idempotência na próxima seção resolve corretamente esse problema.
4.2. Por que o retry pode causar falta de ordem
Se max.in.flight.requests.per.connection > 1, o producer pode enviar múltiplos lotes em paralelo. O lote 2 pode ser escrito pelo broker antes do lote 1 se o lote 1 sofrer um erro e estiver fazendo retry.
max.in.flight.requests.per.connection=5
Com essa configuração sem idempotência, a ordem das mensagens na partição não é garantida quando há retry.
5. Idempotência: resolvendo a duplicação causada por retry
enable.idempotence=true ajuda o producer a evitar automaticamente escrever duplicatas, graças ao Kafka atribuir Producer ID (PID) e número de sequência para cada lote.
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
Ao habilitar idempotência, Kafka define automaticamente os seguintes valores (você não precisa configurar manualmente, mas deve saber):
acks = all
retries = Integer.MAX_VALUE (ou bem alto)
max.in.flight.requests.per.connection <= 5
5.1. Mecanismo de funcionamento
Cada sessão do producer recebe um PID único do broker. Cada mensagem enviada em uma partição tem um número de sequência crescente. O broker mantém o último número de sequência escrito para cada PID + partição.
Producer envia lote seq=5 para a partição 0
Broker: "a última sequência que mantive é 4, 5 é a próxima -> escreve, mantém seq=5"
Producer reenvia (por causa de retry) o lote seq=5
Broker: "seq=5 eu já escrevi -> ignora, retorna ACK de sucesso de qualquer forma"
Por isso, o retry não cria registros duplicados — o broker detecta e remove automaticamente duplicatas com base no número de sequência, sem precisar que o producer ou consumidor lidem com isso.
Nota: idempotência apenas garante sem duplicatas em uma única sessão do producer e dentro do escopo de uma partição. Se o producer reiniciar (um novo PID é atribuído), o mecanismo de dedup baseado no PID antigo não se aplica mais. Para obter exactly-once em múltiplas partições/tópicos, você precisa de transações (
transactional.id) — que está fora do escopo da idempotência simples.
5.2. Exemplo de configuração de producer production-ready
public ProducerFactory() {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// Habilitar idempotência -> define automaticamente acks=all, retries alto, in-flight <= 5
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120_000);
this.producer = new KafkaProducer<>(props);
}
public void sendOrderEvent(String orderId, String payload) {
ProducerRecord<String, String> record =
new ProducerRecord<>("order-events", orderId, payload);
producer.send(record, (metadata, exception) -> {
if (exception != null) {
log.error("Envio do pedido {} falhou", orderId, exception);
} else {
log.info("Pedido {} escrito na partição {} offset {}",
orderId, metadata.partition(), metadata.offset());
}
});
}
ENABLE_IDEMPOTENCE_CONFIG = trueé a linha mais importante — ela puxaacks=allautomaticamente.- Callback em
send()fornece o resultado real: seexception != null, significa que já foi feito retry atédelivery.timeout.mse ainda assim houve erro — neste ponto você precisa lidar com a lógica de negócio (log, alerta, salvar em tabela de erros).
6. Verificando o comportamento com testes
Para ilustrar a parte de dedup, simulamos um MessageDeduplicator simples que simula o mecanismo de número de sequência em nível de aplicação (usado quando você não pode habilitar idempotência, por exemplo, sistemas legados):
public class MessageDeduplicator {
private final Map<String, Long> lastSeenSequence = new ConcurrentHashMap<>();
public boolean shouldWrite(String partitionKey, long sequence) {
Long last = lastSeenSequence.get(partitionKey);
if (last != null && sequence <= last) {
return false; // duplicata, ignorar
}
lastSeenSequence.put(partitionKey, sequence);
return true;
}
}
class MessageDeduplicatorTest {
@Test
void shouldSkipDuplicateSequence() {
MessageDeduplicator deduplicator = new MessageDeduplicator();
assertThat(deduplicator.shouldWrite("order-1", 5)).isTrue();
assertThat(deduplicator.shouldWrite("order-1", 5)).isFalse(); // retry com seq duplicado
assertThat(deduplicator.shouldWrite("order-1", 6)).isTrue();
}
@Test
void shouldAllowDifferentPartitionKeysIndependently() {
MessageDeduplicator deduplicator = new MessageDeduplicator();
assertThat(deduplicator.shouldWrite("order-1", 1)).isTrue();
assertThat(deduplicator.shouldWrite("order-2", 1)).isTrue();
}
}
O primeiro teste afirma que a mesma sequência enviada novamente pela segunda vez é ignorada — correto no espírito da idempotência do producer. O segundo teste afirma que dedup se aplica apenas dentro do escopo de uma key/partição, não afeta outras keys.
7. Tabela de resumo de configurações por necessidade
| Necessidade | acks | enable.idempotence | Risco |
|---|---|---|---|
| Log, métrica não importante | 0 | false | Perde mensagens em caso de erro, mas throughput mais alto |
| Balancear velocidade/segurança | 1 | false | Perde mensagens se o líder falhar antes do follower sincronizar |
| Dados de negócio importantes (pedidos, pagamentos) | all | true | Latência maior, mas sem perda, sem duplicação por retry |
8. Erros comuns
- Definir
acks=allmas esquecermin.insync.replicas: pensa que é seguro mas o broker apenas precisa de 1 ISR ainda assim ACK — não diferente deacks=1. - Escrever lógica de retry manualmente em paralelo com o retry padrão do cliente Kafka: facilmente causa duplicação dupla de envio porque ambas as camadas de retry estão funcionando.
- Habilitar idempotência mas deixar
max.in.flight.requests.per.connection > 5: Kafka rejeitará essa configuração assim que o producer for inicializado (ConfigException), porque excede o limite que idempotência suporta. - Confundir idempotência com exactly-once em todo o sistema: idempotência apenas protege producer-para-broker. Se o consumidor processa a mensagem e depois chama um side-effect (enviar email, debitar dinheiro) não idempotente, você ainda pode ter processamento duplicado do lado do consumidor.
Nota: não ative
enable.idempotence=truese o sistema estiver rodando uma versão do Kafka mais antiga que 0.11 — esse recurso ainda não é suportado pelo broker e o producer falhará durante a inicialização.
9. Conclusão
acks, retries e enable.idempotence são três peças inseparáveis quando você precisa de um producer confiável. acks=all garante que os dados sejam replicados adequadamente antes de considerado bem-sucedido; retries ajuda a superar erros temporários; e idempotência garante que esse retry não cria registros duplicados. Com dados de negócio importantes, use os três juntos por padrão — o custo de latência é muito menor comparado com o custo de lidar com dados incorretos ou duplicados depois.