Saltar al contenido

Arquitectura de captura de datos de cambios: CDC basado en logs con Debezium y Kafka

Cómo funciona de punta a punta el CDC basado en logs: logs de la base de datos, Debezium sobre Kafka Connect, snapshots, orden, cambios de esquema y la trampa del slot.

· Fundador y desarrollador

Publicado el

El servicio de pedidos escribe en Postgres. Un conector Debezium, en un marco de Kafka Connect, lee el WAL por un slot de replicación y publica eventos en un topic de Kafka; los esquemas están en un registro. Lo consumen un índice de búsqueda, una caché, un almacén y un modelo de lectura.

La tabla de pedidos vive en Postgres, y ahora otros cinco sitios la necesitan: el índice de búsqueda, una caché, el almacén de datos, un servicio antifraude y un modelo de lectura para informes. Escribir en todos desde la aplicación acaba fallando a medias tarde o temprano. Consultar la tabla cada minuto pierde los borrados y añade carga. La captura de datos de cambios (CDC) basada en logs toma otro camino: lee el propio log de transacciones de la base de datos, el mismo registro que la base usa para recuperarse de caídas y para replicar, y convierte cada cambio confirmado en un evento.

Este artículo explica el CDC basado en logs con Debezium y Kafka, con un diagrama de arquitectura: las alternativas comparadas, cómo funciona el pipeline, qué contiene un evento de cambio, los snapshots, el orden y las garantías de entrega, los cambios de esquema, la trampa operativa que tumba las bases primarias y cuándo usar en su lugar el patrón outbox.

Cuatro formas de capturar cambios

Enfoque Cómo funciona Problema principal
Escrituras dobles La aplicación escribe en la base de datos y en un broker Una escritura puede fallar mientras la otra tiene éxito, y nada las reconcilia
Polling Un job consulta las filas con updated_at posterior a la última ejecución Pierde borrados y estados intermedios; añade carga de consultas
Triggers Los triggers de la base copian los cambios a una tabla de auditoría Encarece cada escritura; lógica escondida en la base de datos
Basado en logs Un conector lee el log de transacciones Requiere configurar la base de datos y operarla con cuidado

El CDC basado en logs es el único enfoque que ve cada cambio confirmado, incluidos los borrados, en orden de confirmación, sin tocar la aplicación ni ralentizar las escrituras. El resto del artículo trata de él.

Cómo funciona el CDC basado en logs

Las bases de datos ya escriben cada cambio en un log antes de aplicarlo: el write-ahead log en Postgres, el binary log en MySQL, el log de transacciones en SQL Server, el oplog detrás de los change streams de MongoDB. Un conector CDC lee ese log como lo haría una réplica.

En Postgres, eso significa decodificación 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;

El conector crea un slot de replicación, que recuerda hasta dónde ha leído, y decodifica los cambios con el plugin pgoutput, incluido en Postgres. Por defecto, los eventos de actualización y borrado solo llevan la clave primaria antigua; REPLICA IDENTITY FULL añade la fila antigua completa a costa de un log más grande. En MySQL, el equivalente es el binary log por filas, el predeterminado en las versiones actuales, con imágenes completas de las filas.

Debezium sobre Kafka Connect

Debezium es un motor CDC de código abierto muy extendido. Sus conectores se ejecutan dentro de Kafka Connect, leen el log y publican un evento por cada fila modificada en un topic de Kafka por tabla, con la clave primaria de la fila como clave. Una configuración 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"
  }
}

Los cambios de public.orders llegan al topic shop.public.orders. Mantén la contraseña fuera del archivo con un proveedor de secretos. Si no usas Kafka, Debezium Server envía los mismos eventos a otros brokers y a colas en la nube.

Anatomía de un evento de cambio

Recortado a su payload, un evento de actualización tiene este aspecto:

{
  "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
}

El campo op vale c para creaciones, u para actualizaciones, d para borrados y r para filas leídas durante un snapshot. A un borrado le sigue un tombstone, un mensaje con la misma clave y valor nulo, para que la compactación de logs de Kafka pueda eliminar la clave por completo con el tiempo. El bloque source registra exactamente de qué punto del log vino el cambio, y eso es lo que hace posible la deduplicación aguas abajo.

Snapshots: las filas que ya existen

