trilha

Tecnologias/03 - Filas/01 - Kafka8 min

Armazenamento, ISR e KRaft

Conceito

Esta nota abre o motor: como a partição vive em disco, como a replicação decide o que está seguro, e o que o KRaft substituiu quando o ZooKeeper saiu no Kafka 4.0.

A partição em disco: segmentos

Uma partição é um diretório, e dentro dele arquivos de segmento:

/var/lib/kafka/pedidos-0/
├── 00000000000000000000.log        ← as mensagens (append-only)
├── 00000000000000000000.index      ← offset  → posição no .log (ESPARSO)
├── 00000000000000000000.timeindex  ← tempo   → offset          (ESPARSO)
├── 00000000000000368421.log        ← segmento ativo (o único aberto para escrita)
├── 00000000000000368421.index
└── leader-epoch-checkpoint
  • O nome do arquivo é o offset base do segmento. Achar o segmento de um offset é uma busca binária em nomes de arquivo.
  • Novo segmento nasce a cada segment.bytes (1 GB, padrão) ou segment.ms.
  • Retenção apaga segmentos inteiros, nunca mensagens individuais — é por isso que apagar é barato, e por isso a retenção real é sempre "um pouco mais" que o configurado.

Índice esparso: por que a busca é rápida e a RAM é pouca

O .index não tem uma entrada por mensagem. Ele tem uma a cada index.interval.bytes (4 KB, padrão):

buscar offset 368_500:
1. busca binária nos nomes de arquivo → segmento 368421
2. busca binária no .index            → "offset 368480 está no byte 12.288"
3. leitura sequencial do .log a partir do byte 12.288 até achar 368500

Índice denso custaria gigabytes de RAM; o esparso custa megabytes e paga com uma varredura curtíssima e sequencial. O .timeindex faz o mesmo para offsetsForTimes — que é o que permite "me dê tudo a partir de ontem às 14h" (3. Partições, Offsets e Consumer Groups).

Por que é rápido: page cache e zero-copy

O Kafka não tem cache próprio. Ele grava em arquivo e deixa o page cache do SO trabalhar:

produtor → write() → page cache → (o SO decide quando gravar no disco)
consumidor recente → lê do page cache: nunca toca o disco

E na entrega, sendfile() copia do page cache direto para o socket, sem passar pelo espaço de usuário:

sem zero-copy: disco → kernel → JVM → kernel → NIC   (4 cópias, 2 troca de contexto)
com sendfile:  disco → kernel → NIC                  (zero cópias em user space)

Consequências práticas que surpreendem:

  • Dê RAM ao SO, não à JVM. Heap de 6 GB é suficiente para a maioria dos brokers; o resto da RAM deve ficar livre para page cache. Heap gigante é desperdício e mais GC.
  • TLS e compressão no broker anulam parte do zero-copy (é preciso processar os bytes em user space). É um custo real de ligar criptografia em trânsito.

Replicação: leader, follower, ISR

partição pedidos-0, replication.factor=3
├── broker 1: LEADER      (recebe toda escrita e leitura)
├── broker 2: follower    ← faz fetch do leader, como um consumidor
└── broker 3: follower

ISR (In-Sync Replicas) = leader + followers que estão "em dia"
                         (dentro de replica.lag.time.max.ms, padrão 30 s)

E os dois marcadores que governam a visibilidade:

Marcador Significa
LEO (Log End Offset) o fim do log de cada réplica
HW (High Watermark) o maior offset replicado em todas as ISR

Consumidor só lê até o High Watermark. Mensagem que está no leader mas ainda não foi replicada é invisível — é assim que o Kafka evita que você leia algo que uma falha de leader pode desfazer.

A matriz de durabilidade sai daí:

acks O produtor espera Perde mensagem?
0 nada sim, sem aviso
1 o leader gravou sim, se o leader cair antes de replicar
all todas as ISR gravaram não, se min.insync.replicas ≥ 2

