03 - Event Streaming (Kafka)
video: https://youtu.be/... no frontmatter da nota.Event Streaming (Kafka)
Event streaming trata mensagens como um log de eventos persistente e reprocessável, e não como envelopes descartados após o consumo. No modelo introduzido pelo Apache Kafka, o broker é um log append-only distribuído: o produtor adiciona eventos ao fim do log, os consumidores leem do log controlando sua própria posição, o offset, e o log permanece armazenado por uma janela de retenção configurável. A diferença conceitual para a fila tradicional é profunda: a fila remove a mensagem ao confirmar o consumo, enquanto o stream mantém o evento e permite que qualquer consumidor leia de novo, do início ou de qualquer ponto, quantas vezes precisar. Em produção, essa propriedade sustenta integração entre sistemas, pipelines de dados em tempo real, event sourcing, auditoria completa e reconstrução de estado. Compreender streaming é entender por que o Kafka virou infraestrutura padrão para eventos em larga escala e o que ele muda no desenho de consumidores, desde o commit de offset até o replay deliberado.
Conceito: log persistente em vez de fila descartável
A metáfora correta para o Kafka não é o correio, é o livro contábil. Cada evento é um registro datado e imutável anexado ao log, e ler um registro não o consome. Diversos sistemas podem folhear o mesmo livro ao mesmo tempo, cada um em sua página, cada um no seu ritmo, sem interferir uns nos outros.
Essa inversão muda a pergunta central do consumidor. Na fila, a pergunta é qual mensagem o broker vai me entregar agora; no stream, é em que offset eu parei da última vez. O consumidor é dono da própria posição, e avançar, retroceder ou recomeçar são operações explícitas e baratas.
A retenção é configurável por tópico, tipicamente de sete dias até anos, ou por tamanho, e independe do consumo. Os eventos existem porque aconteceram, e não porque alguém está esperando por eles. É essa desvinculação entre existência do dado e interesse no dado que habilita o reprocessamento como prática de engenharia rotineira, e não como recuperação de desastre.
No Kafka especificamente, o log é replicado entre brokers, com um líder por partição e réplicas em sincronia formando o ISR. Durabilidade é função do replication factor e do acks configurado no produtor, e disponibilidade sob falha de broker é automática, com eleição de novo líder. Desde a versão 4.0 o cluster opera em KRaft, sem ZooKeeper, e o protocolo de consenso interna passou a ser parte do próprio Kafka.
O problema que resolve
Filas tradicionais resolvem distribuição de trabalho, mas falham em três situações comuns. A primeira é o bug no consumidor: se a mensagem foi removida após o ack e o processamento estava errado, o dado foi perdido, e a única saída é recuperar da origem. No stream, o evento continua no log, e corrigir o código e consumir novamente do offset do erro resolve o problema em minutos.
A segunda situação é o novo consumidor de um fluxo histórico. Um serviço novo precisa dos últimos trinta dias de eventos para construir seu estado inicial; com fila, ele só vê o que aparecer depois de conectado. Com streaming, ele consome o log desde o início, à velocidade de leitura, e alcança o presente.
A terceira é a múltipla finalidades para o mesmo fluxo. Integração, analytics, auditoria e machine learning precisam dos mesmos eventos com garantias e ritmos diferentes. O log serve todas as leituras de uma única gravação, sem duplicar produtores nem criar filas paralelas por interesse, e é dessa economia que nasce o papel do Kafka como espinha dorsal de dados.
Partições e offsets: a anatomia do log
Um tópico no Kafka é dividido em partições, e cada partição é um log ordenado e imutável com seus próprios offsets sequenciais. O produtor escolhe a partição, em geral pelo hash de uma chave, e eventos com a mesma chave caem sempre na mesma partição. A ordenação existe dentro da partição e apenas dentro dela.
O offset é a posição do evento na partição, um número simples e crescente. O consumidor acompanha seu progresso commitando offsets, o que é barato e explícito. O commit é a declaração: até aqui eu processei; e é também o mecanismo que define as garantias de entrega em caso de falha.
O paralelismo do tópico é dado pelo número de partições. Mais partições permitem mais consumidores simultâneos e maior throughput, ao custo de mais arquivos abertos e mais replicação no broker. A regra prática de desenho é dimensionar partições para o pico esperado com folga, porque reduzir partições depois não é suportado.
| Conceito | Definição | Papel no padrão |
|---|---|---|
| Tópico | Log lógico nomeado | Contrato do fluxo de eventos |
| Partição | Log físico ordenado | Unidade de ordenação e paralelismo |
| Offset | Posição do evento na partição | Progresso do consumidor |
| Chave | Identificador de roteamento | Co-localização de eventos da mesma entidade |
| Retenção | Janela de vida do log | Habilita replay e leitura histórica |
# Desenho e inspeção de um tópico de eventos em produção
kafka-topics.sh --create --topic pagamentos.aprovados \
--partitions 12 --replication-factor 3 \
--config retention.ms=604800000
kafka-topics.sh --describe --topic pagamentos.aprovados
# Partition: 0 Leader: broker-1 Replicas: 1,2,3 Isr: 1,2,3
# Inspeção da posição de um consumer group (lag por partição)
kafka-consumer-groups.sh --bootstrap-server broker:9092 \
--group faturamento --describe
# Replay deliberado: reposiciona o grupo para o offset desejado
kafka-consumer-groups.sh --bootstrap-server broker:9092 \
--group faturamento --topic pagamentos.aprovados \
--reset-offsets --to-offset 42000 --execute
Consumer groups: competição com memória
Um consumer group reúne consumidores que dividem o trabalho de um tópico. Cada partição é atribuída a exatamente um consumidor do grupo, e o rebalanceamento redistribui partições quando membros entram ou saem. É a competição de consumidores da fila, adaptada ao log: mesma semântica de trabalho dividido, com progresso registrável em offsets.
Consumer groups distintos leem o tópico de forma independente. Faturamento e analytics podem estar em offsets completamente diferentes do mesmo log, sem qualquer interferência, e cada um comanda seu próprio replay. A entrega independente do pub-sub e a competição da fila convivem nesse desenho, e é por isso que o Kafka é descrito frequentemente como fila distribuída e pub-sub ao mesmo tempo.
O rebalanceamento é o momento delicado: enquanto partições mudam de dono, o consumo pausa, e offsets não commitados podem ser reprocessados. Grupos com sessões instáveis, membros longos no processamento sem heartbeat adequado e sticky sessions mal configuradas geram rebalanceamentos frequentes que aparecem em produção como picos de lag sem causa aparente.
# Consumidor com controle explícito de offset
consumidor = KafkaConsumer(
"pagamentos.aprovados",
group_id="faturamento",
enable_auto_commit=False, # commit só após processar
)
for evento in consumidor:
processar(evento) # efeito colateral: persistir fatura
if evento.partition == 0: # commit granular, por partição
consumidor.commit(
offsets={OffsetAndPartition(evento.partition, evento.offset + 1)}
)
# Falha antes do commit: o grupo volta ao último offset válido
# e o evento é reprocessado, logo idempotência é obrigatória
Reprocessamento e replay
O replay é a operação que define o padrão. Repor eventos existentes para reconstruir estado, retroalimentar um modelo, corrigir uma projeção ou auditar decisões passadas são variações da mesma operação: reposicionar o offset e consumir.
O reprocessamento exige dois cuidados técnicos. O primeiro é idempotência no consumidor, pois eventos já processados retornarão, e o efeito colateral precisa ser tolerante a repetição. O segundo é a compatibilidade do schema entre a versão que escreveu o evento e a versão que vai lê-lo novamente, pois o log guarda a forma original, e o código novo precisa saber ler o histórico inteiro dentro da janela de retenção.
Replay massivo tem custo real: satura rede, brokers e sistemas de destino. Em produção, replay sério é operação controlada, com throttling, janela acordada e monitoramento do downstream, e não comando disparado em sexta-feira à tarde.
Uma variação útil é o replay seletivo por chave ou por intervalo de tempo, comum em correção de dados: em vez de reprocessar o log inteiro, o time reposiciona o grupo para o offset correspondente ao início da janela afetada e interrompe no offset fim. Ferramentas de pipeline como Kafka Streams e Flink expõem essa capacidade nativamente, e em consumidores próprios ela é apenas aritmética de offsets.
Há ainda o caso do compacted topic, com retenção por chave em vez de por tempo: o log mantém o último valor de cada chave, e o formato é o suporte natural de changelog, tabelas derivadas e estado compactado para stores de agregação. É o mesmo mecanismo de log servindo a um contrato diferente.
Comparação com fila tradicional
| Aspecto | Fila tradicional | Event Streaming (Kafka) |
|---|---|---|
| Após o consumo | Mensagem removida | Evento mantido no log |
| Posição do consumidor | Gerenciada pelo broker | Offset de cada consumer group |
| Releitura | Não suportada | Replay a qualquer offset |
| Novo consumidor do histórico | Só vê o futuro | Lê desde o início, dentro da retenção |
| Ordenação | Por fila única ou não garantida | Por partição, com chave |
| Modelos de consumo | Competição entre workers | Grupos independentes + competição dentro do grupo |
| Caso central | Distribuição de tarefas | Movimentação e histórico de eventos |
A fronteira prática entre os dois modelos: se a mensagem é um comando que um executor precisa fazer exatamente uma vez, fila; se é um fato que múltiplos sistemas precisam observar, derivar e rever, streaming. Muitas arquiteturas usam os dois, com o stream como fonte da verdade e filas derivadas para entrega a consumidores com semântica de trabalho.
No desenho com ambos, o padrão recorrente é o stream interno ao domínio e a fila na borda de entrega. O log central preserva o histórico e alimenta projeções, e filas derivadas fazem a distribuição final para workers com DLQ e visibilidade própria. O erro oposto, usar fila para tudo, aparece como perda de histórico, impossibilidade de replay e duplicação de produtores para cada novo interesse.
Erros comuns
- Chaves mal escolhidas: chave com baixa cardinalidade ou com picos, como loja em Black Friday, cria partições quentes e desequilibra o cluster.
- Commit automático com processamento longo: offsets avançam antes do processamento terminar, e uma falha a partir dali é perda silenciosa de eventos.
- Partições insuficientes: paralelismo máximo já definido na criação do tópico, e escalar o grupo além do número de partições apenas adiciona consumidores ociosos.
- Ignorar o rebalanceamento: processamento longo sem max poll configurado derruba o membro do grupo e desencadeia ciclos de rebalance e lag crescente.
- Tratar o log como banco de dados consultável: busca por conteúdo não é caso de uso do log, e essa demanda pertence a um índice derivado dos eventos.
Boas práticas
- Escolha chaves de partição com alta cardinalidade e distribuição estável, e valide a uniformidade com métricas por partição, não só agregadas.
- Defina retenção pensando em cenários de replay e recuperação, porque a janela disponível delimita o quanto você pode voltar no tempo.
- Torne o processamento idempotente e versione o schema com registro central e política de compatibilidade, pré-requisitos de qualquer replay seguro.
- Monitore lag por consumer group e por partição, com alerta em tendência de crescimento, pois lag é o termômetro universal do streaming.
- Trate replay como procedimento operacional documentado, com runbook, limites de taxa e critério de sucesso, e não como improvisação pós-incidente.