Veröffentlicht am

Outbox Pattern auf Kubernetes mit Debezium und Kafka

Teilen:
Authors

TL;DR

  • Das Transactional Outbox Pattern schreibt Business-Daten und Events in derselben Datenbank-Transaktion -- keine verlorenen oder doppelten Events mehr
  • Change Data Capture (CDC) mit Debezium liest das Datenbank-Transaction-Log und streamt Aenderungen an der Outbox-Tabelle direkt nach Kafka
  • Kafka Connect orchestriert den Debezium Connector auf Kubernetes -- skalierbar, fehlerresistent und ohne eigenen Polling-Code
  • Exactly-Once Semantics erfordert idempotente Consumer und Kafka-Transaktionen -- in der Praxis ist idempotente Verarbeitung der robustere Ansatz
  • Der Outbox-Ansatz eliminiert das Dual-Write-Problem vollstaendig

Kubernetes Outbox Pattern: Zuverlaessige Events fuer Microservices

Event-Driven Microservices haben ein fundamentales Problem: den Dual Write. Ein Service muss Daten in die Datenbank schreiben UND ein Event an einen Message Broker senden. Wenn eine der beiden Operationen fehlschlaegt, entsteht Inkonsistenz.

Das Dual-Write-Problem:

1. Order Service: INSERT INTO orders (...) */} Erfolgreich
2. Order Service: PUBLISH "OrderCreated" an Kafka */} Fehlgeschlagen!

Ergebnis: Bestellung existiert in der Datenbank,
          aber kein Event wurde gesendet.
          Andere Services wissen nichts davon.

Das Transactional Outbox Pattern loest das elegant: Statt direkt an Kafka zu senden, schreibt der Service das Event in eine Outbox-Tabelle innerhalb derselben Datenbank-Transaktion. Ein separater Prozess (Debezium via CDC) liest die Outbox-Tabelle und streamt die Events nach Kafka.

Fuer die grundlegende Service-Kommunikation auf Kubernetes: Kubernetes Dapr: Sidecar-Pattern fuer Microservices.


Das Outbox Pattern im Detail

Order Service
     |
     | BEGIN TRANSACTION
     |   INSERT INTO orders (...)
     |   INSERT INTO outbox (event_type, payload, ...)
     | COMMIT
     |
     +------*/} orders-db (PostgreSQL)
                    |
                    | WAL (Write-Ahead Log)
                    |
                Debezium CDC Connector
                    |
                    v
                Apache Kafka
                    |
          +---------+---------+
          |         |         |
    Payment    Inventory   Notification
    Service    Service     Service

Outbox-Tabelle

