04 - Streaming de Dados (Kafka, Flink)
video: https://youtu.be/... no frontmatter da nota.Streaming de Dados com Kafka e Flink
Este caso acompanha uma plataforma de mobilidade urbana com 8 milhões de corridas por mês, em que a telemetria de cada viagem chega a cada 10 segundos e se soma aos eventos de aplicação, como solicitação, match, pagamento e avaliação. O volume combinado é de 65 milhões de eventos por dia, com média de 750 eventos por segundo e pico de 9.000 na sexta à noite com chuva. Os objetivos arquiteturais são quatro: ingestão durável sem perda, agregações em janela com latência inferior a 10 segundos, exatidão financeira na cobrança por viagem e um data lake que sirva BI e machine learning sem nova coleta. O desenho escolhe Kafka como espinha dorsal de ingestão e Flink como motor de processamento, com contratos versionados no schema registry. A exatidão da cobrança é o requisito que condiciona todas as outras decisões.
Requisitos e pressupostos
O contrato de serviço do pipeline define três classes de latência e uma de exatidão:
- Volume: 65 milhões de eventos por dia, média de 750 por segundo, pico de 9.000, tamanho médio de 1,2 KB por evento.
- Alertas operacionais em menos de 10 segundos do fato à notificação, porque o tempo de resposta da operação de rua depende disso.
- Dashboards de operação em menos de 1 minuto, e faturamento por viagem em menos de 5 minutos, com nenhum valor duplicado ou omitido.
- Ordenação garantida dentro da mesma viagem e do mesmo motorista, e sem garantia entre entidades distintas.
- Contratos versionados no schema registry, com evolução sem quebrar consumidores em produção.
- Retenção quente de 7 dias no Kafka e histórico completo no lake, com custo de armazenamento decrescendo com a idade.
- Privacidade por desenho: eventos não carregam identificadores diretos de pessoa; a correlação usa pseudônimo com tabela de resolução isolada, e o lake nunca recebe o mapa completo.
- Observabilidade do próprio pipeline no mesmo padrão dos serviços, para que a plataforma não dependa de acesso manual aos brokers.
Os pressupostos simplificam o desenho. Os eventos são imutáveis e apendáveis, e nenhuma correção reescreve o passado: retificação é evento novo que se sobrepõe ao anterior. Os produtores citam o schema por identificador, então a validação acontece na borda, e não no consumidor. O faturamento tolera segundos de atraso, mas não tolera erro, o que separa este fluxo dos dashboards, que aceitam aproximação. A ordem entre entidades distintas não é garantida e nenhuma agregação a exige, o que libera o particionamento para seguir a chave de entidade.
Os eventos dividem-se em duas classes com tratamentos distintos. Telemetria é densa, apendável e tolerante a amostragem, com 60% do volume; eventos de negócio são esparsos, carregam valores financeiros e exigem exatidão total. O particionamento separa as duas classes em tópicos distintos desde o início, porque a política de retenção e o custo por byte divergem por ordem de grandeza.
Arquitetura proposta
Os produtores são o aplicativo do passageiro, o SDK de telemetria do motorista e os serviços internos, todos publicando em tópicos por domínio: corridas.telemetria, corridas.eventos e pagamentos.eventos. Os tópicos são particionados por chave de entidade, e um job Flink consome, deduplica, enriquece e agrega, publicando em três destinos: um tópico de saída para alertas, o data lake em Iceberg sobre S3 e um banco analítico para consulta operacional. O schema registry governa os contratos em Avro com compatibilidade backward. A tabela resume os componentes e as configurações que carregam a carga.
| Componente | Papel | Configuração-chave | Motivo |
|---|---|---|---|
| Produtores | Publicam com schema por id | acks=all, idempotência habilitada | Durabilidade sem duplicação na origem |
| Tópicos | Particionados por entidade | 24 partições por tópico | Ordenação por viagem e paralelismo |
| Schema Registry | Contratos versionados | Compatibilidade BACKWARD | Evolução sem quebrar consumidor |
| Flink | Dedupe, janela e agregação | Checkpoint a cada 30 s | Exactly-once com tolerância a falha |
| Banco analítico | Consulta operacional | Escrita idempotente por checkpoint | Painel em segundos sem tocar na origem |
| Data lake | Histórico e ML | Commit por checkpoint no Iceberg | Evolução de schema sem reescrita |
| Tópico de saída | Alertas para operação | Chave por região | Consumo direto pelos times de plantão |
O cluster Kafka opera em modo KRaft, com 3 brokers, fator de replicação 3 e min.insync.replicas 2, e todo produtor usa acks=all com idempotência habilitada. Essa combinação troca um pouco de latência por garantia de durabilidade, e a medição mostrou custo de 4 ms no p99, aceitável para um pipeline que cobra dinheiro.
Três regras de governança atravessam a arquitetura:
- Nenhum produtor publica sem schema registrado e aprovado no pull request do contrato.
- Nenhum consumidor lê tópico de outro domínio diretamente; a integração é por tópico de saída publicado pelo dono.
- Toda reexecução e todo backfill passam por namespace separado, com commit idempotente para não duplicar no lake.
Um exemplo tipifica o caminho de ponta a ponta. A telemetria do motorista publica posição a cada 10 segundos em corridas.telemetria com a chave tripId; o Flink agrega distância e tempo na janela da viagem; o agregado vira registro no Iceberg e linha no analítico; e o evento de término dispara o faturamento em pagamentos.eventos, que segue o mesmo caminho com exatidão reforçada. Nenhum dashboard toca nos tópicos de origem, e nenhum serviço de negócio lê o lake diretamente.
Fluxo principal: da telemetria ao agregado
O fluxo completo tem cinco estágios verificáveis:
- Produção com validação de schema na borda e confirmação de durabilidade.
- Ingestão com ordenação por chave e retenção quente de 7 dias.
- Processamento com dedupe, enriquecimento e janela por tempo de evento.
- Publicação com commit idempotente por checkpoint nos três destinos.
- Consumo com leitura exclusiva por domínio e alerta de lag por subtarefa.
O particionamento por tripId é a decisão estrutural: os eventos da mesma viagem caem na mesma partição, o Kafka garante ordem dentro da partição, e o Flink pode agregar sem reordenar globalmente. O job abre janelas de 5 minutos por viagem com watermark de 2 minutos para tolerar atraso do GPS, e a sessão da viagem fecha quando o evento de término chega. O resultado agregado alimenta o faturamento, e cada commit no sink carrega a versão do checkpoint, o que torna a reexecução idempotente.
# Tópico de telemetria: ordenação por viagem com paralelismo controlado
topic: corridas.telemetria
partitions: 24 # paralelismo máximo de consumo
replication.factor: 3 # sobrevive a um broker fora
min.insync.replicas: 2 # acks=all exige duas réplicas em sync
retention.ms: 604800000 # 7 dias quentes antes do lake
cleanup.policy: delete
// Job Flink: dedupe, janela e publicação com exatidão de checkpoint
var eventos = env.fromSource(kafkaSource, WatermarkStrategy
.<EventoCorrida>forBoundedOutOfOrderness(Duration.ofMinutes(2))
.withTimestampAssigner((e, ts) -> e.ocorridoEm()),
"corridas-telemetria");
var faturamento = eventos
.keyBy(EventoCorrida::getTripId) // ordenação por viagem
.window(TumblingEventTimeWindows.of(Time.minutes(5)))
.allowedLateness(Time.minutes(2)) // aceita atraso do GPS
.aggregate(new AgregaDistancia(), new JanelaFaturamento())
.sinkTo(icebergSink); // commit idempotente por checkpoint
env.enableCheckpointing(30_000, CheckpointingMode.EXACTLY_ONCE);
O dedupe acontece antes da janela, por chave de evento com estado TTL de 24 horas, porque o SDK do motorista pode repetir lote em reconexão. A versão do registro no Iceberg carrega o identificador do checkpoint, então uma falha e reexecução do job reescreve a mesma versão, e não uma nova. O banco analítico recebe o mesmo fluxo com upsert pela chave de viagem, e o painel de operação lê daí, nunca do Kafka diretamente.
Dados atrasados além da tolerância não são descartados: entram como retificação na viagem afetada, com flag de atraso e recomputação da janela. Na medição, 0,8% dos eventos chegam depois da watermark, e 0,05% além da latência permitida; os dois caminhos têm destino definido, o que elimina a perda silenciosa que o desenho anterior acumulava.
O caminho dos alertas é o mais rápido e o mais simples. O job publica eventos de exceção, como viagem parada por mais de 10 minutos ou desvio de rota acima do limiar, no tópico corridas.alertas com chave por região, e o consumidor do plantão empurra para o canal da região em menos de 10 segundos do fato. A regra é uma por tipo de alerta, configurável sem redeploy, e o falso positivo é tratado como defeito de regra, e não como ruído a ignorar.
Decisões e trade-offs
| Decisão | Alternativa descartada | Trade-off aceito |
|---|---|---|
| Avro com schema registry | JSON livre no tópico | Rigidez de contrato; detecção de quebra em tempo de deploy |
| Exactly-once com checkpoints | At-least-once com dedupe no sink | Custo de checkpoint a cada 30 s; billing sem dedupe manual |
| Watermark de 2 minutos | Processamento por tempo de chegada | Alertas até 2 min mais tardios; atraso do GPS vira dado correto |
| Iceberg no lake | Tabelas diretas no analítico | Armazenamento duplicado; consultas pesadas saem do operacional |
| Particionamento por tripId | Partição por round-robin | Viagens longas concentram estado; paralelismo previsível |
| Estado em RocksDB | Estado em heap | Latência de acesso maior; escala para dezenas de GB por instância |
A expressão exactly-once merece honestidade técnica. A garantia real cobre o caminho do checkpoint até os sinks transacionais; a borda de entrada é at-least-once, e o dedupe por chave no job compensa a diferença. Essa combinação entrega o efeito prático de cobrança sem duplicação com custo menor que exatidão end-to-end literal, e o time registrou a fronteira da garantia na documentação do contrato.
A escolha do Avro se sustenta em números. O payload em Avro ficou 40% menor que o JSON equivalente no mesmo volume, o que reduziu o custo de rede e de armazenamento quente na mesma proporção, e o schema registry permite ao consumidor novo validar o contrato antes do deploy. O custo é a necessidade de tooling: bibliotecas de serialização em cada linguagem e disciplina de compatibilidade que exige revisão em pull request.
O número de partições merece registro próprio. Vinte e quatro não vem de fórmula: cobre o paralelismo máximo do job com folga, e cada partição extra aumenta arquivos abertos nos brokers e overhead de gerência no Flink. O teto prático observado pela equipe de plataforma é de 4.000 partições por cluster antes da gerência de metadados pesar, e o desenho guarda distância confortável desse limite.
Escalabilidade e falhas
O lag do consumidor é a métrica central do pipeline, medida por tópico e por subtarefa, com alerta quando ultrapassa 60 segundos. O rescale do Flink segue o lag em modo reativo: o paralelismo sobe de 24 para 48 no pico de sexta, e volta com escala de descida conservadora, porque rebalancear custa estado em trânsito. O backpressure é auto-regulado pelo próprio Flink, e a pressão aparece no painel como tempo de checkpoint esticado, que é o primeiro sintoma antes do lag subir.
As falhas têm caminho previsto. Um broker fora não interrompe nada com fator de replicação 3 e duas réplicas em sync, e a recuperação dele é monitorada por taxa de re-replicação para não competir com o tráfego. Eventos inválidos, cerca de 0,3% do volume, saem por side output para uma DLQ com inspetor semanal, e nunca param o pipeline. A evolução de contrato segue regra de duas fases: campo novo entra com valor padrão, consumidores migram, e a remoção só acontece quando nenhum consumidor ativo cita o campo, verificado no registry.
| Métrica | Limiar de alerta | Primeira ação |
|---|---|---|
| Lag por subtarefa | 60 s | Aumentar paralelismo |
| Duração de checkpoint | 12 s | Redistribuir estado |
| Taxa de DLQ | 1% do volume | Inspecionar produtor |
| Partições sub-replicadas | 1 | Verificar broker e disco |
| Disco dos brokers | 75% | Expandir ou drenar partição |
O estado do job é a peça mais sensível da escala. O backend RocksDB guarda o dedupe de 24 horas e as janelas abertas, cerca de 18 GB por instância no pico, e o checkpoint serializa esse estado para o S3 a cada 30 segundos. Quando o checkpoint passou de 12 segundos de duração, o time distribuiu o estado por chave e aumentou o paralelismo antes que o lag aparecesse, porque checkpoint lento é o sintoma que antecede a falha.
Backfill é operação de rotina na plataforma. Recálculos de faturamento por correção de regra rodam em namespace separado com paralelismo dedicado, e a versão idempotente do sink garante que o resultado substitui, e não soma. O maior backfill do ano reprocessou 90 dias de telemetria em 5 horas, fora do pico, sem afetar o fluxo ao vivo.
O dimensionamento dos brokers segue o disco, e não a CPU. O cluster carrega 9 TB de retenção quente replicada, com discos NVMe e taxa de escrita média de 90 MB por segundo, e a CPU fica abaixo de 30% mesmo no pico, então qualquer alerta de broker nasce de disco ou de rede, e nunca de processamento.
A expansão do pipeline já foi ensaiada. Dobrar partições de 24 para 48 exige chaveamento novo e reprocessamento do lake, então a capacidade ociosa por partição é guardada de propósito: o tópico opera com pico de 400 eventos por segundo por partição, muito abaixo do limite, e a folga absorve o crescimento por dois anos sem re-partição.
Lições aplicáveis
- A chave de partição é decisão estrutural: ordenação por entidade vale mais que balanceamento perfeito, e mudar chave depois custa reprocessamento completo.
- Exactly-once efetivo é meta alcançável: checkpoints com sinks transacionais e dedupe na borda entregam cobrança sem duplicação por fração do custo.
- Lag é o termômetro do pipeline: alerta por subtarefa com limiar em segundos pega o problema antes que o consumidor perceba.
- Schema registry previne incidente silencioso: a quebra de contrato passa a falhar no deploy, e não às 3 da manhã no consumidor de produção.
- Duplicar destino é mais barato que duplicar carga: o lake em Iceberg absorve as consultas pesadas, e o banco analítico permanece pequeno e rápido.
- Guarde folga nas partições: re-partição de tópico em produção é operação de alto risco, e a capacidade ociosa é seguro barato.
- Checkpoint lento é sintoma, e não incômodo: esticar além do intervalo é o aviso que antecede lag e falha, e a resposta é redistribuir estado.
- Separe telemetria de evento de negócio: retenção, custo e exatidão divergem por ordem de grandeza, e tópicos distintos evitam compensações ruins.
- Documente a fronteira da garantia: exatidão declarada sem o alcance definido gerou uma semana de debate com o time financeiro que a usa.