Un conector nuevo empieza con una tabla que ya tiene millones de filas, y el log solo contiene los cambios recientes. Por eso el conector toma un snapshot inicial, emite cada fila existente como evento r y luego transmite desde la posición del log que registró al empezar el snapshot, para que nada se pierda en el hueco.

En tablas grandes, un snapshot largo y bloqueante es doloroso. Los snapshots incrementales copian una tabla por bloques mientras el streaming continúa, usando la técnica de marcas de agua del artículo DBLog de Netflix, como explica el equipo de Debezium. Se lanzan mediante una tabla de señales y, tras un reinicio, continúan donde se quedaron en lugar de empezar de nuevo.

Orden y garantías de entrega

  • Por fila, los eventos mantienen el orden. Todos los eventos de una clave primaria comparten clave, así que caen en la misma partición de Kafka.
  • Entre filas y tablas no hay un orden global. Dos tablas modificadas en una misma transacción se convierten en eventos en dos topics. Si necesitas los límites de transacción, Debezium puede emitir eventos de metadatos de transacción que permiten a los consumidores reagruparlos.
  • La entrega es al menos una vez. Tras una caída o un reinicio, el conector puede reenviar algunos eventos. La documentación de Debezium lo dice sin rodeos.

La consecuencia para los consumidores: hazlos idempotentes. Haz upsert por clave e ignora los eventos cuya posición en el log no sea más reciente que la última aplicada para esa clave.

Cambios de esquema

En cuanto una tabla se captura, su esquema se convierte en un contrato con cada consumidor. Añadir una columna que admite nulos es seguro; renombrar una columna o cambiar un tipo rompe a alguien. Usa un registro de esquemas con Avro, Protobuf o JSON Schema y aplica reglas de compatibilidad, para que un cambio incompatible falle en CI y no en producción. Mejor aún, decide qué tablas son públicas. Las tablas internas pueden cambiar libremente si nada fuera del servicio las consume.

La trampa del slot de replicación

Esta es la lección operativa que la mayoría de los equipos aprende por las malas. Un slot de replicación de Postgres retiene cada segmento de WAL que el conector no ha confirmado. Si el conector se detiene, o se queda muy atrás, el WAL se acumula en la primaria hasta que el disco se llena y la base de datos deja de aceptar escrituras. El valor predeterminado de Postgres para max_slot_wal_keep_size es -1, lo que significa que los slots pueden retener una cantidad ilimitada de WAL.

Una versión más silenciosa del mismo problema: las tablas capturadas están inactivas mientras otras tablas tienen mucho movimiento. El conector no tiene nada que confirmar, así que el slot nunca avanza. El ajuste de heartbeat de Debezium existe justo para este caso.

Defensas:

-- 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;
  • Alerta sobre el WAL retenido por slot, no solo sobre el uso del disco.
  • Fija max_slot_wal_keep_size en un tamaño que el disco pueda soportar. Un slot que lo supera se invalida y el conector tiene que volver a hacer un snapshot, lo cual es mucho mejor que un disco lleno.
  • Activa los heartbeats y añade la tabla de heartbeat a la publicación.
  • Elimina el slot cuando retires un conector.
  • Planifica el failover. PostgreSQL 17 puede sincronizar slots lógicos con un standby físico; comprueba que tu configuración y tu versión del conector lo usan.

¿CDC o patrón outbox?

Capturar las tablas directamente expone tu esquema interno como eventos. La alternativa es el patrón outbox: el servicio escribe eventos explícitos en una tabla outbox en la misma transacción que sus datos, y el CDC publica solo esa tabla. Debezium incluye un outbox event router para esto.

CDC de tablas Outbox con CDC
Forma de los eventos Cambios de fila en el esquema de la tabla Eventos diseñados para los consumidores
Cambios en la aplicación Ninguno Escribir en la outbox en cada transacción
Acoplamiento Los consumidores dependen de tablas internas Los consumidores dependen de un contrato publicado
Ideal para Replicación, búsqueda, cachés, analítica Eventos de integración entre servicios

El patrón transactional outbox trata la segunda columna en detalle.

El diagrama del pipeline CDC

El servicio de pedidos escribe en Postgres. Un conector Debezium, en un marco de Kafka Connect, lee el WAL por un slot de replicación y publica eventos en un topic de Kafka; los esquemas están en un registro. Lo consumen un índice de búsqueda, una caché, un almacén y un modelo de lectura.

11 componentes · 9 conexiones

