trilha

Tecnologias/03 - Filas/01 - Kafka7 min

Kafka

Conceito

A definição que evita 90% da confusão:

Kafka é um log append-only, particionado e replicado, cuja retenção é independente do consumo.

Cada palavra importa:

  • log append-only — só se escreve no fim. Nada de atualizar ou apagar uma mensagem. Isso é o que permite escrita sequencial em disco, e escrita sequencial em disco é rápida até em HDD.
  • particionado — o tópico é dividido em partições; cada partição é um log independente. É a unidade de paralelismo e de ordem.
  • replicado — cada partição tem um leader e N followers. Durabilidade sem depender de disco confiável.
  • retenção independente do consumo — a mensagem fica pelo tempo (ou tamanho) configurado, tenha ou não sido lida. É a diferença essencial em relação a uma fila.
Tópico "pedidos", 3 partições:

P0: [0][1][2][3][4][5]              ← consumidor A no offset 4
P1: [0][1][2][3]                    ← consumidor B no offset 1
P2: [0][1][2][3][4][5][6][7]           (dois leitores, ritmos diferentes,
                                        nenhum interfere no outro)

Por que é rápido

Não é mágica; são quatro decisões que se somam:

  1. Escrita sequencial em arquivos de segmento — o padrão de I/O mais rápido que existe.
  2. Page cache do SO em vez de cache próprio: consumidor que lê o dado recente lê da RAM, sem cópia de aplicação.
  3. sendfile (zero-copy) para enviar do arquivo direto ao socket, sem passar pelo espaço de usuário (TLS e alguns modos reduzem esse ganho).
  4. Batching e compressão do lado do produtor: linger.ms junta mensagens, compression.type (lz4/zstd) comprime o lote. Um lote comprimido atravessa a rede e o disco uma vez só.

KRaft: a mudança mais importante da década

Kafka dependia do ZooKeeper para metadados (quem é leader, quais tópicos existem). Eram dois sistemas distribuídos para operar, com dois modelos de falha.

  • Kafka 4.0 (mar/2025) removeu o ZooKeeper por completo. O KRaft é o único modo: os metadados vivem num log Raft interno, gerenciado pelos próprios brokers (controllers).
  • Consequência prática: um sistema a menos, failover de controller mais rápido, e suporte a muito mais partições por cluster.
  • Se você encontra documentação, tutorial ou script mencionando ZooKeeper, é anterior ao 4.0. Não se aplica mais.

O 4.0 também tornou GA o novo protocolo de consumer group (KIP-848), que move a coordenação de rebalance para o servidor — rebalance mais rápido e menos "paradas do mundo". Ver 3. Partições, Offsets e Consumer Groups.

Kafka 4.1 e 4.2: o Kafka ganhou filas

Versão O que mudou
4.0 (mar/2025) ZooKeeper removido; KRaft único; KIP-848 GA; share groups em preview inicial
4.1 (set/2025) share groups em preview; novo protocolo de rebalance do Kafka Streams (early access)
4.2 (fev/2026) share groups (filas) production-ready; 38 KIPs — recorde histórico

Share groups (KIP-932) são a novidade que mais muda decisões de arquitetura: eles trazem semântica de fila para dentro do Kafka — ack por mensagem, redelivery, processamento fora de ordem — e desacoplam o paralelismo do número de partições. Antes, o número de consumidores úteis era limitado pela quantidade de partições; agora você escala workers pela necessidade de processamento. Ver 5. Kafka vs Fila Tradicional.

Retenção: apagar por tempo ou compactar por chave

# 1) retenção por tempo/tamanho — o padrão, para eventos
cleanup.policy=delete
retention.ms=604800000        # 7 dias

# 2) compactação por chave — mantém o ÚLTIMO valor de cada chave, para sempre
cleanup.policy=compact

Compactação transforma o tópico numa tabela: o último estado de cada chave, sempre disponível. É a base de event sourcing, de tabelas materializadas no Kafka Streams e do próprio armazenamento de offsets. Detalhe crítico: uma mensagem com value=null (tombstone) marca a chave para remoção — é assim que se apaga alguém do log, e é o que torna LGPD/GDPR possível num sistema append-only.

O ecossistema

Componente Para quê
Kafka Connect integrar sem escrever código: CDC de banco (Debezium), sink para S3/Elastic
Kafka Streams processamento com estado dentro do seu app (janelas, joins, agregação)
Schema Registry contrato de mensagem (Avro/Protobuf/JSON Schema) com evolução compatível
Tiered storage dados antigos em object storage: retenção de meses sem disco local caro
Schema Registry não é opcional em produção

Sem contrato versionado, o produtor muda um campo e derruba cinco consumidores — e o log já tem 400 GB no formato antigo. Com backward compatibility imposta pelo registry, isso é bloqueado no produce. É a peça mais barata e mais esquecida.

Trade-offs

Ordem existe apenas dentro de uma partição. Não há ordem global num tópico. Se você precisa que os eventos de um cliente sejam processados em sequência, a chave da mensagem tem que ser o cliente_id — e você aceita que clientes diferentes sejam processados em paralelo, fora de ordem entre si. Ver Sequential Convoy.

