trilha

Tecnologias/03 - Filas/01 - Kafka6 min

Partições, Offsets e Consumer Groups

Conceito

Três conceitos, e praticamente tudo o que dá errado com Kafka em produção mora aqui.

Partição: paralelismo e ordem, no mesmo botão

A partição é o log de verdade. O tópico é só um nome para um conjunto de partições.

chave da mensagem → hash(chave) % nº de partições → partição
sem chave         → distribuição por lotes entre as partições (sticky)

Duas garantias saem daí, e é vital não confundi-las:

  • Ordem dentro da partição: garantida, sempre.
  • Ordem entre partições: não existe. Não há relógio global.

Então "quero processar em ordem" nunca é uma configuração — é uma escolha de chave:

Chave O que fica ordenado Paralelismo
null nada máximo
cliente_id todos os eventos de um cliente por cliente
pedido_id o ciclo de vida de um pedido por pedido
constante tudo (ordem total) 1 — uma partição só

A última linha é a armadilha: ordem total em Kafka significa uma partição, ou seja, throughput de um único consumidor. Ordem global e escala são incompatíveis por construção — a mesma lição de Sequential Convoy e de ordenação global exige consenso.

Offset: a posição, não a mensagem

Cada mensagem tem um número sequencial dentro da partição. O consumidor guarda "estou no offset 4.712 da partição 2" no tópico interno __consumer_offsets.

Isso é o que dá ao Kafka superpoderes que fila não tem:

# reprocessar tudo desde o começo
kafka-consumer-groups.sh --reset-offsets --to-earliest --group faturamento \
  --topic pedidos.criados --execute

# voltar para 2 horas atrás (depois de descobrir um bug no consumidor)
kafka-consumer-groups.sh --reset-offsets --to-datetime 2026-09-08T12:00:00.000 ...

# pular uma mensagem envenenada
kafka-consumer-groups.sh --reset-offsets --shift-by 1 ...

Corrigir um bug de processamento é reposicionar um ponteiro e deixar rodar. Numa fila tradicional, o dado já foi apagado — e não há para onde voltar.

Consumer group: divisão do trabalho

A regra fundamental:

Dentro de um grupo, cada partição é atribuída a exatamente um consumidor. Um consumidor pode ter várias partições. Consumidor além do número de partições fica ocioso.

Tópico com 4 partições:
  1 consumidor  → ele fica com P0,P1,P2,P3
  2 consumidores→ 2 partições cada
  4 consumidores→ 1 cada  ← paralelismo máximo
  6 consumidores→ 4 trabalham, 2 PARADOS (não "ajudam")

E grupos diferentes consomem o mesmo tópico de forma independente, cada um com seus offsets — é assim que fan-out funciona (Publisher-Subscriber):

pedidos.criados ─┬→ grupo "faturamento"  (offset próprio)
                 ├→ grupo "estoque"      (offset próprio)
                 └→ grupo "analytics"    (offset próprio, 3 dias atrás, tudo bem)

Rebalance

Quando um consumidor entra, sai ou morre, o grupo redistribui partições. Historicamente isso era um stop-the-world: todos param, devolvem tudo, recebem de novo.

A evolução:

Estratégia Comportamento
Eager (antigo) todos param e reatribuem tudo
Cooperative sticky só as partições que mudam de dono são revogadas
KIP-848 (GA no Kafka 4.0) coordenação no servidor, incremental; rebalance muito mais rápido e sem depender do consumidor mais lento

Share groups: a exceção à regra (Kafka 4.2)

Os share groups, GA no 4.2, quebram deliberadamente a regra de "uma partição, um consumidor":

Consumer group Share group
Consumidores por partição 1 vários
Teto de paralelismo nº de partições nenhum
Unidade de ack offset (posição) mensagem individual
Ordem por partição não garantida
Redelivery reposicionar offset por mensagem, com estado

Ou seja: você escolhe por grupo se quer semântica de log (ordem, replay) ou de fila (paralelismo livre, ack individual) — no mesmo tópico. É a mudança conceitual mais relevante do Kafka desde o KRaft. Ver 5. Kafka vs Fila Tradicional e Competing Consumers.

Trade-offs

Partições demais também é problema. Cada partição custa descritores de arquivo, memória e tempo de recuperação por broker, e mais requisições por lote. A regra é dimensionar pelo throughput desejado (vazão alvo / vazão por consumidor) com folga de 2–3×, não "1000 partições por garantia".