Abrir en Archboard
Contenido del 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
CDC basado en logs. La base de datos de pedidos escribe cada cambio en su log; Debezium lo lee a través de un slot de replicación y publica en Kafka un evento por fila modificada, que consumen un índice de búsqueda, una caché, el lakehouse y un modelo de lectura.

Cómo leerlo:

  • El servicio de pedidos escribe en Postgres como siempre; no sabe nada del CDC.
  • Debezium, en el marco de Kafka Connect, lee el write-ahead log a través de su slot de replicación, publica eventos de cambio en el topic shop.public.orders y registra los esquemas en el registro.
  • Cuatro consumidores leen el topic de forma independiente: el índice de búsqueda, la caché de Redis, el almacén de datos y un servicio de modelo de lectura.
  • La arista discontinua que vuelve a Postgres es la confirmación del slot, el enlace que evita que el WAL se acumule.
  • El bloque Markdown muestra un evento de cambio recortado y la consulta del retraso del slot.

Dibujar pipelines CDC

  • Dibuja el log, no solo la base de datos. El slot y el WAL son donde el CDC funciona o falla.
  • Muestra los nombres de los topics y las claves. «Con el ID del pedido como clave» explica el orden en pocas palabras.
  • Dibuja cada consumidor por separado. Cada uno tiene su propio retraso, su modo de fallo y su lógica de idempotencia.
  • Indica la garantía de entrega en el diagrama: al menos una vez.

El lado de Kafka se cubre en el diagrama de arquitectura de Kafka. El CDC es la forma habitual de alimentar la capa en bruto de un lakehouse, como muestra el diagrama de arquitectura de pipeline de datos, y de construir modelos de lectura para el diagrama de CQRS y event sourcing.

Constrúyelo en Archboard

  1. Inicia sesión con Google o GitHub y crea un diagrama.
  2. Añade una Instancia de cómputo del pack Nube para el servicio de pedidos y un nodo Postgres del pack Datos.
  3. Añade un Marco del pack Básico para Kafka Connect, con una Instancia de cómputo dentro para el conector Debezium.
  4. Añade un nodo Kafka del pack Datos para el topic y una Caja del pack Básico para el registro de esquemas.
  5. Añade los consumidores: una Base de datos administrada del pack Nube para el índice de búsqueda, un nodo Redis, un nodo Almacén de datos y una Instancia de cómputo para el servicio de modelo de lectura.
  6. Dibuja una arista discontinua del conector a Postgres para las confirmaciones del slot, etiqueta el topic con su clave y añade un bloque Markdown del pack Básico con el ejemplo de evento.

El ejemplo usa 11 nodos de diagrama, dentro del plan Free.

Preguntas frecuentes

¿Qué es la captura de datos de cambios basada en logs? Leer el log de transacciones de una base de datos, como el write-ahead log de Postgres o el binary log de MySQL, y convertir cada cambio confirmado en un evento. Captura inserciones, actualizaciones y borrados en orden sin cambiar la aplicación.

¿Debezium garantiza la entrega exactamente una vez? No. En funcionamiento normal cada cambio se entrega una vez, pero tras un fallo algunos eventos pueden volver a entregarse, por eso Debezium documenta una entrega al menos una vez. Los consumidores deben ser idempotentes.

¿Por qué se llenó el disco de Postgres después de configurar el CDC? Un slot de replicación retiene el WAL hasta que el conector lo confirma. Un conector detenido, o tablas capturadas que siguen inactivas mientras otras cambian, dejan que el WAL se acumule. Vigila el retraso de los slots, activa los heartbeats y fija max_slot_wal_keep_size.

¿Cómo maneja el CDC los datos que ya existen? Con un snapshot. El conector emite primero cada fila existente y luego transmite los cambios desde la posición del log registrada al inicio. Los snapshots incrementales lo hacen por bloques mientras el streaming continúa.

¿Debería usar CDC o el patrón outbox? El CDC de tablas encaja en replicación, búsqueda, cachés y analítica. Para eventos entre servicios, el patrón outbox publica un contrato diseñado en lugar de filas de tablas internas.

Empieza gratis — un puesto, sin tarjeta de crédito.

Artículos relacionados

← Volver al blog

Abre un lienzo en un minuto

Inicia sesión con Google o GitHub. Gratis para un asiento. Mejora el plan cuando se una el equipo.