O número de partições é uma decisão semi-permanente. Aumentar partições é fácil e muda o mapeamento chave → partição, o que quebra a garantia de ordem por chave para o histórico. Diminuir é impossível. Dimensione com folga e não exagere: partições custam memória, descritores e tempo de failover.

Kafka não é bom em coisas de fila (ou não era): prioridade, delay, TTL por mensagem, ack individual, retry por mensagem com backoff. Os share groups do 4.2 cobrem boa parte disso, mas o ecossistema (bibliotecas, ferramentas, conhecimento) ainda está se ajustando.

Operar Kafka é um trabalho. Mesmo sem ZooKeeper: dimensionar disco e retenção, balancear partições, monitorar ISR e lag, planejar upgrade sem downtime. Gerenciado (Confluent Cloud, MSK, Redpanda Cloud) custa dinheiro e devolve fins de semana.

Kafka é overkill na maioria dos casos em que é escolhido. Se você tem 200 mensagens por segundo, um consumidor e nenhuma necessidade de replay, um SKIP LOCKED no Postgres ou um SQS resolve com uma fração da complexidade.

O trade-off central: Kafka paga complexidade operacional para comprar throughput, retenção e replay. Se você não vai reprocessar o histórico e não tem múltiplos consumidores independentes do mesmo dado, você está pagando a conta sem consumir o produto.

Exemplo prático

Configuração de produtor e consumidor em Go (franz-go), com os parâmetros que definem durabilidade e ordem:

cl, err := kgo.NewClient(
	kgo.SeedBrokers("kafka-1:9092", "kafka-2:9092"),

	// --- durabilidade ---
	kgo.RequiredAcks(kgo.AllISRAcks()), // acks=all: espera todas as réplicas em ISR
	kgo.ProducerBatchCompression(kgo.ZstdCompression()),
	// idempotência do produtor é o padrão no franz-go: sem duplicata por retry
	// e sem reordenação, mesmo com várias requisições em voo

	// --- consumo ---
	kgo.ConsumerGroup("faturamento"),
	kgo.ConsumeTopics("pedidos.criados"),
	kgo.DisableAutoCommit(), // commit manual: só depois de processar de verdade
)
for {
	fetches := cl.PollRecords(ctx, 500)
	if errs := fetches.Errors(); len(errs) > 0 { /* trate e continue */ }

	fetches.EachRecord(func(r *kgo.Record) {
		if err := processar(ctx, r); err != nil {
			paraDLQ(ctx, r, err) // não trave a partição por uma mensagem ruim
			return
		}
	})

	// commit DEPOIS do trabalho → at-least-once (o consumidor precisa ser idempotente)
	if err := cl.CommitUncommittedOffsets(ctx); err != nil {
		slog.Error("commit falhou", "err", err)
	}
}

Os três parâmetros que decidem se você perde mensagem:

# no broker/tópico
replication.factor=3
min.insync.replicas=2            # com acks=all, exige 2 réplicas confirmando
unclean.leader.election.enable=false   # NUNCA eleja um leader atrasado

# no produtor
acks=all
enable.idempotence=true

replication.factor=3 + min.insync.replicas=2 + acks=all é a combinação que tolera a perda de um broker sem perder escrita e sem parar de aceitar. Com min.insync.replicas=3 você para de aceitar escrita ao perder um nó — mais seguro e menos disponível (Availability vs Consistency).

Como isso falha em produção

Sintoma Causa
Consumer lag crescendo consumidor mais lento que o produtor; falta partição ou worker
Rebalance em loop processamento passou de max.poll.interval.ms → o grupo expulsa o consumidor, que volta, e repete
Mensagem perdida após falha de broker acks=1, ou unclean.leader.election ligado
Uma partição com 80% do tráfego key skew: um cliente_id gigante ou chave nula mal distribuída
Ordem quebrada depois de um pico retries > 0 com max.in.flight > 1 sem idempotência
Consumidor parado numa mensagem poison pill sem DLQ
Disco cheio no broker retenção maior que a capacidade planejada
Dado "sumiu" do tópico retenção expirou — o log não é um banco de dados

Aprofundamento aplicado

Retenção é parte do contrato com consumidores

Um consumidor independente precisa ler antes de o histórico necessário expirar ou ter outra fonte para reconstrução. Kafka manter dados por tempo configurado não significa que qualquer consumidor poderá ficar parado indefinidamente.

Cenário: retenção de três dias e consumidor parado por cinco. Retomar do offset antigo pode ser impossível. A estratégia precisa decidir entre reconstruir a projeção, aceitar a lacuna ou usar um histórico externo.

Compactação mantém outro tipo de histórico

Um tópico compactado favorece reter estado por chave conforme suas regras. Ele não promete preservar cada transição antiga. Para reconstruir auditoria de mudanças, a distinção entre estado atual e sequência completa de fatos é essencial. Referência: log compaction.

Exercício: compare cliente-42: nome A → nome B → excluído em um histórico de eventos e em uma projeção compactada. Resultado esperado: explicar qual pergunta cada um consegue responder depois da retenção e da compactação.

Relacionado


Parte de MOC Kafka · kafka.apache.org/documentation · Anúncio do 4.2.0

Buscar

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