Mensageria e streaming de eventos
Arquitetura do Apache Kafka: topics, partições, replicação, consumer groups e KRaft
Intermediário12 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
O Kafka não é uma fila com recursos a mais: é um log particionado e replicado que os consumidores leem no próprio ritmo. Este guia constrói esse modelo do zero — registros, partições, offsets, segmentos, retenção e compactação, réplicas líder e seguidoras, o conjunto de réplicas sincronizadas, consumer groups e rebalanceamento, e o quórum de metadados KRaft que substituiu o ZooKeeper — para que as configurações do producer, a operação e as decisões de design façam sentido depois.
O que você vai aprender
- Explicar por que o Kafka é um log e não uma fila, e o que isso muda para os consumidores
- Descrever partições, offsets, segmentos, retenção e compactação, e como eles interagem
- Explicar a replicação líder/seguidor, o conjunto de réplicas sincronizadas, acks e min.insync.replicas
- Descrever consumer groups, commits de offset e rebalanceamento, e o efeito deles na ordenação e no paralelismo
- Explicar o papel do quórum de controllers KRaft e como ele substituiu o ZooKeeper
- Definir o número de partições, as chaves e o fator de replicação de um novo topic
O Apache Kafka surgiu como uma forma de transportar logs de atividade na escala do LinkedIn e virou a espinha dorsal padrão de event streaming. Ele costuma ser apresentado como "uma fila de mensagens que escala", uma descrição enganosa que leva muita gente a usá-lo como se fosse uma. O Kafka é um commit log distribuído: uma sequência de registros apenas de inserção (append-only), dividida em partições espalhadas pelos brokers, replicada por segurança, retida por um tempo configurado e lida por consumidores que guardam a própria posição. Todo o resto decorre desse design. Este guia trata do Kafka 3.x em modo KRaft e considera o ZooKeeper coisa do passado.
Registros, topics e partições
Um registro (record) é composto por uma chave, um valor, um timestamp e headers opcionais. Para o Kafka, chave e valor são arrays de bytes; a serialização é responsabilidade exclusiva do cliente. Um topic é uma categoria nomeada de registros, dividida em uma ou mais partições. Cada partição é uma sequência ordenada e imutável: um registro adicionado à partição 3 recebe o próximo offset da partição 3, um número de 64 bits que nunca muda e nunca se repete dentro daquela partição.
Três consequências moldam qualquer design com Kafka:
- A ordem existe dentro de uma partição e em nenhum outro lugar. Dois registros em partições diferentes não têm ordem definida.
- O paralelismo é limitado pelo número de partições. Uma partição é uma unidade de armazenamento em um broker e uma unidade de consumo para um único consumidor de um grupo.
- A chave decide a partição. O particionador padrão calcula o hash da chave e aplica o módulo pelo número de partições, então todos os registros de
customer-4711caem na mesma partição e, portanto, são consumidos em ordem. Registros com chave nula são distribuídos entre as partições em lotes.
topic "orders" with 3 partitions
partition 0 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | ← offsets; new records append on the right
partition 1 | 0 | 1 | 2 | 3 |
partition 2 | 0 | 1 | 2 | 3 | 4 | 5 |
key "customer-4711" → hash → partition 1, always
A posição de um registro é totalmente identificada por (topic, partição, offset), e é essa tripla que um consumidor armazena como progresso. Não existe confirmação por registro, nem estado "não confirmado", nem fila de reentrega; existe apenas "o consumer group leu até aqui".
Segmentos, retenção e compactação
Em disco, uma partição é um diretório de arquivos de segmento. O segmento ativo recebe as inserções; quando atinge log.segment.bytes (1 GB por padrão) ou log.roll.ms, ele é fechado e um novo começa. Cada segmento tem ao lado um índice de offsets e um índice de tempo, de modo que um consumidor que pede "o offset 1.234.567" ou "o primeiro registro depois das 09:00" é atendido com um par de seeks em vez de uma varredura. As escritas são inserções sequenciais que passam pelo page cache do sistema operacional, e as leituras de dados recentes são servidas desse cache; é isso, mais do que qualquer código engenhoso, que faz o Kafka movimentar centenas de megabytes por segundo por broker em discos comuns.
Os registros são removidos pela retenção, não pelo consumo. Existem duas políticas por topic:
cleanup.policy | Regra | Uso |
|---|---|---|
delete (padrão) | Descarta segmentos inteiros mais antigos que retention.ms (7 dias por padrão) ou além de retention.bytes | Fluxos de eventos, logs, qualquer coisa limitada no tempo |
compact | Mantém pelo menos o registro mais recente de cada chave; registros mais antigos com a mesma chave são removidos em segundo plano | Changelogs, estado atual por entidade, configuração, o topic interno de offsets |
compact,delete | As duas: compacta e também descarta dados mais antigos que a retenção | Changelogs que não devem manter para sempre chaves mortas há muito tempo |
A compactação transforma um topic em um snapshot chave-valor que pode ser reconstruído lendo-o desde o início; é assim que os state stores do Kafka Streams e muitos serviços de "views materializadas" se recuperam depois de um restart. Um registro com valor nulo é um tombstone: ele marca a chave como apagada e é removido depois de delete.retention.ms. A compactação só atua em segmentos fechados e roda quando a proporção de dados obsoletos justifica, então um topic compactado sempre contém algumas duplicatas; quem lê precisa contar com isso.
É a retenção que permite reprocessar o log. Um consumer group novo pode começar do offset mais antigo e processar uma semana de histórico; um grupo que encontrou um bug pode voltar para ontem e reprocessar. Nenhuma fila oferece isso, e esse é o motivo mais forte para escolher o Kafka.
Brokers, replicação e o conjunto de réplicas sincronizadas
Um broker é um processo servidor do Kafka. Cada partição tem um fator de replicação — três é o padrão em produção — e suas réplicas ficam em outros tantos brokers. Uma das réplicas é a líder (leader): todas as requisições de produção e, por padrão, todas as de fetch daquela partição vão para ela. As outras são seguidoras (followers) que fazem fetch continuamente da líder para se manterem atualizadas, exatamente como consumidores.
A líder acompanha quais seguidoras estão em dia. Uma seguidora que buscou até o fim do log da líder dentro de replica.lag.time.max.ms (30 segundos por padrão) faz parte do conjunto de réplicas sincronizadas (ISR). O ISR importa por dois motivos:
- Um registro está commitado quando todas as réplicas do ISR o gravaram. Os consumidores só conseguem ler registros commitados, até o high watermark.
- Se a líder falhar, o controller elege uma nova líder a partir do ISR, o que garante que nenhum registro commitado seja perdido. Com
unclean.leader.election.enable=false(o padrão), uma réplica fora de sincronia nunca é escolhida, então uma partição com o ISR vazio fica indisponível em vez de perder dados.
A configuração acks do producer decide quanto tempo ele espera:
acks | O producer retorna quando | Risco |
|---|---|---|
0 | O registro foi entregue à rede | Perda em qualquer falha; nem mesmo um erro do broker é reportado |
1 | A líder o gravou | Perda se a líder falhar antes de as seguidoras buscarem o registro |
all (padrão desde a 3.0) | Todos os membros do ISR o gravaram | Nenhum por falha de broker — desde que o ISR tenha mais de um membro |
Essa última ressalva é o motivo de existir min.insync.replicas. Com fator de replicação 3 e min.insync.replicas=2, a líder rejeita escritas com acks=all quando há menos de duas réplicas sincronizadas, trocando disponibilidade pela garantia de que um registro confirmado existe em pelo menos dois brokers. A combinação comum em produção é fator de replicação 3, min.insync.replicas 2 e acks=all: um broker pode ficar fora para manutenção sem bloquear os producers, e nenhum registro confirmado depende de uma única máquina.
Desde o Kafka 2.4, dá para fazer os consumidores lerem da réplica mais próxima (replica.selector.class no broker e client.rack no consumidor), o que reduz o tráfego entre zonas em implantações na nuvem.
Consumer groups e offsets
Um consumer group é um conjunto de instâncias consumidoras que compartilham um group.id. O Kafka atribui cada partição dos topics assinados a exatamente um membro do grupo; um grupo com mais membros do que partições deixa os membros excedentes ociosos. Dois grupos independentes lendo o mesmo topic recebem, cada um, todos os registros — é assim que o Kafka oferece tanto divisão de trabalho (dentro de um grupo) quanto publish-subscribe (entre grupos).
O progresso de cada grupo é um conjunto de offsets commitados, um por partição, armazenados no topic interno __consumer_offsets (compactado, com chave composta por grupo, topic e partição). Um offset commitado é "o próximo registro a ler", então fazer commit do offset 42 significa que os registros de 0 a 41 estão concluídos. Os consumidores fazem commit periodicamente ou de forma explícita; ao reiniciar ou numa reatribuição, o novo dono da partição começa a partir do último offset commitado. Tudo o que estiver entre o último commit e a queda é lido de novo — isso é entrega at-least-once, e é por isso que o guia de consumidores dedica tanto espaço a quando fazer commit.
group "billing" partition 0: committed 1,204 partition 1: committed 988 partition 2: committed 1,377
group "analytics" partition 0: committed 1,190 partition 1: committed 980 partition 2: committed 1,377
log end offsets partition 0: 1,210 partition 1: 991 partition 2: 1,377
lag(billing) = (1,210 − 1,204) + (991 − 988) + 0 = 9 records
O consumer lag — offset de fim do log menos offset commitado, somado sobre as partições — é a métrica mais importante de uma aplicação consumidora. Lag crescente significa que os consumidores não estão dando conta ou pararam.
Rebalanceamento
Quando um membro entra ou sai, ou quando o número de partições do topic muda, o grupo passa por um rebalanceamento: o group coordinator (um broker escolhido a partir do hash do id do grupo) conduz um protocolo em que os membros chegam a um acordo sobre uma nova atribuição. Em um rebalanceamento eager clássico, cada membro abre mão de todas as suas partições e recebe novas, o que pausa o grupo inteiro por segundos; o assignor cooperative sticky, disponível desde a 2.4, só move as partições que precisam mudar. O Kafka 4.0 introduziu um novo protocolo de grupos (KIP-848) que leva a lógica de atribuição para o coordinator e torna os rebalanceamentos incrementais por design.
Um membro é considerado morto quando para de enviar heartbeats por session.timeout.ms ou quando não chama poll() dentro de max.poll.interval.ms. O segundo caso é o que pega: um consumidor que leva seis minutos para processar um lote é expulso do grupo, suas partições são reatribuídas, o trabalho sem commit é refeito em outro lugar e, quando ele finalmente faz commit, falha com um erro de rebalanceamento. Dimensionar lotes e timeouts em conjunto é uma tarefa operacional recorrente.
Metadados do cluster: o controller KRaft
Alguém precisa saber quais brokers existem, onde fica cada partição, quem lidera cada uma e quais réplicas estão sincronizadas, e alguém precisa tomar decisões quando um broker desaparece. Esse alguém é o controller.
Até o Kafka 2.8, esse estado ficava no ZooKeeper, um serviço de coordenação separado; um broker era eleito controller por meio de um lock no ZooKeeper e propagava as mudanças de metadados para os demais. Funcionava, mas exigia operar e proteger dois sistemas distribuídos, limitava o número de partições que um cluster conseguia suportar porque o failover do controller precisava recarregar tudo do ZooKeeper, e criava divergências sutis entre o que o ZooKeeper guardava e o que os brokers acreditavam.
O KRaft (Kafka Raft) o substitui pela própria maquinaria do Kafka. Um pequeno quórum de nós controller — três ou cinco — executa o protocolo de consenso Raft sobre um log interno de metadados, __cluster_metadata. O líder eleito desse quórum é o controller ativo; os outros são hot standbys que já têm todos os metadados em memória, então o failover é quase instantâneo. Os brokers fazem fetch do log de metadados como qualquer consumidor e aplicam as mudanças em ordem. Os controllers podem rodar em nós dedicados (process.roles=controller) ou, em clusters pequenos e em desenvolvimento, nos mesmos processos dos brokers (process.roles=broker,controller).
| Modo ZooKeeper (legado) | Modo KRaft | |
|---|---|---|
| Armazenamento de metadados | Ensemble do ZooKeeper | Log Raft interno no quórum de controllers |
| Failover do controller | De segundos a minutos, recarga a partir do ZooKeeper | Milissegundos, o standby tem o estado em memória |
| Partições por cluster | Dezenas de milhares | Milhões |
| Sistemas para operar | Dois | Um |
| Status | Descontinuado na 3.5, removido na 4.0 | Pronto para produção desde a 3.3, único modo na 4.0 |
Para quem desenvolve aplicações, o KRaft não muda nada na API cliente. Para quem opera, ele remove um sistema inteiro da implantação: bootstrap.servers mais um quórum de controllers é toda a história.
Além do log: Connect e Streams
Dois componentes vêm com o Kafka e reaproveitam tudo o que foi visto acima. O Kafka Connect é um framework de conectores source e sink que copiam dados entre o Kafka e sistemas externos — bancos de dados via change data capture, object storage, motores de busca — cuidando de offsets, retries e escalabilidade para você. O Kafka Streams é uma biblioteca Java para processar registros de topic para topic com joins, janelas e state stores apoiados em topics de changelog compactados, rodando dentro da sua própria aplicação sem nenhum cluster extra. Os dois explicam por que o Kafka é chamado de plataforma e não de broker, e ambos dependem da semântica de log descrita aqui: partições reprocessáveis, ordem por chave e compactação.
Projetando um topic
Colocar o modelo em prática significa responder a algumas perguntas antes de rodar kafka-topics.sh --create.
- Chave. Escolha a chave pela ordem de que você precisa: um id de pedido mantém os eventos de um pedido em sequência; um id de cliente mantém em sequência tudo sobre um cliente, mas concentra os clientes mais ativos em uma única partição. Uma chave nula maximiza a distribuição e abre mão da ordem.
- Partições. Parta do throughput desejado dividido pelo que um consumidor consegue processar, arredonde para cima até um múltiplo do número de brokers e deixe folga — adicionar partições depois remapeia as chaves. Números muito altos custam memória e file handles, e alongam as eleições de líder.
- Fator de replicação e
min.insync.replicas. 3 e 2 para qualquer coisa que importe; 1 só para dados descartáveis de desenvolvimento. - Retenção. Longa o bastante para reconstruir do zero um consumidor downstream ou para cobrir a maior indisponibilidade que você quer suportar; compactação se o topic representa estado, e não histórico.
- Schema. O Kafka não se importa com o que são os bytes, então um schema registry com Avro, Protobuf ou JSON Schema, junto com uma regra de versionamento para mudanças compatíveis, é a diferença entre um contrato durável e uma pilha de JSON sem documentação.
Erros comuns
- Esperar semântica de fila. Não há confirmação por registro, nem dead-letter queue, nem reentrega; um registro com falha precisa ser tratado pelo consumidor (pular, tentar de novo ou gravar em um topic de erros). Times que querem esses recursos em cima do Kafka os constroem por conta própria ou usam uma fila.
- Uma única partição "para manter a ordem". Funciona até o dia em que um consumidor não dá conta, e aí a única saída é reparticionar, o que muda a ordem de qualquer jeito. Ordene por chave desde o início.
- Adicionar partições a um topic com chaves em produção. Depois disso, as chaves caem em partições diferentes; a ordem por chave se quebra na mudança e qualquer estado compactado que dependia da localidade das chaves fica inconsistente.
- Fator de replicação 3 com
min.insync.replicas1. A escrita em um único broker é confirmada como durável. Configure como 2. - Ignorar o lag. Consumer lag crescente é o primeiro sinal de um consumidor travado, de um grupo subdimensionado ou de um poison record. Crie alertas para ele.
- Tratar o topic de metadados ou os controllers como opcionais. No modo KRaft, perder a maioria dos controllers interrompe as eleições de líder e as mudanças de metadados no cluster inteiro. Rode três ou cinco controllers em domínios de falha separados.
Principais pontos
- O Kafka é um log particionado, replicado e retido; os consumidores controlam seus offsets, e nada é apagado por ter sido lido.
- Ordem e paralelismo são ambos por partição, e a chave do registro decide a partição, então a escolha da chave é a decisão central de design.
- Segmentos e retenção (delete ou compact) governam o armazenamento; a compactação mantém o valor mais recente por chave e transforma um topic em um snapshot reconstruível.
- Replicação com líder e conjunto de réplicas sincronizadas,
acks=allemin.insync.replicas=2sobre fator de replicação 3 é a configuração padrão de durabilidade. - Consumer groups dividem as partições e fazem commit de offsets em
__consumer_offsets; rebalanceamentos e timeouts de poll são onde as aplicações consumidoras mais costumam dar errado. - Os controllers KRaft mantêm os metadados do cluster em um log Raft interno; o ZooKeeper foi descontinuado na 3.x e não existe mais na 4.0.
Fontes e leituras complementares
Perguntas frequentes
- Um topic do Kafka é ordenado?
- Uma partição é ordenada; um topic com mais de uma partição, não. Registros com a mesma chave vão para a mesma partição (com o particionador padrão e um número fixo de partições), então a ordem vale por chave. Se você precisa de ordem global, precisa de uma única partição, o que limita o throughput ao que um único broker e um único consumidor conseguem entregar.
- Quantas partições um topic deve ter?
- O suficiente para distribuir a carga entre os brokers e dar ao maior consumer group o paralelismo de que ele precisa, já que uma partição pode ser lida por no máximo um consumidor dentro de um grupo. Comece com um múltiplo pequeno do número de brokers — seis ou doze para um cluster de três brokers — e lembre-se de que dá para adicionar partições, mas não removê-las, e que adicioná-las muda a partição para a qual cada chave é mapeada.
- O Kafka apaga um registro depois que ele é consumido?
- Não. Os registros são removidos pela política de retenção — depois de uma idade configurada ou quando a partição passa de um tamanho — ou substituídos por um registro mais recente com a mesma chave, no caso de compactação. O consumo apenas avança o offset de um consumer group. É isso que permite que vários grupos leiam os mesmos dados e que qualquer grupo volte atrás.
- Ainda preciso do ZooKeeper?
- Não para clusters novos. O modo KRaft, em que um quórum de controllers do Kafka armazena os metadados do cluster em um log interno, está pronto para produção desde o Kafka 3.3 e é o único modo no Kafka 4.0. Clusters 3.x baseados em ZooKeeper devem ser migrados com o procedimento de migração documentado antes do upgrade para 4.x.