Kafka em Escala: Tiered Storage, KRaft e Exactly-Once
Ouvir estudo
gerado ao ouvirGerado apenas no primeiro play
Com tecnologia Amazon Polly + OmniVoice
Uma análise aprofundada da arquitetura interna do Apache Kafka — modelo de log/segmentos, eliminação do ZooKeeper via KRaft, offload de dados para S3 com Tiered Storage, semântica exactly-once e o que o Amazon MSK abstrai (e o que ele não abstrai). Um teardown técnico para quem precisa operar Kafka de verdade.
Kafka não é uma fila. Essa distinção importa mais do que parece — ela define como você dimensiona, como você retém dados, como você garante entrega e, principalmente, onde você vai errar em produção. Neste teardown, desmonto a arquitetura interna do Kafka a partir das fontes primárias: o modelo de log imutável, a transição do ZooKeeper para o KRaft, o Tiered Storage com offload para S3, a semântica exactly-once com producers idempotentes e transações, e o que o Amazon MSK realmente gerencia — e o que ainda fica na sua conta.
Ficha Técnica
- Sistema
- Apache Kafka + Amazon MSK
- Domínio
- Streaming de eventos / Plataforma de dados
- Versão de referência
- Kafka 3.x (KRaft GA: 3.3+, Tiered Storage GA: 3.6+)
- Escala típica (produção)
- Milhões de mensagens/segundo; retenção de TB a PB por cluster
- Stack principal
- Java/Scala (broker), Apache ZooKeeper (legado), KRaft (Raft interno), Amazon S3 (tiered storage via MSK)
- MSK — disponibilidade
- Multi-AZ por padrão; SLA AWS de 99,9%
- Mudança arquitetural chave
- Remoção do ZooKeeper (KIP-500/833); ZooKeeper deprecated no Kafka 3.5, removido no 4.0
- Fontes primárias
- kafka.apache.org/documentation, KIP-405, docs.aws.amazon.com/msk
O Problema Fundamental: Log, Não Fila
A abstração central do Kafka é o log de partição: uma sequência imutável, append-only, de registros ordenados por offset. Isso não é detalhe de implementação — é a decisão de design que explica tudo o mais. Filas tradicionais (SQS, RabbitMQ) deletam mensagens após o consumo; o Kafka retém. Isso permite múltiplos consumer groups lendo o mesmo dado de forma independente, reprocessamento determinístico e auditoria nativa. O preço é que o broker precisa gerenciar retenção ativa, e disco local vira gargalo em clusters com alto volume ou retenção longa.
Cada tópico é dividido em partições, cada partição replicada entre brokers conforme o fator de replicação configurado. O broker líder de cada partição aceita writes; os followers replicam de forma síncrona (se no ISR — In-Sync Replica set). Um producer escreve para o líder; o registro só é considerado committed quando todos os membros do ISR confirmam — esse é o contrato de durabilidade do Kafka. O parâmetro acks=all (ou acks=-1) junto com min.insync.replicas define o quórum mínimo de confirmação. Sem entender esse triângulo (líder, ISR, acks), qualquer discussão sobre exactly-once é prematura.
O modelo de segmentos é onde o Kafka ganha eficiência de I/O: cada partição no disco é uma sequência de segmentos (arquivos .log + índices de offset e timestamp). O broker só escreve no segmento ativo (tail); segmentos fechados são imutáveis. Isso permite sendfile() direto do filesystem cache para o socket do consumer — zero-copy — sem passar pelo heap da JVM. Em workloads de alto throughput, o Kafka frequentemente satura a rede antes do disco, justamente por esse design.
KRaft: Por Que o ZooKeeper Precisava Ir
O ZooKeeper foi a escolha pragmática de 2011: um serviço de coordenação distribuída já maduro, que o Kafka usou para armazenar metadados de cluster (quais brokers estão vivos, quais são os líderes de partição, configurações de tópicos, ACLs). Funcionou por uma década, mas criou um acoplamento arquitetural caro: dois sistemas distribuídos para operar, dois modelos de segurança para configurar, e um gargalo de escala real — o ZooKeeper armazenava todos os metadados em memória, e clusters com centenas de milhares de partições começavam a sentir latência no controller election e na propagação de metadados.
O KRaft (Kafka Raft Metadata) resolve isso via KIP-500: os próprios brokers Kafka passam a executar um protocolo Raft interno para consenso de metadados. Um subconjunto de brokers (ou nodes dedicados de controller) forma o quórum de controllers; o controller ativo mantém um log de metadados replicado — o mesmo modelo de log/segmento do Kafka, aplicado a si mesmo. Isso elimina o ZooKeeper como dependência operacional e, mais importante, remove o limite prático de partições por cluster: benchmarks da Confluent mostraram recuperação de controller failure em segundos com KRaft vs. dezenas de segundos com ZooKeeper em clusters grandes.
A transição não foi trivial. O KIP-500 foi proposto em 2019; KRaft ficou em early access no Kafka 2.8 (2021), atingiu GA no 3.3 (2022) para novos clusters, e o suporte a migração de ZooKeeper para KRaft chegou no 3.5. O ZooKeeper foi oficialmente removido no Kafka 4.0 (2024). No Amazon MSK, clusters KRaft estão disponíveis a partir do Kafka 3.6 no MSK. O ponto operacional crítico: em modo KRaft, os controller nodes precisam de discos confiáveis e baixa latência de rede entre si — eles são o novo plano de controle do cluster. Em MSK, isso é abstraído; em clusters self-managed, é uma decisão de infraestrutura explícita.
Arquitetura Kafka: Log, KRaft, Tiered Storage e Consumer Groups
Visão reconstruída da arquitetura interna do Kafka 3.6+ com KRaft e Tiered Storage habilitado no Amazon MSK. Mostra o fluxo de escrita (producer → líder → ISR), o offload de segmentos frios para S3, o plano de controle KRaft e o consumo via consumer groups.
- Producer A · idempotent + txn
- Producer B · acks=all
- Broker 1 · Partition Leader · (AZ-a)
- Broker 2 · ISR Follower · (AZ-b)
- Broker 3 · ISR Follower · (AZ-c)
- Segmentos Ativos · (hot — local disk)
- Segmentos Fechados · (cold — candidatos a offload)
- Amazon S3 · Segmentos Offloaded · (retenção longa)
- Tiered Storage · Plugin (RemoteLogManager)
- Controller 1 · (active — AZ-a)
- Controller 2 · (standby — AZ-b)
- Controller 3 · (standby — AZ-c)
- Consumer Group A · (analytics — offset latest)
- Consumer Group B · (audit — offset earliest · / S3 fetch)
- Group Coordinator · (broker-side)
Tiered Storage: Separando Hot de Cold no Log
O KIP-405 (Tiered Storage) resolve um problema econômico real: disco local em brokers Kafka é caro e não escala independentemente de compute. Em clusters com retenção de dias ou semanas, a maior parte dos dados está fria — raramente lida, mas precisa estar disponível para reprocessamento ou auditoria. Manter isso em EBS ou NVMe local é desperdício.
O mecanismo funciona assim: o RemoteLogManager (RLM), componente introduzido pelo KIP-405, monitora os segmentos fechados de cada partição. Quando um segmento atinge o critério de offload (configurável por tempo ou tamanho), o RLM faz upload do arquivo .log, dos índices de offset e timestamp, e dos metadados para o storage remoto — no caso do MSK, Amazon S3. Após confirmação do upload, o segmento local pode ser deletado, liberando disco. O broker mantém um mapa de metadados remotos que indica quais offsets estão em S3 vs. local.
Do ponto de vista do consumer, o protocolo é transparente: ele faz um FetchRequest normal para o broker líder. Se o offset solicitado está em storage remoto, o broker busca o dado no S3, faz o cache localmente (configurável), e retorna ao consumer. O consumer não sabe que o dado veio de S3. Isso é elegante, mas tem implicações de latência: um fetch de dado frio tem latência de S3 (tipicamente dezenas de ms) vs. disco local ou page cache (sub-ms). Para workloads de reprocessamento em lote, isso é aceitável; para consumidores que precisam de baixa latência em dados históricos, não é.
No Amazon MSK, o Tiered Storage é habilitado por configuração no cluster e usa S3 gerenciado pela AWS — você não configura o bucket diretamente, mas pode usar S3 Lifecycle Policies para controlar custo de armazenamento de longo prazo. A limitação atual: Tiered Storage no MSK não suporta todos os tipos de instância, e a compactação de log (log compaction) tem restrições com tiered storage habilitado — segmentos compactados não são elegíveis para offload em certas versões. Isso é crítico para tópicos de changelog (Kafka Streams, CDC).
Exactly-Once: Idempotência, Transações e o Custo Real
Exactly-once no Kafka é implementado em duas camadas complementares, e confundir as duas é um erro comum em entrevistas e em produção.
Camada 1 — Producer Idempotente: habilitado com enable.idempotence=true (default true desde Kafka 3.0). O broker atribui ao producer um ProducerID (PID) e rastreia o número de sequência por partição. Se o producer retransmite uma mensagem (por timeout ou falha de rede), o broker detecta o número de sequência duplicado e descarta — sem duplicatas no log. Isso garante exactly-once por partição, por sessão de producer. Custo: praticamente zero — é bookkeeping no broker, sem coordenação distribuída adicional.
Camada 2 — Transações: habilitado com transactional.id no producer. O producer coordena com um Transaction Coordinator (um broker especial eleito por hash do transactional.id) para abrir, commitar ou abortar uma transação que pode abranger múltiplas partições e múltiplos tópicos. O mecanismo usa um two-phase commit interno: o coordinator grava o estado da transação em um tópico interno (__transaction_state) antes de instruir os brokers a marcar as mensagens como committed. Consumers com isolation.level=read_committed só veem mensagens de transações commitadas — mensagens de transações abertas ou abortadas são filtradas.
O custo real das transações é não-trivial: latência adicional de round-trip para o Transaction Coordinator, overhead de escrita no __transaction_state, e o risco de transações zumbis — producers que falharam sem abortar a transação, deixando-a aberta até o transaction.timeout.ms. Em sistemas de alto throughput, transações muito grandes (muitas mensagens em uma transação) ou muito frequentes (uma transação por mensagem) degradam throughput significativamente. A recomendação prática: batch suas transações por janela de tempo ou contagem de registros, não por mensagem individual.
Um ponto frequentemente ignorado: exactly-once end-to-end (producer → broker → consumer → sink) requer que o sink também seja idempotente ou transacional. O Kafka garante exactly-once no log; se o consumer processa e grava em um banco sem idempotência, você ainda tem at-least-once no sistema como um todo. O Kafka Streams implementa exactly-once end-to-end usando transações Kafka para coordenar leitura, processamento e escrita atomicamente — mas isso só funciona quando source e sink são ambos Kafka.
Consumer Groups, Rebalance e o Problema do Stop-the-World
O modelo de consumer group é o que permite Kafka escalar o consumo horizontalmente: cada partição é atribuída a exatamente um consumer dentro do grupo, e múltiplos grupos podem consumir o mesmo tópico independentemente. O Group Coordinator (um broker eleito por hash do group.id) gerencia o ciclo de vida do grupo: join, sincronização de assignment, heartbeats e detecção de falhas.
O problema clássico é o rebalance: sempre que um consumer entra, sai, ou para de enviar heartbeats dentro de session.timeout.ms, o coordinator dispara um rebalance. No protocolo original (Eager Rebalance), todos os consumers do grupo param de consumir, revogam todas as partições, e aguardam o novo assignment. Em grupos grandes com muitas partições, isso pode causar pausas de segundos a dezenas de segundos — um stop-the-world distribuído.
O Kafka 2.4 introduziu o Cooperative Sticky Assignor (KIP-429/KIP-482), que implementa rebalance incremental: apenas as partições que precisam ser movidas são revogadas; as demais continuam sendo consumidas durante o rebalance. Isso reduz drasticamente o impacto, mas requer que todos os consumers do grupo usem um assignor cooperativo — um detalhe de configuração que frequentemente é esquecido em migrações.
Outro ponto operacional crítico: max.poll.interval.ms define o tempo máximo entre duas chamadas a poll(). Se o processamento de um batch demora mais que esse intervalo (processamento pesado, chamadas externas lentas), o consumer é considerado morto pelo coordinator e um rebalance é disparado — mesmo que o consumer esteja vivo e processando. Isso cria um loop de rebalances em workloads com processamento variável. A solução não é aumentar max.poll.interval.ms indefinidamente (isso atrasa a detecção de falhas reais), mas reduzir o tamanho do batch via max.poll.records para garantir que o processamento caiba dentro do intervalo configurado. No Amazon MSK, esses parâmetros são configuráveis por consumer — o MSK não os gerencia; eles ficam inteiramente na sua aplicação.
Trade-offs Arquiteturais: Decisões Chave no Design do Kafka
KRaft vs. ZooKeeper
- Elimina dependência operacional do ZooKeeper
- Recovery de controller em segundos (vs. dezenas de segundos)
- Suporte a clusters com milhões de partições
- Modelo de segurança unificado
- Migração de clusters existentes requer procedimento específico (3.5+)
- Controller nodes precisam de discos confiáveis em self-managed
- Ecossistema de tooling ainda em adaptação (monitoramento, etc.)
Adote KRaft para novos clusters. Sem exceções.
Tiered Storage (S3) vs. Retenção Local
- Custo de armazenamento drasticamente menor para dados frios
- Escala de retenção independente de compute
- Transparente para consumers (sem mudança de protocolo)
- Latência de fetch para dados frios (dezenas de ms vs. sub-ms)
- Incompatibilidade com log compaction em certas versões
- Custo de API S3 (GET/PUT) pode surpreender em alta frequência de offload
Ideal para retenção longa (>24h) com acesso esporádico a dados históricos. Evite em tópicos compactados.
Exactly-Once Transactions vs. At-Least-Once + Idempotência no Sink
- Garantia nativa no broker; sem lógica de deduplicação no consumer
- Necessário para Kafka Streams EOS
- Latência adicional (round-trip ao Transaction Coordinator)
- Throughput reduzido se transações muito frequentes ou grandes
- Não garante EOS end-to-end se o sink não for idempotente
Use transações onde o custo de duplicata é alto (financeiro, CDC). Para analytics, at-least-once + idempotência no sink é mais simples e mais rápido.
Amazon MSK vs. Kafka Self-Managed (EC2/EKS)
- MSK: sem overhead de gestão de brokers, patches, ZK/KRaft
- MSK: integração nativa com IAM, VPC, CloudWatch, S3 (tiered)
- MSK: Multi-AZ por design; failover automático de broker
- MSK: menos controle sobre configurações de broker (algumas são fixas)
- MSK: custo mais alto que EC2 equivalente em escala muito grande
- Self-managed: flexibilidade total, mas overhead operacional real
MSK para a maioria dos casos. Self-managed apenas se você tem equipe dedicada de plataforma e requisitos que o MSK não suporta.
Leitura Well-Architected: Kafka / Amazon MSK
Segurança
O Kafka tem um modelo de segurança multidimensional: autenticação (SASL/PLAIN, SCRAM, mTLS, IAM no MSK), autorização (ACLs por recurso — tópico, grupo, cluster), e criptografia em trânsito (TLS) e em repouso (EBS encryption no MSK). O ponto crítico: ACLs no Kafka são aditivas e granulares — um producer mal configurado com permissão de WRITE em * (todos os tópicos) é um vetor de injeção de dados. No MSK com IAM auth, as políticas IAM substituem as ACLs internas para autenticação, mas as ACLs ainda podem coexistir.
Confiabilidade
Confiabilidade no Kafka é função direta de três configurações: fator de replicação (≥3 em produção), min.insync.replicas (≥2, tipicamente RF-1), e acks=all no producer. O MSK gerencia failover de broker automaticamente, mas o tempo de eleição de líder ainda impacta producers durante o failover — retries e retry.backoff.ms no producer devem ser configurados para absorver esse intervalo. Com KRaft, o controller election é mais rápido, reduzindo a janela de indisponibilidade.
Eficiência de performance
O Kafka é otimizado para throughput, não para latência de mensagem individual. O mecanismo de batching no producer (linger.ms, batch.size) é fundamental: aumentar linger.ms de 0 para 5-20ms pode multiplicar o throughput por 10x às custas de latência de ponta a ponta. Compressão (snappy ou lz4) reduz uso de rede e disco com custo de CPU mínimo em dados textuais/JSON. No MSK, o tipo de instância define o throughput máximo de rede — instâncias kafka.m5.4xlarge e superiores têm Enhanced Networking habilitado.
Otimização de custos
O custo do MSK tem três componentes: instâncias de broker (por hora), armazenamento EBS (por GB/mês), e transferência de dados (intra-AZ é gratuita; inter-AZ tem custo). Tiered Storage adiciona custo de S3 (armazenamento + API), mas tipicamente reduz o custo total ao permitir instâncias menores com menos EBS. O custo oculto mais comum: retenção excessiva em tópicos de alto volume sem Tiered Storage — times que configuram retention.ms=7d em tópicos de 100MB/s precisam de ~60TB de EBS por réplica.
Depois de operar e projetar sistemas sobre Kafka em contextos financeiros, há três padrões de erro que vejo repetidamente — e que eu abordaria de forma diferente.
1. Tiered Storage como afterthought, não como design inicial. A maioria dos times habilita Tiered Storage depois que o custo de EBS já está alto e a migração é dolorosa. Eu definiria a política de tiered storage na criação do tópico: local.retention.ms para hot tier (ex: 6-24h), retention.ms para o total (ex: 30d em S3). Isso é uma decisão de dia zero, não de dia 90.
2. Exactly-once mal aplicado. Vejo times habilitando transactional.id em todos os producers por "segurança", sem entender o custo. Em sistemas financeiros, exactly-once é necessário em fluxos de pagamento e reconciliação — não em telemetria ou logs de auditoria onde at-least-once + deduplicação no sink é mais eficiente. A decisão deve ser por fluxo, não por cluster.
3. Consumer group design negligenciado. Times criam um consumer group por serviço e colocam múltiplas responsabilidades nele. Eu separo consumer groups por responsabilidade de processamento — um grupo para processamento em tempo real (baixo max.poll.records, alta prioridade de SLA), outro para batch/analytics (alto max.poll.records, tolerante a lag). Isso permite tuning independente e evita que um consumer lento em processamento pesado cause rebalances que afetam o fluxo em tempo real.
O ponto mais importante que eu enfatizaria para qualquer time adotando Kafka: o Kafka resolve o problema de transporte e retenção; ele não resolve o problema de contrato de dados. Sem um schema registry (Confluent Schema Registry ou AWS Glue Schema Registry) com evolução de schema versionada, você vai quebrar consumers em produção em algum momento. Isso não é opcional em sistemas de produção sérios.
Veredicto
O Apache Kafka é uma das arquiteturas de software mais bem projetadas dos últimos 15 anos — o modelo de log imutável é simples, correto e escala de formas que sistemas de fila tradicionais não conseguem. A transição para KRaft remove o último grande ponto de fragilidade operacional (o ZooKeeper) sem comprometer o modelo de consistência. O Tiered Storage resolve o problema econômico de retenção longa de forma elegante, com trade-off de latência bem definido e documentado. O exactly-once é genuinamente útil e corretamente implementado no nível do broker — mas é frequentemente mal aplicado em produção, gerando overhead desnecessário ou, pior, uma falsa sensação de segurança quando o sink não é idempotente. O Amazon MSK é a escolha certa para a maioria dos times: elimina o overhead operacional de gestão de brokers e integra nativamente com o ecossistema AWS (IAM, VPC, S3, CloudWatch). A limitação real do MSK não é técnica — é que ele abstrai o suficiente para que times menos experientes subestimem o que ainda é responsabilidade deles: tuning de producer/consumer, design de consumer groups, schema management, e monitoramento de consumer lag.
Referências
Post-mortems, ADRs e deep dives de arquitetura no seu email — do jeito que um arquiteto lê.
Sem spam · cancele quando quiser
Pergunte ao Fernando sobre isto
Receba uma resposta focada sobre este estudo do meu assistente de IA, baseada no meu trabalho.
Participe da conversa
Entre para comentar
Confirme seu e-mail para participar — você também recebe a newsletter. Sem senha.