A tabela de pedidos vive no Postgres, e agora outros cinco lugares precisam dela: o índice de busca, um cache, o data warehouse, um serviço antifraude e um modelo de leitura para relatórios. Gravar em todos a partir da aplicação acaba falhando no meio do caminho, cedo ou tarde. Consultar a tabela a cada minuto perde as exclusões e gera carga. O change data capture (CDC) baseado em log segue outro caminho: ele lê o próprio log de transações do banco, o mesmo registro que o banco usa para se recuperar de falhas e para replicar, e transforma cada mudança confirmada em um evento.
Este post explica o CDC baseado em log com Debezium e Kafka, com um diagrama de arquitetura: as abordagens comparadas, como o pipeline funciona, o que um evento de mudança contém, snapshots, ordem e garantias de entrega, mudanças de esquema, a armadilha operacional que derruba bancos primários e quando usar o padrão outbox no lugar.
Quatro formas de capturar mudanças
| Abordagem | Como funciona | Problema principal |
|---|---|---|
| Gravação dupla | A aplicação grava no banco e em um broker | Uma gravação pode falhar enquanto a outra dá certo, e nada as reconcilia |
| Polling | Um job consulta as linhas com updated_at mais recente que a última execução |
Perde exclusões e estados intermediários; gera carga de consultas |
| Triggers | Triggers do banco copiam as mudanças para uma tabela de auditoria | Encarece cada gravação; lógica escondida no banco |
| Baseado em log | Um conector lê o log de transações | Exige configurar o banco e operá-lo com cuidado |
O CDC baseado em log é a única abordagem que enxerga cada mudança confirmada, inclusive exclusões, na ordem de confirmação, sem mexer na aplicação nem deixar as gravações mais lentas. O resto do post é sobre ele.
Como o CDC baseado em log funciona
Os bancos já gravam cada mudança em um log antes de aplicá-la: o write-ahead log no Postgres, o binary log no MySQL, o log de transações no SQL Server, o oplog por trás dos change streams do MongoDB. Um conector de CDC lê esse log do jeito que uma réplica leria.
No Postgres, isso significa decodificação lógica:
-- postgresql.conf: wal_level = logical (requires a restart)
CREATE PUBLICATION orders_pub FOR TABLE orders, order_items;
-- Include all old column values in update and delete events
ALTER TABLE orders REPLICA IDENTITY FULL;
O conector cria um slot de replicação, que lembra até onde ele leu, e decodifica as mudanças com o plugin pgoutput, que já vem no Postgres. Por padrão, eventos de atualização e exclusão trazem só a chave primária antiga; REPLICA IDENTITY FULL adiciona a linha antiga completa, ao custo de um log maior. No MySQL, o equivalente é o binary log baseado em linhas, padrão nas versões atuais, com imagens completas das linhas.
Debezium no Kafka Connect
O Debezium é um motor de CDC open source muito usado. Seus conectores rodam dentro do Kafka Connect, leem o log e publicam um evento por linha alterada em um tópico do Kafka por tabela, usando a chave primária da linha como chave. Uma configuração mínima de conector para Postgres:
{
"name": "orders-cdc",
"config": {
"connector.class": "io.debezium.connector.postgresql.PostgresConnector",
"plugin.name": "pgoutput",
"database.hostname": "orders-db",
"database.port": "5432",
"database.user": "debezium",
"database.dbname": "orders",
"topic.prefix": "shop",
"table.include.list": "public.orders,public.order_items",
"publication.name": "orders_pub",
"slot.name": "orders_cdc",
"heartbeat.interval.ms": "10000"
}
}
As mudanças em public.orders vão para o tópico shop.public.orders. Mantenha a senha fora do arquivo com um provedor de segredos. Se você não roda Kafka, o Debezium Server envia os mesmos eventos para outros brokers e filas na nuvem.
Anatomia de um evento de mudança
Reduzido ao payload, um evento de atualização fica assim:
{
"before": { "id": 1042, "status": "pending", "total": 59.90 },
"after": { "id": 1042, "status": "paid", "total": 59.90 },
"source": { "db": "orders", "table": "orders", "lsn": 24023128, "txId": 5521 },
"op": "u",
"ts_ms": 1793606400123
}
O campo op é c para criação, u para atualização, d para exclusão e r para linhas lidas durante um snapshot. Uma exclusão é seguida por um tombstone, uma mensagem com a mesma chave e valor nulo, para que a compactação de log do Kafka possa remover a chave por completo com o tempo. O bloco source registra exatamente de que ponto do log veio a mudança, e é isso que torna possível a deduplicação mais adiante.
Snapshots: as linhas que já existem
Um conector novo começa com uma tabela que já tem milhões de linhas, e o log só contém as mudanças recentes. Por isso o conector faz um snapshot inicial, emitindo cada linha existente como um evento r, e depois passa a ler o fluxo a partir da posição do log que registrou no início do snapshot, para que nada se perca no intervalo.
Em tabelas grandes, um snapshot longo e bloqueante é doloroso. Snapshots incrementais copiam uma tabela em blocos enquanto o streaming continua, usando a técnica de watermark do artigo DBLog da Netflix, como explica a equipe do Debezium. Você os dispara por uma tabela de sinalização, e eles retomam de onde pararam depois de um reinício, em vez de começar do zero.
Ordem e garantias de entrega
- Por linha, os eventos mantêm a ordem. Todos os eventos de uma chave primária compartilham a mesma chave, então caem na mesma partição do Kafka.
- Entre linhas e tabelas, não existe ordem global. Duas tabelas alteradas em uma mesma transação viram eventos em dois tópicos. Se você precisa das fronteiras de transação, o Debezium pode emitir eventos de metadados de transação que permitem aos consumidores reagrupá-los.
- A entrega é pelo menos uma vez. Depois de uma falha ou reinício, o conector pode reenviar alguns eventos. A documentação do Debezium diz isso com todas as letras.
A consequência para os consumidores: torne-os idempotentes. Faça upsert por chave e ignore um evento cuja posição no log não seja mais recente que a última aplicada para aquela chave.
Mudanças de esquema
Assim que uma tabela passa a ser capturada, o esquema dela vira um contrato com cada consumidor. Adicionar uma coluna anulável é seguro; renomear uma coluna ou mudar um tipo quebra alguém. Use um schema registry com Avro, Protobuf ou JSON Schema e aplique regras de compatibilidade, para que uma mudança incompatível falhe na CI, e não em produção. Melhor ainda, decida quais tabelas são públicas. Tabelas internas podem mudar à vontade se nada fora do serviço as consome.
A armadilha do slot de replicação
Esta é a lição operacional que a maioria dos times aprende do jeito difícil. Um slot de replicação do Postgres retém cada segmento de WAL que o conector não confirmou. Se o conector para, ou fica muito para trás, o WAL se acumula no primário até o disco encher e o banco parar de aceitar gravações. O padrão do Postgres para max_slot_wal_keep_size é -1, o que significa que os slots podem reter uma quantidade ilimitada de WAL.
Uma versão mais silenciosa do mesmo problema: as tabelas capturadas ficam paradas enquanto outras tabelas estão movimentadas. O conector não tem nada para confirmar, então o slot nunca avança. A configuração de heartbeat do Debezium existe exatamente para esse caso.
Defesas:
-- How much WAL each slot is holding back
SELECT slot_name, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS retained
FROM pg_replication_slots;
- Crie alertas sobre o WAL retido por slot, não só sobre o uso de disco.
- Defina
max_slot_wal_keep_sizecom um tamanho que o disco aguente. Um slot que passa desse limite é invalidado e o conector precisa refazer o snapshot, o que é muito melhor do que um disco cheio. - Ative os heartbeats e adicione a tabela de heartbeat à publicação.
- Remova o slot quando desativar um conector.
- Planeje o failover. O PostgreSQL 17 consegue sincronizar slots lógicos com um standby físico; confira se a sua configuração e a sua versão do conector usam isso.
CDC ou padrão outbox?
Capturar tabelas diretamente expõe o seu esquema interno como eventos. A alternativa é o padrão outbox: o serviço grava eventos explícitos em uma tabela outbox na mesma transação dos seus dados, e o CDC publica só essa tabela. O Debezium inclui um outbox event router para isso.
| CDC de tabelas | Outbox com CDC | |
|---|---|---|
| Formato dos eventos | Mudanças de linha no esquema da tabela | Eventos projetados para os consumidores |
| Mudanças na aplicação | Nenhuma | Gravar na outbox em cada transação |
| Acoplamento | Consumidores dependem de tabelas internas | Consumidores dependem de um contrato publicado |
| Melhor para | Replicação, busca, caches, analytics | Eventos de integração entre serviços |
O post sobre o padrão transactional outbox detalha a segunda coluna.
O diagrama do pipeline de CDC
Conteúdo do diagrama
- Orders service
- Postgres: WAL + slot orders_cdc
- Kafka Connect
- Debezium connector
- shop.public.orders, key: id
- Schema registry
- Search index
- Cache
- Lakehouse bronze
- Read-model service
- INSERT / UPDATE / DELETE
- WAL changes
- confirm LSN
- change events
- schemas
- at least once
Como ler:
- O serviço de pedidos grava no Postgres como sempre; ele não sabe nada sobre CDC.
- O Debezium, na moldura do Kafka Connect, lê o write-ahead log pelo seu slot de replicação, publica eventos de mudança no tópico
shop.public.orderse registra os esquemas no registry. - Quatro consumidores leem o tópico de forma independente: o índice de busca, o cache Redis, o data warehouse e um serviço de modelo de leitura.
- A aresta tracejada de volta ao Postgres é a confirmação do slot, a ligação que impede o WAL de se acumular.
- O bloco Markdown mostra um evento de mudança resumido e a consulta de atraso do slot.
Desenhar pipelines de CDC
- Desenhe o log, não só o banco. É no slot e no WAL que o CDC dá certo ou falha.
- Mostre os nomes dos tópicos e as chaves. “Chave: ID do pedido” explica a ordem em poucas palavras.
- Desenhe cada consumidor separadamente. Cada um tem seu próprio atraso, seu modo de falha e sua lógica de idempotência.
- Marque a garantia de entrega no diagrama: pelo menos uma vez.
O lado do Kafka está no diagrama de arquitetura do Kafka. O CDC é o jeito usual de alimentar a camada bruta de um lakehouse, mostrada no diagrama de arquitetura de pipeline de dados, e de construir modelos de leitura para o diagrama de CQRS e event sourcing.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Adicione uma Instância de computação do pack Nuvem para o serviço de pedidos e um nó Postgres do pack Dados.
- Adicione uma Moldura do pack Básico para o Kafka Connect, com uma Instância de computação dentro para o conector Debezium.
- Adicione um nó Kafka do pack Dados para o tópico e uma Caixa do pack Básico para o schema registry.
- Adicione os consumidores: um Banco de dados gerenciado do pack Nuvem para o índice de busca, um nó Redis, um nó Armazém de dados e uma Instância de computação para o serviço de modelo de leitura.
- Desenhe uma aresta tracejada do conector até o Postgres para as confirmações do slot, rotule o tópico com a sua chave e adicione um bloco Markdown do pack Básico com o exemplo de evento.
O exemplo usa 11 nós de diagrama e fica dentro do plano Free.
Perguntas frequentes
O que é change data capture baseado em log? Ler o log de transações de um banco, como o write-ahead log do Postgres ou o binary log do MySQL, e transformar cada mudança confirmada em um evento. Ele captura inserções, atualizações e exclusões em ordem, sem mudar a aplicação.
O Debezium garante entrega exatamente uma vez? Não. Em operação normal, cada mudança é entregue uma vez, mas depois de falhas alguns eventos podem ser entregues de novo; por isso o Debezium documenta entrega pelo menos uma vez. Os consumidores devem ser idempotentes.
Por que o disco do meu Postgres encheu depois de configurar o CDC? Um slot de replicação retém o WAL até o conector confirmá-lo. Um conector parado, ou tabelas capturadas que ficam paradas enquanto outras mudam, deixam o WAL se acumular. Monitore o atraso dos slots, ative os heartbeats e defina max_slot_wal_keep_size.
Como o CDC lida com os dados que já existem? Com um snapshot. O conector primeiro emite cada linha existente e depois transmite as mudanças a partir da posição do log registrada no início. Snapshots incrementais fazem isso em blocos enquanto o streaming continua.
Devo usar CDC ou o padrão outbox? O CDC de tabelas serve para replicação, busca, caches e analytics. Para eventos entre serviços, o padrão outbox publica um contrato projetado em vez de linhas de tabelas internas.