O que acontece depois que um produtor Kafka chama send()?

Isso parece uma pergunta simples. O produtor cria um registro, envia-o ao Kafka e recebe uma confirmação.

No entanto, muita coisa pode acontecer entre esses passos.

O produtor pode esperar por mais registros e criar um batch. Ele precisa escolher uma partição. Ele pode comprimir o batch. Se o broker estiver lento, o produtor pode ficar sem espaço no buffer. Se uma requisição falhar, ele pode tentá-la novamente. Se a retentativa for bem-sucedida depois que a primeira requisição já foi escrita, o Kafka pode receber o mesmo batch duas vezes.

O Kafka Simulator v1.1 foca nessa semântica do produtor.

A versão adiciona 10 novos cenários, elevando o currículo completo para 23 de 125 cenários.

O que está incluído na v1.1?

Oito dos dez cenários focam no produtor. Os outros dois estendem a trilha de anatomia do log: a leitura até o high watermark e o log start offset.

Os cenários do produtor cobrem cinco áreas:

  • batching com linger.ms e batch.size,
  • particionadores do produtor,
  • compressão,
  • backpressure com buffer.memory e max.block.ms,
  • retentativas e idempotência.

Vamos analisar cada uma delas.

Batching com linger.ms e batch.size

Um produtor Kafka não envia necessariamente cada registro em uma requisição separada.

Registros atribuídos à mesma partição podem ser reunidos em um batch. O produtor envia o batch quando ele fica cheio ou quando o tempo de espera configurado expira.

É aqui que duas configurações do produtor se tornam importantes:

  • batch.size limita o tamanho de um batch,
  • linger.ms permite que o produtor espere por registros adicionais antes de enviá-lo.

O compromisso é entre latência e vazão.

Um batch maior geralmente significa menos requisições e melhor compressão. No entanto, esperar o batch encher pode aumentar a latência de um registro individual.

Os novos cenários tornam esse comportamento visível. Você pode ver quando os registros entram em um batch, o que faz o batch ser enviado e como alterar a configuração afeta as requisições produzidas pelo cliente.

Particionadores do produtor

Antes que o produtor possa criar um batch específico de partição, ele precisa selecionar uma partição.

Para registros com chave, a chave é usada para escolher a partição. Registros com a mesma chave são, portanto, normalmente atribuídos à mesma partição.

Para registros sem chave, o comportamento é diferente. O produtor pode agrupar registros usando sua estratégia de particionamento em vez de calcular a partição a partir de uma chave.

O simulador mostra ambos os casos passo a passo:

  1. o produtor recebe o registro,
  2. o particionador seleciona uma partição,
  3. o registro é adicionado ao batch correspondente,
  4. o batch é enviado ao líder daquela partição.

Isso facilita entender por que o particionamento afeta tanto a ordenação quanto o batching.

A compressão acontece por batch

A compressão do produtor Kafka é aplicada a batches de registros, não de forma independente a cada registro.

Essa distinção importa.

A compressão costuma ser mais eficaz quando um batch contém múltiplos registros semelhantes. Um batch que contém apenas um registro pequeno pode ganhar muito pouco com a compressão. Um batch maior com dados repetitivos pode ser comprimido de forma muito mais eficiente.

Como resultado, a compressão está conectada à configuração de batching. Configurações como batch.size e linger.ms podem influenciar não só a frequência das requisições, mas também o formato e a compressibilidade dos batches resultantes.

Os novos cenários mostram os tamanhos dos batches não comprimidos e comprimidos, tornando essa relação explícita.

Backpressure com buffer.memory e max.block.ms

O que acontece quando uma aplicação produz registros mais rápido do que o Kafka consegue aceitá-los?

O produtor não pode manter um número ilimitado de registros não enviados na memória. Ele usa um buffer limitado, configurado por meio de buffer.memory.

Quando o buffer não tem espaço disponível, uma chamada a send() pode bloquear enquanto espera que memória seja liberada.

Ela não esperará para sempre. O tempo máximo de bloqueio é controlado por max.block.ms.

Isso nos dá dois resultados possíveis:

  • dados suficientes são enviados e confirmados, liberando espaço no buffer,
  • nenhum espaço fica disponível antes do timeout, e o envio falha.