CREATE TABLE outbox (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    aggregate_type  VARCHAR(255) NOT NULL,
    aggregate_id    VARCHAR(255) NOT NULL,
    event_type      VARCHAR(255) NOT NULL,
    payload         JSONB NOT NULL,
    created_at      TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_outbox_created_at ON outbox (created_at);

Transaktionale Schreiboperation

BEGIN;

INSERT INTO orders (id, customer_id, total, status)
VALUES ('order-456', 'customer-789', 299.99, 'CREATED');

INSERT INTO outbox (aggregate_type, aggregate_id, event_type, payload)
VALUES (
    'Order',
    'order-456',
    'OrderCreated',
    '{"orderId": "order-456", "customerId": "customer-789", "total": 299.99}'
);

COMMIT;
-- Beide Inserts sind atomar. Entweder beide erfolgreich oder keiner.

Debezium: Change Data Capture fuer Kubernetes

Debezium liest das Transaction-Log der Datenbank (PostgreSQL WAL, MySQL Binlog) und streamt Aenderungen als Events nach Kafka. Fuer das Outbox Pattern ueberwacht Debezium die Outbox-Tabelle und routet Events an die richtigen Kafka-Topics.

Kafka Connect Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kafka-connect
  namespace: shop
spec:
  replicas: 2
  selector:
    matchLabels:
      app: kafka-connect
  template:
    metadata:
      labels:
        app: kafka-connect
    spec:
      containers:
        - name: kafka-connect
          image: debezium/connect:2.6
          ports:
            - containerPort: 8083
              name: rest-api
          env:
            - name: BOOTSTRAP_SERVERS
              value: "kafka-0.kafka.shop.svc.cluster.local:9092"
            - name: GROUP_ID
              value: "shop-connect-cluster"
            - name: CONFIG_STORAGE_TOPIC
              value: "connect-configs"
            - name: OFFSET_STORAGE_TOPIC
              value: "connect-offsets"
            - name: STATUS_STORAGE_TOPIC
              value: "connect-status"
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              cpu: "1"
              memory: 2Gi
          readinessProbe:
            httpGet:
              path: /connectors
              port: 8083
            initialDelaySeconds: 30
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: kafka-connect
  namespace: shop
spec:
  selector:
    app: kafka-connect
  ports:
    - port: 8083
      targetPort: 8083

Debezium Outbox Connector registrieren

Der Connector wird ueber die Kafka Connect REST API registriert. Die Konfiguration als Kubernetes Job:

apiVersion: batch/v1
kind: Job
metadata:
  name: register-outbox-connector
  namespace: shop
spec:
  template:
    spec:
      containers:
        - name: register-connector
          image: curlimages/curl:8.6.0
          command:
            - /bin/sh
            - -c
            - |
              until curl -s http://kafka-connect.shop.svc.cluster.local:8083/connectors; do
                echo "Waiting for Kafka Connect..."
                sleep 5
              done
              curl -X POST http://kafka-connect.shop.svc.cluster.local:8083/connectors \
                -H "Content-Type: application/json" \
                -d '{
                  "name": "order-outbox-connector",
                  "config": {
                    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
                    "database.hostname": "orders-db.shop.svc.cluster.local",
                    "database.port": "5432",
                    "database.dbname": "orders",
                    "table.include.list": "public.outbox",
                    "transforms": "outbox",
                    "transforms.outbox.type": "io.debezium.transforms.outbox.EventRouter",
                    "transforms.outbox.route.by.field": "aggregate_type",
                    "transforms.outbox.route.topic.replacement": "events.${routedByValue}",
                    "plugin.name": "pgoutput",
                    "slot.name": "order_outbox_slot"
                  }
                }'
      restartPolicy: OnFailure
  backoffLimit: 5

Der Debezium EventRouter transformiert Outbox-Eintraege in saubere Kafka-Events. Ein Eintrag mit aggregate_type: "Order" landet im Topic events.Order.


Exactly-Once Semantics: Theorie und Praxis

Debezium garantiert At-Least-Once Delivery. Bei einem Restart kann ein Event doppelt gesendet werden.

Szenario: Debezium Restart

1. Debezium liest Outbox-Eintrag */} sendet an Kafka
2. Debezium-Pod crasht BEVOR Offset committed wird
3. Neuer Pod startet, liest ab letztem Offset
4. Event wird ERNEUT gesendet */} Duplikat!

Idempotente Consumer (empfohlen)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
  namespace: shop
spec:
  replicas: 3
  selector:
    matchLabels:
      app: payment-service
  template:
    metadata:
      labels:
        app: payment-service
    spec:
      containers:
        - name: payment-service
          image: registry.example.com/payment-service:v3.2.0
          env:
            - name: KAFKA_BROKERS
              value: "kafka-0.kafka.shop.svc.cluster.local:9092"
            - name: KAFKA_CONSUMER_GROUP
              value: "payment-service"
            - name: KAFKA_TOPICS
              value: "events.Order"
            - name: IDEMPOTENCY_ENABLED
              value: "true"
            - name: IDEMPOTENCY_STORE
              value: "redis-master.shop.svc.cluster.local:6379"
            - name: IDEMPOTENCY_TTL_HOURS
              value: "72"
          resources:
            requests:
              cpu: 250m
              memory: 256Mi
            limits:
              cpu: 500m
              memory: 512Mi

Der Payment Service speichert die Event-ID bei Verarbeitung in Redis. Kommt dasselbe Event erneut, wird es erkannt und uebersprungen.

