Aller au contenu

Architecture de change data capture : CDC basé sur les journaux avec Debezium et Kafka

Le CDC basé sur les journaux de bout en bout : journaux de base de données, Debezium sur Kafka Connect, snapshots, ordre, évolutions de schéma et le piège du slot.

· Fondateur et développeur

Publié le

Le service des commandes écrit dans Postgres. Un connecteur Debezium, dans un cadre Kafka Connect, lit le WAL via un slot de réplication et publie des événements dans un topic Kafka ; les schémas sont dans un registre. Index de recherche, cache, entrepôt et modèle de lecture le consomment.

La table des commandes vit dans Postgres, et maintenant cinq autres endroits en ont besoin : l'index de recherche, un cache, l'entrepôt de données, un service antifraude et un modèle de lecture pour le reporting. Écrire dans tous depuis l'application finit tôt ou tard par échouer à mi-chemin. Interroger la table toutes les minutes rate les suppressions et ajoute de la charge. Le change data capture (CDC) basé sur les journaux prend une autre route : il lit le journal de transactions de la base elle-même, le même enregistrement que la base utilise pour la reprise après crash et la réplication, et transforme chaque changement validé en événement.

Cet article explique le CDC basé sur les journaux avec Debezium et Kafka, diagramme d'architecture à l'appui : les approches comparées, le fonctionnement du pipeline, le contenu d'un événement de changement, les snapshots, l'ordre et les garanties de livraison, les évolutions de schéma, le piège d'exploitation qui fait tomber les bases primaires, et quand utiliser plutôt le pattern outbox.

Quatre façons de capturer les changements

Approche Fonctionnement Problème principal
Doubles écritures L'application écrit dans la base et dans un broker Une écriture peut échouer pendant que l'autre réussit, et rien ne les réconcilie
Polling Une tâche interroge les lignes dont updated_at est plus récent que la dernière exécution Rate les suppressions et les états intermédiaires ; ajoute de la charge de requêtes
Triggers Des triggers de base copient les changements dans une table d'audit Alourdit chaque écriture ; logique cachée dans la base
Par journaux Un connecteur lit le journal de transactions Demande de configurer la base et de l'exploiter avec soin

Le CDC par journaux est la seule approche qui voit chaque changement validé, suppressions comprises, dans l'ordre de validation, sans toucher à l'application ni ralentir les écritures. La suite de l'article lui est consacrée.

Comment fonctionne le CDC par journaux

Les bases écrivent déjà chaque changement dans un journal avant de l'appliquer : le write-ahead log dans Postgres, le binary log dans MySQL, le journal de transactions dans SQL Server, l'oplog derrière les change streams de MongoDB. Un connecteur CDC lit ce journal comme le ferait un réplica.

Pour Postgres, cela passe par le décodage logique :

-- 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;

Le connecteur crée un slot de réplication, qui mémorise jusqu'où il a lu, et décode les changements avec le plugin pgoutput intégré à Postgres. Par défaut, les événements de mise à jour et de suppression ne portent que l'ancienne clé primaire ; REPLICA IDENTITY FULL ajoute toute l'ancienne ligne, au prix d'un journal plus volumineux. Pour MySQL, l'équivalent est la journalisation binaire par ligne, par défaut dans les versions actuelles, avec des images complètes des lignes.

Debezium sur Kafka Connect

Debezium est un moteur CDC open source très répandu. Ses connecteurs tournent dans Kafka Connect, lisent le journal et publient un événement par ligne modifiée dans un topic Kafka par table, avec la clé primaire de la ligne comme clé. Une configuration minimale de connecteur 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"
  }
}

Les changements de public.orders arrivent dans le topic shop.public.orders. Gardez le mot de passe hors du fichier grâce à un fournisseur de secrets. Si vous n'exploitez pas Kafka, Debezium Server envoie les mêmes événements vers d'autres brokers et des files cloud.

Anatomie d'un événement de changement

Réduit à son payload, un événement de mise à jour ressemble à ceci :

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

Le champ op vaut c pour une création, u pour une mise à jour, d pour une suppression et r pour les lignes lues pendant un snapshot. Une suppression est suivie d'un tombstone, un message de même clé avec une valeur nulle, pour que la compaction des journaux Kafka puisse finir par supprimer complètement la clé. Le bloc source enregistre précisément d'où vient le changement dans le journal, ce qui rend possible la déduplication en aval.

Les snapshots : les lignes qui existent déjà