Aumentar partições quebra a ordem histórica. hash(chave) % N muda quando N muda: mensagens antigas de um cliente estão em P1 e as novas vão para P3. Se algum consumidor depende de ordem por chave, esse é um evento de migração, não um ajuste de capacidade.

Commit antes ou depois define a garantia. Commit antes de processar = at-most-once (pode perder). Commit depois = at-least-once (pode duplicar). Não existe terceira opção sem transação. Ver 4. Garantias de Entrega e Idempotência.

Auto-commit é conveniente e traiçoeiro. Com enable.auto.commit=true, o offset avança em segundo plano a cada intervalo — inclusive de mensagens que você ainda não terminou de processar. Se o processo cair no meio, elas somem. Em produção: commit manual, sempre depois do trabalho.

O trade-off central: partição é o único botão do Kafka, e ele controla duas coisas ao mesmo tempo — quanto você paraleliza e o quanto fica ordenado. Toda decisão aqui é escolher onde nesse eixo você quer ficar. Os share groups do 4.2 finalmente separam os dois botões, para quem dispensa a ordem.

Exemplo prático

Dimensionar partições

Vazão alvo:                   30.000 msg/s
Um consumidor processa:        2.000 msg/s (medido, não estimado!)
Consumidores necessários:            15
Partições (com folga 2×):            32   ← escolha

Meça o consumidor fazendo o trabalho real (incluindo a escrita no banco), não lendo para o vazio. O gargalo quase nunca é o Kafka.

Escolher a chave

// ❌ Sem chave: máximo paralelismo, zero ordem.
// Um PedidoAtualizado pode ser processado ANTES do PedidoCriado.
cl.Produce(ctx, &kgo.Record{Topic: "pedidos", Value: payload}, nil)

// ✅ Chave = a entidade cujo ciclo de vida precisa de ordem
cl.Produce(ctx, &kgo.Record{
	Topic: "pedidos",
	Key:   []byte(pedido.ID),   // todos os eventos deste pedido, na mesma partição, em ordem
	Value: payload,
}, nil)
O bug mais comum de ordenação

Publicar PedidoCriado e PedidoPago sem chave, ou com chaves diferentes. Eles vão para partições diferentes, são processados por consumidores diferentes, e o PedidoPago chega antes. O consumidor então cria um pedido "pago" sem saber o que foi comprado — e o bug aparece em 0,1% dos casos, só sob carga.

Monitorar o que importa

kafka-consumer-groups.sh --bootstrap-server kafka:9092 \
  --describe --group faturamento
# TOPIC  PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG  CONSUMER-ID

LAG é a métrica número um de todo sistema com Kafka — mais importante que CPU, mais importante que memória. Lag estável (mesmo que alto) é saudável. Lag crescendo é um incidente em andamento, e o alerta deve ser sobre a derivada, não sobre o valor absoluto.

Como isso falha em produção

Sintoma Causa
Metade dos consumidores ociosa mais consumidores que partições
Rebalance a cada poucos minutos processamento excede max.poll.interval.ms; reduza max.poll.records ou processe em paralelo dentro do consumidor
Uma partição sempre atrasada key skew — uma chave concentra o volume
Mensagens reprocessadas depois de deploy consumidor morto sem commit; esperado em at-least-once
Mensagens perdidas depois de deploy auto-commit avançou offset de mensagem não processada
Ordem quebrada após aumentar partições mudou o hash % N
LAG negativo ou grupo "vazio" grupo sem membros (todos caíram) ou offsets resetados

Aprofundamento aplicado

Conclusão fora de ordem e commit prematuro

Considere um consumidor comum, com processamento paralelo de registros de uma partição:

registro 10: ainda processando
registro 11: concluído
registro 12: concluído

Confirmar a posição 13 indica que, ao reiniciar, o grupo pode retomar dali. Isso pularia o registro 10 se seu efeito não terminou. O commit deve acompanhar o maior prefixo contíguo concluído; terminar um registro mais adiante não fecha o buraco anterior.

A mesma análise vale durante revogação de partições: parar novas tarefas, coordenar as existentes e confirmar apenas progresso seguro. Consumidores baseados em share groups têm outro modelo de confirmação e não devem ser confundidos com esse exemplo. Referência: consumer offsets e garantias.

Exercício: conclua 12, depois 10, depois 11. Resultado esperado: após 12 isolado, o próximo offset seguro continua 10; depois de 10, passa a 11; após 11, pode chegar a 13.

Relacionado


Parte de MOC Kafka · Consumer (Kafka docs)

Buscar

Busca por título, seção e texto das notas