O simulador acompanha o buffer do produtor diretamente. Você pode ver a memória sendo alocada quando registros são adicionados e liberada conforme as requisições são concluídas.

Isso é especialmente útil quando combinado com um broker lento ou uma quota de vazão. Em vez de tratar o backpressure do produtor como uma questão abstrata de configuração, você pode observar o buffer encher e ver exatamente qual envio fica bloqueado.

Retentativas e o problema das duplicatas

Suponha que o produtor envie um batch a um broker.

O broker escreve o batch com sucesso, mas a confirmação é perdida antes de chegar ao produtor.

Do ponto de vista do produtor, a requisição falhou. Ele não tem como saber se o broker escreveu os dados. Tentar novamente é, portanto, razoável.

No entanto, se a primeira requisição foi escrita com sucesso, a retentativa pode acrescentar o mesmo batch novamente.

Esse é o problema das duplicatas causado por falhas ambíguas.

O cenário de retentativa mostra ambas as requisições:

  1. a requisição original chega ao broker,
  2. a confirmação é perdida,
  3. o produtor tenta o batch novamente,
  4. o broker recebe os mesmos registros pela segunda vez.

Sem idempotência, ambas as cópias podem ser acrescentadas.

Semântica do produtor idempotente

Um produtor idempotente permite que o Kafka reconheça batches retransmitidos.

O Kafka atribui uma identidade ao produtor e rastreia números de sequência de forma independente para cada partição. Quando um batch é retransmitido com um número de sequência que o broker já processou, o broker pode confirmá-lo sem acrescentar os registros novamente.

A distinção importante é entre uma retentativa e uma duplicata genuína.

Dois registros podem conter exatamente a mesma chave e o mesmo valor e ainda assim serem dois registros separados enviados intencionalmente pela aplicação. O Kafka não deve remover esses registros.

A idempotência deduplica retentativas do protocolo do produtor. Ela não deduplica os dados da aplicação com base em seu conteúdo.

Os novos cenários mostram:

  • IDs de produtor,
  • números de sequência por partição,
  • retentativas idempotentes bem-sucedidas,
  • registros duplicados genuínos,
  • detecção de sequência fora de ordem.

Por que max.in.flight.requests.per.connection importa?

Um produtor pode ter várias requisições aguardando respostas ao mesmo tempo.

Isso melhora a vazão, mas também cria um possível problema de ordenação.

Suponha que o produtor envie o batch A e depois o batch B. O batch A falha temporariamente, enquanto o batch B tem sucesso. Se A for retransmitido depois que B já foi escrito, os registros podem ser acrescentados em uma ordem diferente.

A idempotência usa números de sequência para detectar ordenação inválida.

O simulador mostra isso explicitamente. Você pode ver o número de sequência esperado pelo broker, o número carregado pelo batch que chega e o ponto em que o broker rejeita uma requisição fora de ordem.

Inspecionando o estado interno do produtor

Os novos cenários se apoiam fortemente no inspetor do simulador, que expõe o estado interno de produtores e partições, incluindo:

  • contadores de retentativa,
  • números de sequência do produtor,
  • números de sequência do broker,
  • alocação de buffer.

Normalmente, esses valores ficam escondidos por trás de métricas do produtor, logs e comportamento de protocolo. No simulador, eles podem ser inspecionados diretamente enquanto você avança ou retrocede em um cenário.

Correções no modelo do produtor

Adicionar os novos cenários também exigiu tornar o modelo de produtor existente mais preciso.

As retentativas são contadas por batch

O Kafka retransmite requisições de produção que contêm batches de registros.

Uma requisição que falha deve, portanto, consumir uma retentativa para o batch completo, não uma retentativa para cada registro dentro dele.

O simulador agora segue esse modelo. Cada registro de um batch que falhou é enviado novamente, enquanto o contador de retentativas avança uma única vez.

A memória do buffer é liberada com precisão

A memória do buffer do produtor agora é liberada conforme as confirmações chegam.

Isso corrige casos em que o simulador podia reportar backpressure mesmo que registros confirmados já devessem ter liberado sua memória.

