Zum Inhalt springen

Change-Data-Capture-Architektur: logbasiertes CDC mit Debezium und Kafka

Wie logbasiertes Change Data Capture funktioniert: Datenbanklogs, Debezium auf Kafka Connect, Snapshots, Reihenfolge, Schemaänderungen und die Slot-Falle.

· Gründer & Entwickler

Veröffentlicht am

Der Bestellservice schreibt in Postgres. Ein Debezium-Connector im Kafka-Connect-Rahmen liest das WAL über einen Replication Slot und veröffentlicht Change Events in ein Kafka-Topic; Schemas liegen in einer Registry. Suchindex, Cache, Data Warehouse und Read-Model-Service konsumieren es.

Die Bestelltabelle liegt in Postgres, und jetzt brauchen sie fünf andere Stellen: der Suchindex, ein Cache, das Data Warehouse, ein Betrugserkennungsdienst und ein Read Model fürs Reporting. Aus der Anwendung in alle zu schreiben, scheitert früher oder später auf halbem Weg. Die Tabelle jede Minute abzufragen, übersieht Löschungen und erzeugt Last. Logbasiertes Change Data Capture (CDC) geht einen anderen Weg: Es liest das eigene Transaktionslog der Datenbank, denselben Datensatz, den die Datenbank für Crash-Recovery und Replikation nutzt, und macht aus jeder bestätigten Änderung ein Event.

Dieser Beitrag erklärt logbasiertes CDC mit Debezium und Kafka anhand eines Architekturdiagramms: die Ansätze im Vergleich, wie die Pipeline funktioniert, was ein Change Event enthält, Snapshots, Reihenfolge und Zustellgarantien, Schemaänderungen, die betriebliche Falle, die Primärsysteme lahmlegt, und wann du stattdessen das Outbox-Pattern nutzen solltest.

Vier Wege, Änderungen zu erfassen

Ansatz Wie er funktioniert Hauptproblem
Dual Writes Die Anwendung schreibt in die Datenbank und in einen Broker Ein Schreibvorgang kann scheitern, während der andere gelingt, und nichts gleicht sie ab
Polling Ein Job fragt Zeilen ab, deren updated_at neuer als der letzte Lauf ist Übersieht Löschungen und Zwischenzustände; erzeugt Abfragelast
Trigger Datenbank-Trigger kopieren Änderungen in eine Audit-Tabelle Verteuert jeden Schreibvorgang; Logik versteckt sich in der Datenbank
Logbasiert Ein Connector liest das Transaktionslog Braucht Datenbankkonfiguration und sorgfältigen Betrieb

Logbasiertes CDC ist der einzige Ansatz, der jede bestätigte Änderung sieht, einschließlich Löschungen, in Commit-Reihenfolge, ohne die Anwendung anzufassen oder Schreibvorgänge zu verlangsamen. Um ihn geht es im Rest dieses Beitrags.

Wie logbasiertes CDC funktioniert

Datenbanken schreiben jede Änderung ohnehin in ein Log, bevor sie sie anwenden: das Write-Ahead-Log in Postgres, das Binary Log in MySQL, das Transaktionslog in SQL Server, das Oplog hinter MongoDB Change Streams. Ein CDC-Connector liest dieses Log so, wie es ein Replikat täte.

Für Postgres bedeutet das logische Dekodierung:

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

Der Connector legt einen Replication Slot an, der sich merkt, wie weit er gelesen hat, und dekodiert Änderungen mit dem in Postgres eingebauten Plugin pgoutput. Standardmäßig enthalten Update- und Delete-Events nur den alten Primärschlüssel; REPLICA IDENTITY FULL fügt die vollständige alte Zeile hinzu, auf Kosten eines größeren Logs. Für MySQL ist das Gegenstück zeilenbasiertes Binary Logging, in aktuellen Versionen der Standard, mit vollständigen Zeilenabbildern.

Debezium auf Kafka Connect