In der Praxis ist die Kombination aus At-Least-Once Delivery und idempotenten Consumern zuverlaessiger und einfacher zu debuggen als End-to-End Exactly-Once mit Kafka-Transaktionen.

Fuer event-basierte Skalierung der Consumer: Kubernetes KEDA Autoscaling.


Outbox Cleanup: Alte Events loeschen

Die Outbox-Tabelle waechst kontinuierlich. Ein CronJob raeumt regelmaessig auf:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: outbox-cleanup
  namespace: shop
spec:
  schedule: "0 3 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: cleanup
              image: postgres:16-alpine
              command:
                - /bin/sh
                - -c
                - |
                  PGPASSWORD=$DB_PASSWORD psql -h $DB_HOST -U $DB_USER -d orders -c \
                    "DELETE FROM outbox WHERE created_at < NOW() - INTERVAL '7 days';"
              env:
                - name: DB_HOST
                  value: "orders-db.shop.svc.cluster.local"
                - name: DB_USER
                  valueFrom:
                    secretKeyRef:
                      name: orders-db-credentials
                      key: username
                - name: DB_PASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: orders-db-credentials
                      key: password
          restartPolicy: OnFailure

Monitoring: Debezium und Outbox ueberwachen

MetrikQuelleAlert-Schwelle
Outbox Table SizePostgreSQLUeber 100.000 Zeilen
Debezium Lag (ms)Kafka ConnectUeber 30 Sekunden
Connector StatusKafka Connect REST APIStatus != RUNNING
Consumer LagKafkaTopic-abhaengig
Replication Slot SizePostgreSQLUeber 1 GB

Fuer umfassende Health Checks und Probes: Kubernetes Health Checks und Probes.


Outbox Pattern vs. Alternativen

AnsatzZuverlaessigkeitKomplexitaetLatenz
Dual WriteNiedrig (Inkonsistenz)NiedrigNiedrig
Outbox + PollingHochMittelHoeher (Polling-Intervall)
Outbox + CDC (Debezium)Sehr hochMittel-HochNiedrig (near-realtime)
Event SourcingSehr hochHochNiedrig

Die Empfehlung fuer die meisten Teams: Outbox + CDC mit Debezium. Es bietet hohe Zuverlaessigkeit bei ueberschaubarer Komplexitaet und near-realtime Latenz.


Best Practices

  1. Outbox-Tabelle minimal halten: Nur Event-Metadaten und Payload, keine Business-Logik in der Outbox-Struktur.
  2. Replication Slots ueberwachen: Ein blockierter Debezium-Connector laesst den PostgreSQL WAL wachsen und kann die Disk volllaufen lassen.
  3. Separate Datenbank-Credentials fuer Debezium: Ein eigener User mit minimalen Rechten (SELECT auf Outbox, Replication-Rolle).
  4. Event-Schema versionieren: Payload-Aenderungen muessen rueckwaertskompatibel sein. Avro mit Schema Registry ist bewaehrt.
  5. Tombstone Records aktivieren: Debezium kann nach dem Outbox-Event ein Tombstone senden, damit Kafka Log Compaction funktioniert.

Fuer Backup-Strategien der Datenbank: Kubernetes Backup und Disaster Recovery. Fuer die richtige Deployment-Strategie: Kubernetes Deployment Strategien.


Verwandte Artikel


Sie implementieren zuverlaessiges Event-Publishing in Ihrer Microservices-Architektur oder evaluieren Debezium fuer Change Data Capture? Wir unterstuetzen Sie bei der Outbox-Architektur, Kafka Connect Konfiguration und dem produktiven Betrieb auf Kubernetes. Kontaktieren Sie uns unter /kontakt.

Kubernetes-Beratung gesucht?

Wir helfen deutschen Unternehmen bei der Kubernetes-Implementierung, Migration und Optimierung. DSGVO-konform und praxiserprobt.

📖 Verwandte Artikel

Weitere interessante Beiträge zu ähnlichen Themen