O mesmo se aplica aos timeouts de max.block.ms. Quando um envio bloqueado expira, sua alocação de buffer é limpa.

acks=all é rastreado por partição

A durabilidade do Kafka é definida de forma independente para cada partição.

Um produtor que escreve em várias partições precisa, portanto, esperar que a partição correta satisfaça seus requisitos de confirmação. O progresso em outra partição não deve confirmar uma requisição não relacionada.

O simulador agora rastreia esse estado separadamente para cada partição e tópico.

O reset do produtor limpa o estado de idempotência

O broker armazena o estado de deduplicação associado a um produtor idempotente.

Quando um produtor simulado é removido ou resetado, esse estado também precisa ser removido. Caso contrário, um produtor recém-criado pode ter seus registros classificados incorretamente como retentativas antigas.

O comportamento de reset agora limpa o estado correspondente no lado do broker.

Retroceder restaura o estado de retentativa

Os cenários podem ser avançados e retrocedidos um passo de cada vez.

Retroceder agora reconstrói os contadores de retentativa usados durante cada rodada de simulação. O estado visível do produtor é, portanto, consistente, independentemente de o cenário estar avançando ou retrocedendo.

Melhorias no playground

A versão também contém várias correções fora do modelo do produtor.

Rebalanceamento de grupos de consumidores

Os grupos de consumidores no playground receberam várias correções de corretude:

  • o sticky assignor agora minimiza a movimentação de partições em vez de reatribuir tudo do zero,
  • um membro estático que fica ausente por mais tempo que session.timeout.ms é removido como qualquer outro membro,
  • alterar a contagem de partições de um tópico agora dispara um rebalanceamento adequado sem perturbar o estado dos membros,
  • partições nunca são atribuídas a um membro que não está mais vivo.

Configuração de tópicos e compactação

O Kafka nunca reduz a contagem de partições de um tópico. Encolher apagaria dados e quebraria o particionamento baseado em chave, então kafka-topics --alter rejeita a operação com InvalidPartitionsException.

O playground agora segue a mesma regra. Uma redução na contagem de partições é rejeitada com um erro didático em vez de descartar partições silenciosamente.

Tópicos compactados também se comportam com mais precisão:

  • um tombstone sobrevive até que sua própria janela de carência de delete.retention.ms expire,
  • um registro sem chave não pode ser produzido em um tópico compactado,
  • com compact,delete, a retenção por tempo agora recupera registros antigos independentemente da compactação.

O playground serializa suas ações na URL para que um cenário possa ser compartilhado com outra pessoa.

O codec agora inclui todas as mudanças de configuração, incluindo atualizações na contagem de partições. Um link que solicita um tipo de cluster de um pacote de lançamento posterior agora recorre a uma topologia suportada em vez de renderizar um cluster inutilizável.

Abrir um link compartilhado deve, portanto, recriar o mesmo cenário que foi originalmente construído.

Correções de interação

Vários problemas menores do playground também foram corrigidos:

  • identificadores de entidade não colidem mais depois que uma entidade é removida e recriada,
  • o posicionamento de produtores funciona corretamente em grades de broker mais largas.

Cabeçalhos de segurança

O site agora envia uma Content Security Policy e uma Permissions Policy.

Por que começar pelo produtor?

As garantias do Kafka costumam ser discutidas do ponto de vista do broker: replicação, participação no ISR, high watermarks e failover de líder.

No entanto, a garantia observada por uma aplicação também depende do produtor.

Qual modo de confirmação ele usa? Ele tenta novamente? A idempotência está ativada? Quantas requisições podem estar em voo? O que acontece quando seu buffer local fica cheio?

Essas configurações determinam se uma escrita é rápida, durável, ordenada, duplicada, bloqueada ou rejeitada.

É por isso que o primeiro pacote maior de cenários do Kafka Simulator foca no produtor.

Uma vez que batching, confirmações, retentativas e idempotência ficam visíveis, os cenários posteriores que cobrem replicação, durabilidade e transações têm uma base mais clara.

Abra o Kafka Simulator, selecione a trilha do produtor e experimente os cenários de falha passo a passo.