Debezium ist eine weit verbreitete Open-Source-CDC-Engine. Seine Connectors laufen in Kafka Connect, lesen das Log und veröffentlichen pro Zeilenänderung ein Event in ein Kafka-Topic pro Tabelle, mit dem Primärschlüssel der Zeile als Key. Eine minimale Postgres-Connector-Konfiguration:

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

Änderungen an public.orders landen im Topic shop.public.orders. Halte das Passwort mit einem Secrets-Provider aus der Datei heraus. Wenn du kein Kafka betreibst, streamt Debezium Server dieselben Events zu anderen Brokern und Cloud-Queues.

Anatomie eines Change Events

Auf den Payload gekürzt sieht ein Update-Event so aus:

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

Das Feld op ist c für Create, u für Update, d für Delete und r für Zeilen, die während eines Snapshots gelesen wurden. Auf eine Löschung folgt ein Tombstone, eine Nachricht mit demselben Key und einem Null-Wert, damit Kafka Log Compaction den Key schließlich ganz entfernen kann. Der Block source hält genau fest, woher im Log die Änderung kam, und genau das macht Deduplizierung downstream möglich.

Snapshots: die Zeilen, die schon existieren

Ein neuer Connector beginnt mit einer Tabelle, die bereits Millionen Zeilen hat, während das Log nur jüngere Änderungen enthält. Der Connector nimmt deshalb einen initialen Snapshot, gibt jede vorhandene Zeile als r-Event aus und streamt dann ab der Log-Position, die er zu Beginn des Snapshots notiert hat, damit nichts in die Lücke fällt.

Bei großen Tabellen ist ein langer, blockierender Snapshot schmerzhaft. Inkrementelle Snapshots kopieren eine Tabelle in Häppchen, während das Streaming weiterläuft, mit der Watermark-Technik aus Netflix' DBLog-Paper, wie das Debezium-Team erklärt. Du startest sie über eine Signaltabelle, und nach einem Neustart setzen sie fort, statt von vorn zu beginnen.

Reihenfolge und Zustellgarantien

  • Pro Zeile bleiben Events in Reihenfolge. Alle Events eines Primärschlüssels teilen sich einen Key und landen deshalb in derselben Kafka-Partition.
  • Über Zeilen und Tabellen hinweg gibt es keine globale Reihenfolge. Zwei Tabellen, die in einer Transaktion geändert werden, werden zu Events in zwei Topics. Wenn du Transaktionsgrenzen brauchst, kann Debezium Transaktions-Metadaten-Events ausgeben, mit denen Konsumenten die Events neu gruppieren.
  • Die Zustellung ist at least once. Nach einem Absturz oder Neustart kann der Connector einige Events erneut senden. Die Debezium-Dokumentation sagt das ganz offen.

Die Konsequenz für Konsumenten: Mach sie idempotent. Führe Upserts nach Key aus und ignoriere ein Event, dessen Log-Position nicht neuer ist als die zuletzt angewendete für diesen Key.

Schemaänderungen

Sobald eine Tabelle erfasst wird, wird ihr Schema zu einem Vertrag mit jedem Konsumenten. Eine nullable Spalte hinzuzufügen ist sicher; eine Spalte umzubenennen oder einen Typ zu ändern, bricht jemanden. Nutze eine Schema Registry mit Avro, Protobuf oder JSON Schema und erzwinge Kompatibilitätsregeln, damit eine brechende Änderung in der CI scheitert statt in Produktion. Noch besser: Entscheide, welche Tabellen öffentlich sind. Interne Tabellen können sich frei ändern, wenn nichts außerhalb des Service sie konsumiert.

Die Replication-Slot-Falle

Das ist die betriebliche Lektion, die die meisten Teams auf die harte Tour lernen. Ein Postgres-Replication-Slot behält jedes WAL-Segment, das der Connector nicht bestätigt hat. Stoppt der Connector oder fällt er weit zurück, sammelt sich WAL auf dem Primärsystem an, bis die Platte voll ist und die Datenbank keine Schreibvorgänge mehr annimmt. Der Postgres-Standard für max_slot_wal_keep_size ist -1, was bedeutet, dass Slots unbegrenzt viel WAL zurückhalten dürfen.

