# Kafka em Escala: Tiered Storage, KRaft e Exactly-Once

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.

- URL: https://fernando.moretes.com/studies/kafka-tiered-storage-kraft-exactly-once

- Markdown: https://fernando.moretes.com/studies/kafka-tiered-storage-kraft-exactly-once/study.md?lang=pt

- Type: Teardown

- Company: Apache Kafka / Amazon MSK

- Domain: Streaming

- Date: 2024-06-01

- Tags: kafka, streaming, event-driven, amazon-msk, tiered-storage, kraft, exactly-once, data-platform

- Reading time: 10 min

---

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.

### 👤 Producers

- Producer A idempotent + txn (external)
- Producer B acks=all (external)

### ⚙️ MSK Broker Plane (Multi-AZ)

- Broker 1 Partition Leader (AZ-a) (messaging)
- Broker 2 ISR Follower (AZ-b) (messaging)
- Broker 3 ISR Follower (AZ-c) (messaging)

### 🗂️ Local Storage (EBS/NVMe)

- Segmentos Ativos (hot, local disk) (storage)
- Segmentos Fechados (cold, candidatos a offload) (storage)

### ☁️ Tiered Storage (S3)

- Amazon S3 Segmentos Offloaded (retenção longa) (storage)
- Tiered Storage Plugin (RemoteLogManager) (compute)

### 🔐 KRaft Controller Quorum

- Controller 1 (active: AZ-a) (compute)
- Controller 2 (standby: AZ-b) (compute)
- Controller 3 (standby: AZ-c) (compute)

### 📦 Consumer Groups

- Consumer Group A (analytics, offset latest) (external)
- Consumer Group B (audit, offset earliest / S3 fetch) (external)
- Group Coordinator (broker-side) (messaging)

### Fluxos

- p1 -> b1: write (acks=all)
- p2 -> b1: write (idempotente)
- b1 -> b2: replicação ISR
- b1 -> b3: replicação ISR
- b1 -> seg_hot: append segmento ativo
- seg_hot -> seg_cold: segmento fechado (roll)
- seg_cold -> ts_plugin: offload trigger
- ts_plugin -> s3: upload segmento + índice
- seg_cold -> ts_plugin: delete local após offload
- kc1 -> b1: metadata / leader election
- kc1 -> kc2: Raft replication
- kc1 -> kc3: Raft replication
- cg1 -> b1: fetch (local)
- cg2 -> b1: fetch request
- b1 -> s3: fetch remoto (tiered)
- cg1 -> gc: heartbeat / rebalance
- cg2 -> gc: heartbeat / rebalance

## 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

**Pros**
- 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

**Cons**
- 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.)

**Verdict:** Adote KRaft para novos clusters. Sem exceções.

### Tiered Storage (S3) vs. Retenção Local

**Pros**
- Custo de armazenamento drasticamente menor para dados frios
- Escala de retenção independente de compute
- Transparente para consumers (sem mudança de protocolo)

**Cons**
- 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

**Verdict:** 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

**Pros**
- Garantia nativa no broker; sem lógica de deduplicação no consumer
- Necessário para Kafka Streams EOS

**Cons**
- 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

**Verdict:** 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)

**Pros**
- 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

**Cons**
- 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

**Verdict:** 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

- **security**: 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.
- **reliability**: 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.
- **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.
- **cost**: 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.

> **O que eu faria diferente, e o que o mercado ainda erra:** 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

- [Apache Kafka: Official Documentation](https://kafka.apache.org/documentation/)
- [KIP-405: Kafka Tiered Storage](https://cwiki.apache.org/confluence/display/KAFKA/KIP-405%3A+Kafka+Tiered+Storage)
- [Amazon MSK: AWS Documentation](https://docs.aws.amazon.com/msk/)
- [KIP-500: Replace ZooKeeper with a Self-Managed Metadata Quorum](https://cwiki.apache.org/confluence/display/KAFKA/KIP-500%3A+Replace+ZooKeeper+with+a+Self-Managed+Metadata+Quorum)
- [KIP-429: Kafka Consumer Incremental Rebalance Protocol](https://cwiki.apache.org/confluence/display/KAFKA/KIP-429%3A+Kafka+Consumer+Incremental+Rebalance+Protocol)
- [Kafka Exactly-Once Semantics: Confluent Blog](https://www.confluent.io/blog/exactly-once-semantics-are-possible-heres-how-apache-kafka-does-it/)
- [Amazon MSK Tiered Storage: AWS Docs](https://docs.aws.amazon.com/msk/latest/developerguide/msk-tiered-storage.html)

## Fontes do caso

- [Apache Kafka: Documentation](https://kafka.apache.org/documentation/)
- [KIP-405: Kafka Tiered Storage](https://cwiki.apache.org/confluence/display/KAFKA/KIP-405%3A+Kafka+Tiered+Storage)
- [Amazon MSK: AWS docs](https://docs.aws.amazon.com/msk/)
