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:
- Escrita sequencial em arquivos de segmento — o padrão de I/O mais rápido que existe.
- 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.
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).- Batching e compressão do lado do produtor:
linger.msjunta 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 |
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
- 1. Mensageria e Desacoplamento
- 3. Partições, Offsets e Consumer Groups
- 4. Garantias de Entrega e Idempotência
- 5. Kafka vs Fila Tradicional
- Kinesis
- Sequential Convoy
- Claim Check
- 5. Go em Produção
Parte de MOC Kafka · kafka.apache.org/documentation · Anúncio do 4.2.0