Un nouveau connecteur démarre face à une table qui contient déjà des millions de lignes, alors que le journal ne contient que les changements récents. Le connecteur prend donc un snapshot initial, émet chaque ligne existante comme événement r, puis lit le flux à partir de la position du journal notée au début du snapshot, pour que rien ne tombe dans l'intervalle.

Sur les grandes tables, un long snapshot bloquant est pénible. Les snapshots incrémentaux copient une table par morceaux pendant que le streaming continue, grâce à la technique de watermark de l'article DBLog de Netflix, comme l'explique l'équipe Debezium. Vous les déclenchez via une table de signalisation, et ils reprennent après un redémarrage au lieu de recommencer de zéro.

Ordre et garanties de livraison

  • Pour une ligne donnée, les événements restent dans l'ordre. Tous les événements d'une clé primaire partagent une clé et arrivent donc dans la même partition Kafka.
  • Entre lignes et entre tables, il n'y a pas d'ordre global. Deux tables modifiées dans une même transaction deviennent des événements dans deux topics. Si vous avez besoin des frontières de transaction, Debezium peut émettre des événements de métadonnées de transaction qui permettent aux consommateurs de les regrouper.
  • La livraison est au moins une fois. Après un crash ou un redémarrage, le connecteur peut renvoyer certains événements. La documentation de Debezium le dit clairement.

La conséquence pour les consommateurs : rendez-les idempotents. Faites des upserts par clé, et ignorez un événement dont la position dans le journal n'est pas plus récente que la dernière appliquée pour cette clé.

Les évolutions de schéma

Dès qu'une table est capturée, son schéma devient un contrat avec chaque consommateur. Ajouter une colonne nullable est sans risque ; renommer une colonne ou changer un type casse quelqu'un. Utilisez un registre de schémas avec Avro, Protobuf ou JSON Schema et imposez des règles de compatibilité, pour qu'un changement cassant échoue en CI plutôt qu'en production. Mieux encore, décidez quelles tables sont publiques. Les tables internes peuvent évoluer librement si rien hors du service ne les consomme.

Le piège du slot de réplication

C'est la leçon d'exploitation que la plupart des équipes apprennent à leurs dépens. Un slot de réplication Postgres conserve chaque segment de WAL que le connecteur n'a pas confirmé. Si le connecteur s'arrête, ou prend beaucoup de retard, le WAL s'accumule sur le primaire jusqu'à ce que le disque soit plein et que la base n'accepte plus d'écritures. La valeur par défaut de Postgres pour max_slot_wal_keep_size est -1, ce qui signifie que les slots peuvent retenir une quantité illimitée de WAL.

Une version plus discrète du même problème : les tables capturées sont inactives pendant que d'autres tables sont très sollicitées. Le connecteur n'a rien à confirmer, donc le slot n'avance jamais. Le réglage heartbeat de Debezium existe précisément pour ce cas.

Les parades :

-- 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;
  • Alertez sur le WAL retenu par slot, pas seulement sur l'occupation du disque.
  • Réglez max_slot_wal_keep_size sur une taille que le disque peut absorber. Un slot qui la dépasse est invalidé et le connecteur doit refaire un snapshot, ce qui vaut bien mieux qu'un disque plein.
  • Activez les heartbeats, et ajoutez la table de heartbeat à la publication.
  • Supprimez le slot quand vous retirez un connecteur.
  • Préparez le basculement. PostgreSQL 17 peut synchroniser les slots logiques vers un standby physique ; vérifiez que votre configuration et votre version de connecteur l'utilisent.

CDC ou pattern outbox ?

Capturer directement les tables expose votre schéma interne sous forme d'événements. L'alternative est le pattern outbox : le service écrit des événements explicites dans une table outbox, dans la même transaction que ses données, et le CDC ne publie que cette table. Debezium inclut un outbox event router à cet effet.

CDC de tables Outbox avec CDC
Forme des événements Changements de lignes dans le schéma de la table Événements conçus pour les consommateurs
Changements dans l'application Aucun Écrire dans l'outbox à chaque transaction
Couplage Les consommateurs dépendent des tables internes Les consommateurs dépendent d'un contrat publié
Idéal pour Réplication, recherche, caches, analytique Événements d'intégration entre services

Le pattern transactional outbox détaille la seconde colonne.

Le diagramme du pipeline CDC

Le service des commandes écrit dans Postgres. Un connecteur Debezium, dans un cadre Kafka Connect, lit le WAL via un slot de réplication et publie des événements dans un topic Kafka ; les schémas sont dans un registre. Index de recherche, cache, entrepôt et modèle de lecture le consomment.

