Pular para o conteúdo

Arquitetura de change data capture: CDC baseado em log com Debezium e Kafka

Como o CDC baseado em log funciona de ponta a ponta: logs do banco de dados, Debezium no Kafka Connect, snapshots, ordem, mudanças de esquema e a armadilha do slot.

· Fundador e desenvolvedor

Publicado em

O serviço de pedidos grava no Postgres. Um conector Debezium, em uma moldura do Kafka Connect, lê o WAL por um slot de replicação e publica eventos em um tópico do Kafka; os esquemas ficam em um registry. Índice de busca, cache, armazém de dados e modelo de leitura o consomem.

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_size com 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

O serviço de pedidos grava no Postgres. Um conector Debezium, em uma moldura do Kafka Connect, lê o WAL por um slot de replicação e publica eventos em um tópico do Kafka; os esquemas ficam em um registry. Índice de busca, cache, armazém de dados e modelo de leitura o consomem.

11 componentes · 9 conexões

Abrir no Archboard
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
Change data capture baseado em log. O banco de pedidos grava cada mudança no seu log; o Debezium lê esse log por um slot de replicação e publica um evento por linha alterada no Kafka, onde um índice de busca, um cache, o lakehouse e um modelo de leitura o consomem.

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.orders e 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

  1. Entre com Google ou GitHub e crie um diagrama.
  2. Adicione uma Instância de computação do pack Nuvem para o serviço de pedidos e um nó Postgres do pack Dados.
  3. Adicione uma Moldura do pack Básico para o Kafka Connect, com uma Instância de computação dentro para o conector Debezium.
  4. Adicione um nó Kafka do pack Dados para o tópico e uma Caixa do pack Básico para o schema registry.
  5. 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.
  6. 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.

Comece grátis — um assento, sem cartão de crédito.

Artigos relacionados

← Voltar ao blog

Abra um canvas em um minuto

Entre com Google ou GitHub. Grátis para um assento. Faça upgrade quando a equipe entrar.