Eine leisere Variante desselben Problems: Die erfassten Tabellen sind ruhig, während andere Tabellen viel zu tun haben. Der Connector hat nichts zu bestätigen, also rückt der Slot nie vor. Genau für diesen Fall gibt es die Heartbeat-Einstellung von Debezium.

Gegenmaßnahmen:

-- 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;
  • Alarmiere auf das zurückgehaltene WAL pro Slot, nicht nur auf die Plattenbelegung.
  • Setz max_slot_wal_keep_size auf eine Größe, die die Platte verkraftet. Ein Slot, der sie überschreitet, wird invalidiert, und der Connector muss einen neuen Snapshot nehmen, was weit besser ist als eine volle Platte.
  • Aktiviere Heartbeats und nimm die Heartbeat-Tabelle in die Publication auf.
  • Lösch den Slot, wenn du einen Connector stilllegst.
  • Plane das Failover. PostgreSQL 17 kann logische Slots auf einen physischen Standby synchronisieren; prüf, ob dein Setup und deine Connector-Version das nutzen.

CDC oder das Outbox-Pattern?

Tabellen direkt zu erfassen, legt dein internes Schema als Events offen. Die Alternative ist das Outbox-Pattern: Der Service schreibt explizite Events in derselben Transaktion wie seine Daten in eine Outbox-Tabelle, und CDC veröffentlicht nur diese Tabelle. Debezium bringt dafür einen Outbox Event Router mit.

Tabellen-CDC Outbox mit CDC
Event-Form Zeilenänderungen im Schema der Tabelle Für Konsumenten entworfene Events
Änderungen an der Anwendung Keine In jeder Transaktion in die Outbox schreiben
Kopplung Konsumenten hängen von internen Tabellen ab Konsumenten hängen von einem veröffentlichten Vertrag ab
Am besten für Replikation, Suche, Caches, Analytics Integrations-Events zwischen Services

Das Transactional-Outbox-Pattern behandelt die zweite Spalte im Detail.

Das CDC-Pipeline-Diagramm

Der Bestellservice schreibt in Postgres. Ein Debezium-Connector im Kafka-Connect-Rahmen liest das WAL über einen Replication Slot und veröffentlicht Change Events in ein Kafka-Topic; Schemas liegen in einer Registry. Suchindex, Cache, Data Warehouse und Read-Model-Service konsumieren es.

11 Komponenten · 9 Verbindungen

In Archboard öffnen
Diagramminhalt
  • 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
Logbasiertes Change Data Capture. Die Bestelldatenbank schreibt jede Änderung in ihr Log; Debezium liest es über einen Replication Slot und veröffentlicht pro Zeilenänderung ein Event in Kafka, wo ein Suchindex, ein Cache, das Lakehouse und ein Read Model es konsumieren.

So liest du es:

  • Der Bestellservice schreibt wie gewohnt in Postgres; er weiß nichts von CDC.
  • Debezium im Kafka-Connect-Rahmen liest das Write-Ahead-Log über seinen Replication Slot, veröffentlicht Change Events im Topic shop.public.orders und registriert Schemas in der Registry.
  • Vier Konsumenten lesen das Topic unabhängig voneinander: der Suchindex, der Redis-Cache, das Data Warehouse und ein Read-Model-Service.
  • Die gestrichelte Kante zurück zu Postgres ist die Slot-Bestätigung, die Verbindung, die verhindert, dass sich WAL anstaut.
  • Der Markdown-Block zeigt ein gekürztes Change Event und die Abfrage für den Slot-Rückstand.