E a combinação canônica:

replication.factor = 3
min.insync.replicas = 2       # com acks=all: 2 réplicas devem confirmar
acks = all                    # (no produtor)
unclean.leader.election.enable = false

Com isso: tolera um broker fora sem perder escrita e sem parar. Com min.insync.replicas = 3, perder um broker para a escrita (NotEnoughReplicas) — mais seguro, menos disponível (Availability vs Consistency).

unclean.leader.election.enable = true perde dados por projeto

Ele permite que uma réplica fora do ISR (atrasada) se torne leader quando todas as ISR estão fora. O cluster volta a aceitar escrita, e tudo que existia além do log dessa réplica desaparece. É escolher disponibilidade sacrificando durabilidade — e o padrão false está certo para quase todo mundo.

Leader epoch: o detalhe que corrigiu divergência silenciosa

Cada troca de leader incrementa um epoch. Um follower que volta pergunta "qual era o fim do log no epoch N?" antes de truncar o próprio log. Sem isso (Kafka antigo), havia cenários de divergência silenciosa entre réplicas após falhas sucessivas. É o tipo de correção invisível que faz um sistema distribuído ser confiável.

KRaft: o log de metadados

Antes do 4.0, o ZooKeeper guardava metadados (tópicos, partições, ISR, ACLs) e elegia o controller. O KRaft substitui isso por um log Raft interno:

tópico interno __cluster_metadata (replicado por Raft entre os controllers)
├── controller ativo  = o LÍDER Raft
└── controllers em espera = seguidores, com o estado já em memória

Ganhos concretos:

ZooKeeper (≤3.x) KRaft (4.0+)
Sistemas a operar dois um
Failover de controller recarregava metadados do ZK (lento) seguidor já tem o estado: quase instantâneo
Escala de partições ~200 mil por cluster milhões
Propagação de metadados notificações (watches) os brokers consomem o log

Os brokers agora acompanham metadados como se fossem consumidores de um tópico — o Kafka passou a usar a própria abstração para se gerenciar. Se você encontrar documentação com zookeeper.connect, ela é anterior ao 4.0 e não se aplica mais (2. Kafka).

Tiered storage

Segmentos antigos são movidos para object storage (S3, GCS), mantendo só os recentes em disco local:

disco local:  últimas horas/dias (leitura quente, zero-copy)
S3:           semanas/meses (leitura fria, sob demanda)

Isso desacopla retenção de custo de disco local e é o que torna viável guardar meses de log — o pré-requisito prático para tratar o Kafka como fonte da verdade e não só como transporte.

Trade-offs

Page cache é brilhante e frágil ao padrão de acesso. Consumidor em tempo real lê da RAM. Um consumidor que faz replay de 3 dias atrás varre o disco e suja o page cache, degradando os consumidores em tempo real. Mitigações: réplicas dedicadas a replay, follower fetching, ou tiered storage.

Segmento grande é eficiente e granular demais para retenção. Com segment.bytes = 1 GB e pouco volume, um segmento pode levar dias para fechar — e a retenção só age em segmento fechado. Em tópicos de baixo volume com retenção curta, reduza segment.ms.

min.insync.replicas é o botão que ninguém quer configurar e todos precisam. É literalmente onde você escolhe entre "perder mensagem" e "parar de aceitar mensagem". Não existe resposta universal; existe resposta por tópico.

O trade-off central: o Kafka obtém throughput extremo delegando ao SO (arquivo sequencial, page cache, sendfile) e ao operador (ISR, acks, retenção). É simples por dentro e exigente por fora: as decisões que outros sistemas tomam para você, aqui são configuração explícita — e cada uma delas é uma decisão de durabilidade.

Exemplo prático

Configuração por criticidade do tópico

