- Authors

- Name
- Phillip Pham
- @ddppham
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
| Metrik | Quelle | Alert-Schwelle |
|---|---|---|
| Outbox Table Size | PostgreSQL | Ueber 100.000 Zeilen |
| Debezium Lag (ms) | Kafka Connect | Ueber 30 Sekunden |
| Connector Status | Kafka Connect REST API | Status != RUNNING |
| Consumer Lag | Kafka | Topic-abhaengig |
| Replication Slot Size | PostgreSQL | Ueber 1 GB |
Fuer umfassende Health Checks und Probes: Kubernetes Health Checks und Probes.
Outbox Pattern vs. Alternativen
| Ansatz | Zuverlaessigkeit | Komplexitaet | Latenz |
|---|---|---|---|
| Dual Write | Niedrig (Inkonsistenz) | Niedrig | Niedrig |
| Outbox + Polling | Hoch | Mittel | Hoeher (Polling-Intervall) |
| Outbox + CDC (Debezium) | Sehr hoch | Mittel-Hoch | Niedrig (near-realtime) |
| Event Sourcing | Sehr hoch | Hoch | Niedrig |
Die Empfehlung fuer die meisten Teams: Outbox + CDC mit Debezium. Es bietet hohe Zuverlaessigkeit bei ueberschaubarer Komplexitaet und near-realtime Latenz.
Best Practices
- Outbox-Tabelle minimal halten: Nur Event-Metadaten und Payload, keine Business-Logik in der Outbox-Struktur.
- Replication Slots ueberwachen: Ein blockierter Debezium-Connector laesst den PostgreSQL WAL wachsen und kann die Disk volllaufen lassen.
- Separate Datenbank-Credentials fuer Debezium: Ein eigener User mit minimalen Rechten (SELECT auf Outbox, Replication-Rolle).
- Event-Schema versionieren: Payload-Aenderungen muessen rueckwaertskompatibel sein. Avro mit Schema Registry ist bewaehrt.
- 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
- Kubernetes Dapr: Sidecar-Pattern fuer Microservices
- Kubernetes KEDA: Event-driven Autoscaling
- Kubernetes Deployment Strategien
- Kubernetes Backup und Disaster Recovery
- Kubernetes Health Checks und Probes
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
CQRS Pattern auf Kubernetes: Read/Write-Trennung umsetzen
CQRS Pattern auf Kubernetes umsetzen: Getrennte Read/Write-Services deployen, Event-driven Synchronisation mit Kafka und unabhängiges Scaling per HPA.
Event Sourcing auf Kubernetes mit Kafka und CQRS
Event Sourcing auf Kubernetes umsetzen: Event Stores mit Kafka oder EventStoreDB als StatefulSet, CQRS-Projections und Schema-Evolution für Microservices.
Kubernetes CQRS Pattern Microservices in Deutschland optimal nutzen
Optimieren Sie Skalierbarkeit und Performance Ihrer komplexen Microservices auf Kubernetes in Deutschland mit dem CQRS Pattern. Erfahren Sie, wie diese zukunftsweisende Architektur Compliance-Anforderungen erfüllt und digitale Souveränität für deutsche Unternehmen sichert.
Distributed Tracing: Jaeger auf Kubernetes einrichten
Jaeger für Distributed Tracing auf Kubernetes einrichten mit dem Jaeger Operator. OpenTelemetry-Instrumentation in Go und Python, Trace-Propagation zwischen Microservices und praktische Analyse von Latenz-Problemen.
Apache Kafka auf Kubernetes mit Strimzi betreiben
Apache Kafka mit dem Strimzi Operator auf Kubernetes deployen und verwalten. Komplette Anleitung für Cluster, Topics und Skalierung.