CDC-Pipelines zeichnen

  • Zeichne das Log, nicht nur die Datenbank. Am Slot und am WAL entscheidet sich, ob CDC funktioniert.
  • Zeig die Topic-Namen und Keys. „Key ist die Bestell-ID“ erklärt die Reihenfolge in wenigen Worten.
  • Zeichne jeden Konsumenten einzeln. Jeder hat seinen eigenen Rückstand, seinen eigenen Fehlermodus und seine eigene Idempotenzlogik.
  • Schreib die Zustellgarantie aufs Diagramm: at least once.

Die Kafka-Seite behandelt das Kafka-Architekturdiagramm. CDC ist der übliche Weg, die Rohschicht eines Lakehouse zu befüllen, wie im Datenpipeline-Architekturdiagramm gezeigt, und Read Models für das CQRS- und Event-Sourcing-Diagramm aufzubauen.

In Archboard nachbauen

  1. Melde dich mit Google oder GitHub an und leg ein Diagramm an.
  2. Füge eine Recheninstanz aus dem Cloud-Pack für den Bestellservice und einen Postgres-Node aus dem Daten-Pack hinzu.
  3. Füge einen Rahmen aus dem Basis-Pack für Kafka Connect hinzu, darin eine Recheninstanz für den Debezium-Connector.
  4. Füge einen Kafka-Node aus dem Daten-Pack für das Topic und einen Kasten aus dem Basis-Pack für die Schema Registry hinzu.
  5. Füge die Konsumenten hinzu: eine Verwaltete Datenbank aus dem Cloud-Pack für den Suchindex, einen Redis-Node, einen Datenlager-Node und eine Recheninstanz für den Read-Model-Service.
  6. Zeichne eine gestrichelte Kante vom Connector zu Postgres für die Slot-Bestätigungen, beschrifte das Topic mit seinem Key und füge einen Markdown-Block aus dem Basis-Pack mit dem Event-Beispiel hinzu.

Das Beispiel nutzt 11 Diagramm-Nodes und bleibt damit im Free-Plan.

FAQ

Was ist logbasiertes Change Data Capture? Das Lesen des Transaktionslogs einer Datenbank, etwa des Postgres-Write-Ahead-Logs oder des MySQL-Binary-Logs, und das Umwandeln jeder bestätigten Änderung in ein Event. Es erfasst Inserts, Updates und Deletes in Reihenfolge, ohne die Anwendung zu ändern.

Garantiert Debezium Exactly-once-Zustellung? Nein. Im Normalbetrieb wird jede Änderung einmal zugestellt, aber nach Ausfällen können einige Events erneut zugestellt werden, daher dokumentiert Debezium At-least-once-Zustellung. Konsumenten sollten idempotent sein.

Warum ist meine Postgres-Platte nach dem Einrichten von CDC vollgelaufen? Ein Replication Slot behält WAL, bis der Connector es bestätigt. Ein gestoppter Connector oder erfasste Tabellen, die ruhig bleiben, während sich andere ändern, lassen WAL anwachsen. Überwache den Slot-Rückstand, aktiviere Heartbeats und setz max_slot_wal_keep_size.

Wie geht CDC mit vorhandenen Daten um? Mit einem Snapshot. Der Connector gibt zuerst jede vorhandene Zeile aus und streamt dann Änderungen ab der Log-Position, die er zu Beginn notiert hat. Inkrementelle Snapshots tun das in Häppchen, während das Streaming weiterläuft.

Sollte ich CDC oder das Outbox-Pattern nutzen? Tabellen-CDC passt zu Replikation, Suche, Caches und Analytics. Für Events zwischen Services veröffentlicht das Outbox-Pattern einen entworfenen Vertrag statt interner Tabellenzeilen.

Kostenlos starten — ein Seat, keine Kreditkarte.

Ähnliche Beiträge

← Zurück zum Blog

In einer Minute ein Canvas öffnen

Melden Sie sich mit Google oder GitHub an. Kostenlos für einen Sitzplatz. Upgraden Sie, wenn das Team dazukommt.