# Tópico crítico (financeiro): durabilidade acima de tudo
kafka-topics.sh --create --topic pagamentos --partitions 12 \
  --replication-factor 3 \
  --config min.insync.replicas=2 \
  --config retention.ms=2592000000 \
  --config compression.type=zstd \
  --config unclean.leader.election.enable=false

# Tópico de estado (compactado): o último valor de cada chave, para sempre
kafka-topics.sh --create --topic perfis --partitions 6 --replication-factor 3 \
  --config cleanup.policy=compact \
  --config min.cleanable.dirty.ratio=0.5 \
  --config delete.retention.ms=86400000     # janela para tombstones serem vistos

# Tópico de telemetria: volume alto, perder é aceitável
kafka-topics.sh --create --topic metricas --partitions 24 --replication-factor 2 \
  --config min.insync.replicas=1 \
  --config retention.ms=86400000 \
  --config segment.ms=3600000

Verificar saúde de replicação

# partições sub-replicadas: o alerta nº 1 de um cluster Kafka
kafka-topics.sh --bootstrap-server kafka:9092 --describe --under-replicated-partitions
# vazio = saudável. Qualquer linha = uma réplica saiu do ISR.

# partições sem leader: indisponibilidade real
kafka-topics.sh --bootstrap-server kafka:9092 --describe --unavailable-partitions

# quorum de metadados do KRaft
kafka-metadata-quorum.sh --bootstrap-server kafka:9092 describe --status
kafka-metadata-quorum.sh --bootstrap-server kafka:9092 describe --replication

Métricas JMX que valem alerta:

Métrica Alerta
UnderReplicatedPartitions > 0 por mais de alguns minutos
OfflinePartitionsCount > 0 (sempre)
ActiveControllerCount ≠ 1 no cluster (0 = sem controller; 2 = split brain)
IsrShrinksPerSec picos frequentes = broker ou disco com problema
RequestHandlerAvgIdlePercent < 30% = brokers saturados
MaxLag / consumer lag crescendo (3. Partições, Offsets e Consumer Groups)

Como isso falha em produção

Sintoma Causa
UnderReplicatedPartitions > 0 broker lento/fora, disco saturado, rede entre brokers
NotEnoughReplicasException no produtor ISR abaixo de min.insync.replicas: o tópico está protegendo o dado
Mensagem perdida após falha de broker acks=1, ou unclean.leader.election ligado
Latência dos consumidores em tempo real degradou alguém iniciou um replay e sujou o page cache
Disco cheio mesmo com retenção curta segmento ativo nunca fecha (segment.ms alto e volume baixo)
Tombstone "não apagou" o registro delete.retention.ms expirou antes do consumidor ler
Broker com GC longo heap grande demais; devolva RAM ao page cache
Cluster sem controller ativo quorum KRaft sem maioria (menos de 2 de 3 controllers)

Aprofundamento aplicado

Réplicas de dados e quorum de metadados

O quorum de controllers mantém decisões de metadados no KRaft. Réplicas das partições mantêm registros dos tópicos. Um controller saudável não significa que todas as partições possuam réplicas suficientes para aceitar escritas com a garantia exigida.

Cenário: fator de replicação 3, acks=all e min.insync.replicas=2. Se só uma réplica permanece no ISR, o produtor deve lidar com falha de escrita nessa configuração. Relaxar a exigência recupera disponibilidade ao alterar o risco aceito. Referência: replicação.

Diagnóstico por cadeia

Ao ver aumento de latência no produtor, compare partições afetadas, ISR, armazenamento, rede e erros. Ao ver atraso de consumo, verifique se ele nasce no processamento do cliente ou na disponibilidade dos dados. “O Kafka está lento” junta problemas distintos.

Exercício: represente perda de um broker e perda da maioria de controllers. Resultado esperado: explicar qual responsabilidade é afetada em cada caso, sem inferir que dois quorums diferentes são o mesmo mecanismo.

Relacionado


Parte de MOC Kafka · Design (Kafka docs)

Buscar

Busca por título, seção e texto das notas