11 composants · 9 connexions

Ouvrir dans Archboard
Contenu du diagramme
  • 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 basé sur les journaux. La base des commandes écrit chaque changement dans son journal ; Debezium le lit via un slot de réplication et publie un événement par ligne modifiée dans Kafka, que consomment un index de recherche, un cache, le lakehouse et un modèle de lecture.

Comment le lire :

  • Le service des commandes écrit dans Postgres comme d'habitude ; il ne sait rien du CDC.
  • Debezium, dans le cadre Kafka Connect, lit le write-ahead log via son slot de réplication, publie les événements dans le topic shop.public.orders et enregistre les schémas dans le registre.
  • Quatre consommateurs lisent le topic indépendamment : l'index de recherche, le cache Redis, l'entrepôt de données et un service de modèle de lecture.
  • L'arête en pointillé qui revient vers Postgres est la confirmation du slot, le lien qui empêche le WAL de s'accumuler.
  • Le bloc Markdown montre un événement de changement abrégé et la requête de retard du slot.

Dessiner des pipelines CDC

  • Dessinez le journal, pas seulement la base. C'est au slot et au WAL que le CDC réussit ou échoue.
  • Montrez les noms des topics et les clés. « Clé : l'ID de commande » explique l'ordre en quatre mots.
  • Dessinez chaque consommateur séparément. Chacun a son propre retard, son propre mode de panne et sa propre logique d'idempotence.
  • Indiquez la garantie de livraison sur le diagramme : au moins une fois.

Le côté Kafka est couvert par le diagramme d'architecture Kafka. Le CDC est la façon habituelle d'alimenter la couche brute d'un lakehouse, présentée dans le diagramme d'architecture de pipeline de données, et de construire les modèles de lecture du diagramme CQRS et event sourcing.

Le construire dans Archboard

  1. Connectez-vous avec Google ou GitHub et créez un diagramme.
  2. Ajoutez une Instance de calcul du pack Cloud pour le service des commandes et un nœud Postgres du pack Données.
  3. Ajoutez un Cadre du pack Base pour Kafka Connect, avec une Instance de calcul à l'intérieur pour le connecteur Debezium.
  4. Ajoutez un nœud Kafka du pack Données pour le topic et une Boîte du pack Base pour le registre de schémas.
  5. Ajoutez les consommateurs : une Base de données gérée du pack Cloud pour l'index de recherche, un nœud Redis, un nœud Entrepôt de données et une Instance de calcul pour le service de modèle de lecture.
  6. Dessinez une arête en pointillé du connecteur vers Postgres pour les confirmations du slot, étiquetez le topic avec sa clé, et ajoutez un bloc Markdown du pack Base avec l'exemple d'événement.

L'exemple utilise 11 nœuds de diagramme et reste dans les limites de l'offre Free.

FAQ

Qu'est-ce que le change data capture basé sur les journaux ? La lecture du journal de transactions d'une base, comme le write-ahead log de Postgres ou le binary log de MySQL, pour transformer chaque changement validé en événement. Il capture insertions, mises à jour et suppressions dans l'ordre, sans modifier l'application.

Debezium garantit-il une livraison exactement une fois ? Non. En fonctionnement normal, chaque changement est livré une fois, mais après une panne certains événements peuvent être relivrés ; Debezium documente donc une livraison au moins une fois. Les consommateurs doivent être idempotents.

Pourquoi mon disque Postgres s'est-il rempli après la mise en place du CDC ? Un slot de réplication retient le WAL jusqu'à ce que le connecteur le confirme. Un connecteur arrêté, ou des tables capturées inactives pendant que d'autres changent, laissent le WAL s'accumuler. Surveillez le retard des slots, activez les heartbeats et réglez max_slot_wal_keep_size.

Comment le CDC gère-t-il les données existantes ? Avec un snapshot. Le connecteur émet d'abord chaque ligne existante, puis diffuse les changements à partir de la position du journal notée au départ. Les snapshots incrémentaux font cela par morceaux pendant que le streaming continue.

Faut-il utiliser le CDC ou le pattern outbox ? Le CDC de tables convient à la réplication, à la recherche, aux caches et à l'analytique. Pour les événements entre services, le pattern outbox publie un contrat conçu plutôt que des lignes de tables internes.

Commencez gratuitement — un siège, sans carte bancaire.

Articles associés

← Retour au blog

Ouvrez un canevas en une minute

Connectez-vous avec Google ou GitHub. Gratuit pour un siège. Passez à un forfait supérieur quand l’équipe vous rejoint.