- Authors

- Name
- Phillip Pham
- @ddppham
Kubernetes CQRS Pattern: Command Query Responsibility Segregation fuer Microservices
TL;DR
- CQRS trennt Schreib-Operationen (Commands) und Lese-Operationen (Queries) in separate Services mit eigenen Datenmodellen, optimiert fuer ihren jeweiligen Zweck
- Der Write Service nutzt ein normalisiertes Datenmodell fuer Konsistenz, der Read Service denormalisierte Views fuer schnelle Abfragen
- Event-driven Synchronisation mit Kafka oder NATS propagiert Aenderungen vom Write-Modell zum Read-Modell mit Eventual Consistency als bewusstem Trade-off
- Auf Kubernetes werden Read- und Write-Services unabhaengig skaliert: Read-Replicas per HPA bei hoher Query-Last, Write-Service mit begrenzter Skalierung
- CQRS lohnt sich bei hohem Read/Write-Verhaeltnis (10:1 oder mehr) -- bei einfachen CRUD-Anwendungen ist es Overkill
Was ist CQRS und warum auf Kubernetes?
In einer klassischen Architektur verwenden Lese- und Schreib-Operationen dasselbe Datenmodell und dieselbe Datenbank. Das funktioniert fuer einfache Anwendungen, aber in einer Microservices-Architektur stossen Sie schnell an Grenzen:
- Lese-Operationen brauchen denormalisierte Daten, Joins ueber Service-Grenzen und schnelle Antwortzeiten
- Schreib-Operationen brauchen normalisierte Daten, Validierung, Konsistenz-Garantien und Transaktionen
Ein einzelnes Datenmodell kann nicht beides optimal bedienen. CQRS loest dieses Problem durch Trennung: Commands (Schreiben) und Queries (Lesen) werden von unterschiedlichen Services mit unterschiedlichen Datenmodellen verarbeitet.
Klassisch (ein Modell):
Client */} API */} Datenbank (normalisiert)
|
+-- Lesen: Langsame Joins, nicht optimiert
+-- Schreiben: Funktioniert gut
CQRS (zwei Modelle):
Client */} Command API */} Write DB (normalisiert, PostgreSQL)
|
+-- Events */} Kafka
|
Client */} Query API */} Read DB (denormalisiert, Elasticsearch)
Wer die Grundlagen der Datenbank-Trennung in Microservices vertiefen will: Kubernetes Database per Service.
Write-Seite: Command Service auf Kubernetes
Der Command Service verarbeitet alle Schreib-Operationen. Er validiert eingehende Commands, fuehrt die Business-Logik aus und persistiert Aenderungen in der Write-Datenbank.
Command Service Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-command-service
namespace: shop
spec:
replicas: 3
selector:
matchLabels:
app: order-command-service
template:
metadata:
labels:
app: order-command-service
cqrs-role: command
spec:
containers:
- name: command-service
image: registry.example.com/order-command:v2.1.0
ports:
- containerPort: 8080
env:
- name: POSTGRES_HOST
value: "postgres-primary.shop.svc.cluster.local"
- name: POSTGRES_DB
value: "orders_write"
- name: KAFKA_BROKERS
value: "kafka-0.kafka.shop.svc.cluster.local:9092"
- name: KAFKA_TOPIC
value: "order.events"
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
Die Write-Datenbank (PostgreSQL) laeuft typischerweise als StatefulSet mit persistentem Volume. Ein StatefulSet stellt stabile Netzwerk-Identitaeten und geordnetes Scaling sicher -- ideal fuer Datenbanken auf Kubernetes.
Der Command Service sollte konservativ skaliert werden. Zu viele parallele Write-Instanzen erhoehen das Risiko von Konflikten und machen optimistisches Locking komplexer.
Read-Seite: Query Service auf Kubernetes
Der Query Service bedient alle Lese-Anfragen. Er liest aus einem denormalisierten Read Store, der fuer schnelle Abfragen optimiert ist.
Query Service Deployment mit HPA
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-query-service
namespace: shop
spec:
replicas: 3
selector:
matchLabels:
app: order-query-service
template:
metadata:
labels:
app: order-query-service
cqrs-role: query
spec:
containers:
- name: query-service
image: registry.example.com/order-query:v2.1.0
ports:
- containerPort: 8080
env:
- name: ELASTICSEARCH_HOST
value: "elasticsearch.shop.svc.cluster.local:9200"
- name: REDIS_HOST
value: "redis-master.shop.svc.cluster.local:6379"
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-query-hpa
namespace: shop
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-query-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Der Query Service skaliert unabhaengig vom Command Service. Bei einem Black-Friday-Szenario mit 10x mehr Lese-Anfragen skaliert der Query Service auf 20 Replicas, waehrend der Command Service bei 3 Replicas bleibt. Diese unabhaengige Skalierung ist einer der groessten Vorteile von CQRS.
Fuer verschiedene Deployment-Strategien beim Ausrollen neuer Versionen lesen Sie Kubernetes Deployment Strategien.
Event-driven Synchronisation mit Kafka
Das Write-Modell und das Read-Modell muessen synchron gehalten werden. Der Command Service publiziert Events nach jeder Schreib-Operation. Ein Event-Processor konsumiert diese Events und aktualisiert den Read Store.
Kafka Topic Konfiguration
apiVersion: v1
kind: ConfigMap
metadata:
name: kafka-topics-config
namespace: shop
data:
topics.yaml: |
topics:
- name: order.events
partitions: 12
replication_factor: 3
config:
retention.ms: 604800000
cleanup.policy: delete
min.insync.replicas: 2
- name: order.events.dlq
partitions: 3
replication_factor: 3
config:
retention.ms: 2592000000
cleanup.policy: delete
Event-Processor Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-event-processor
namespace: shop
spec:
replicas: 3
selector:
matchLabels:
app: order-event-processor
template:
metadata:
labels:
app: order-event-processor
cqrs-role: projector
spec:
containers:
- name: processor
image: registry.example.com/order-projector:v2.1.0
env:
- name: KAFKA_BROKERS
value: "kafka-0.kafka.shop.svc.cluster.local:9092"
- name: KAFKA_TOPIC
value: "order.events"
- name: KAFKA_CONSUMER_GROUP
value: "order-read-projector"
- name: ELASTICSEARCH_HOST
value: "elasticsearch.shop.svc.cluster.local:9200"
- name: REDIS_HOST
value: "redis-master.shop.svc.cluster.local:6379"
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
Ablauf der Synchronisation
1. Client sendet Command: POST /orders {"product": "K8s-Book", "qty": 1}
2. Command Service:
- Validiert den Command
- Schreibt in PostgreSQL (orders_write)
- Publiziert Event: OrderCreated {orderId: 42, product: "K8s-Book"}
3. Kafka: Event wird an Topic "order.events" geschrieben
4. Event-Processor:
- Konsumiert OrderCreated Event
- Aktualisiert Elasticsearch (denormalisierte Order-View)
- Aktualisiert Redis (Order-Summary Cache)
5. Client liest: GET /orders/42 */} Query Service liest aus Elasticsearch
Fuer die zuverlaessige Event-Publizierung ohne Datenverlust eignet sich das Outbox Pattern.
Eventual Consistency: Der bewusste Trade-off
Zwischen Schreib-Operation und Verfuegbarkeit im Read-Modell vergeht Zeit. Das ist Eventual Consistency -- das Read-Modell ist kurzzeitig veraltet.
Typische Latenz
| Schritt | Dauer |
|---|---|
| Command Service schreibt in PostgreSQL | 5-20ms |
| Event wird an Kafka publiziert | 5-10ms |
| Event-Processor konsumiert Event | 10-50ms |
| Read Store wird aktualisiert | 5-20ms |
| Gesamt | 25-100ms |
In der Praxis sind Read-Modelle innerhalb von 100 Millisekunden aktuell. Fuer die meisten Anwendungsfaelle ist das voellig ausreichend.
Umgang mit Eventual Consistency im Client
Read-your-own-Writes: Nach einem Command leitet der Client die Antwort des Command Service direkt weiter, ohne sofort den Query Service zu befragen.
Client */} POST /orders */} Command Service */} 201 Created {orderId: 42}
Client zeigt: "Bestellung 42 erfolgreich erstellt"
Spaeter (nach 100ms+):
Client */} GET /orders/42 */} Query Service */} 200 OK {orderId: 42, ...}
Version-basierte Konsistenz: Der Command Service gibt eine Version zurueck. Der Query Service liefert die Version mit. Wenn die Version im Read-Modell aelter ist, wartet der Client kurz oder zeigt einen Hinweis.
Event Sourcing als Ergaenzung
Event Sourcing geht einen Schritt weiter als CQRS: Statt den aktuellen Zustand zu speichern, werden alle Events gespeichert. Der aktuelle Zustand wird durch Abspielen aller Events rekonstruiert.
| Szenario | CQRS allein | CQRS mit Event Sourcing |
|---|---|---|
| Audit-Trail erforderlich | Separates Audit-Log noetig | Events sind der Audit-Trail |
| Zustand zu beliebigem Zeitpunkt rekonstruieren | Nicht moeglich | Events bis Zeitpunkt X abspielen |
| Einfache Anwendung | Ausreichend | Overkill |
| Regulatorische Anforderungen | Zusaetzliche Implementierung | Eingebaut |
Event Sourcing erhoeht die Komplexitaet erheblich. Event-Stores muessen versioniert werden, Snapshots sind fuer Performance noetig, und Event-Upcasting bei Schema-Aenderungen ist aufwaendig. Starten Sie mit CQRS ohne Event Sourcing und ergaenzen Sie es nur bei konkretem Bedarf.
Fuer eine ausfuehrliche Behandlung von Event Sourcing auf Kubernetes lesen Sie Kubernetes Event Sourcing.
Materialized Views: Optimierte Read-Modelle
Das Read-Modell muss nicht die gleiche Struktur wie das Write-Modell haben. Materialized Views sind vorberechnete Datenstrukturen, die exakt auf die Abfrage-Patterns zugeschnitten sind.
In der Write-Datenbank sind Bestellungen, Kunden, Produkte und Adressen in separaten normalisierten Tabellen gespeichert. Eine Materialized View im Read-Modell (z.B. ein Elasticsearch-Index) kombiniert alle Informationen in einem einzigen denormalisierten Dokument. Eine Abfrage des Order-Dashboards braucht keinen Join -- alle Daten sind vorberechnet.
Beispiel: Verschiedene Read-Modelle fuer verschiedene Abfragen
| Abfrage | Read Store | Datenstruktur |
|---|---|---|
| Order-Detail-Ansicht | Elasticsearch | Vollstaendiges denormalisiertes Dokument |
| Order-Liste mit Filtern | Elasticsearch | Index mit Facetten und Volltextsuche |
| Order-Anzahl pro Status | Redis | Sorted Set mit Zaehler pro Status |
| Dashboard-Aggregation | Redis | Vorberechnete Summen und Durchschnitte |
Jeder Event-Processor kann mehrere Read-Modelle gleichzeitig aktualisieren. Ein einzelnes OrderCreated-Event fuehrt zu Updates in Elasticsearch, Redis und ggf. weiteren Stores.
Wann CQRS sinnvoll ist -- und wann nicht
CQRS lohnt sich bei
- Hohem Read/Write-Verhaeltnis: 10:1 oder mehr -- typisch fuer E-Commerce-Kataloge, Dashboards, Reporting
- Unterschiedlichen Skalierungs-Anforderungen: Read-Traffic schwankt stark (Black Friday), Write-Traffic bleibt konstant
- Komplexen Abfragen ueber Service-Grenzen: Wenn ein Dashboard Daten aus 5 Services aggregiert, ist eine denormalisierte Read-View effizienter als 5 synchrone API-Calls
- Unterschiedlichen Konsistenz-Anforderungen: Writes brauchen strenge Konsistenz, Reads koennen mit leicht veralteten Daten arbeiten
CQRS ist Overkill bei
- Einfachen CRUD-Anwendungen: Wenn Read und Write die gleichen Daten in der gleichen Struktur nutzen
- Starker Konsistenz-Anforderung: Wenn das Read-Modell immer den aktuellsten Zustand zeigen muss
- Wenigen Nutzern: Bei 100 gleichzeitigen Nutzern lohnt sich die Komplexitaet zweier Datenmodelle nicht
- Kleinen Teams: CQRS verdoppelt die Anzahl der zu wartenden Services und Datenbanken
Monitoring und Observability
CQRS-spezifische Metriken
| Metrik | Beschreibung | Alert-Schwelle |
|---|---|---|
| event_processing_lag | Verzoegerung zwischen Event-Publizierung und Verarbeitung | Ueber 5 Sekunden |
| read_model_staleness | Alter des neuesten Eintrags im Read-Modell | Ueber 10 Sekunden |
| command_success_rate | Anteil erfolgreicher Commands | Unter 99 Prozent |
| query_latency_p99 | 99. Perzentil der Query-Antwortzeit | Ueber 200ms |
| kafka_consumer_lag | Unverarbeitete Events im Kafka-Topic | Ueber 1000 |
# Kafka Consumer Lag pruefen
kubectl exec -n shop kafka-0 -- \
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group order-read-projector --describe
# Elasticsearch Index Health
kubectl exec -n shop deploy/order-query-service -- \
curl -s "elasticsearch.shop.svc.cluster.local:9200/_cat/indices?v"
Best Practices
- Starten Sie ohne CQRS und fuehren Sie es erst ein, wenn die Lese-Performance zum Engpass wird
- Event-Schema versionieren: Aenderungen am Event-Format muessen rueckwaertskompatibel sein
- Dead Letter Queues einrichten: Events, die der Projector nicht verarbeiten kann, landen in einer DLQ statt verloren zu gehen
- Idempotente Projektoren: Der Event-Processor muss dasselbe Event mehrfach verarbeiten koennen, ohne den Read-Store zu korrumpieren
- Read-Modell rebuild-faehig halten: Bei Schema-Aenderungen muessen Sie das Read-Modell aus allen Events neu aufbauen koennen
- Separate Skalierung nutzen: Query Service per HPA skalieren, Command Service konservativ skalieren
Fuer Saga-basierte Workflows, die auf CQRS aufbauen, lesen Sie Kubernetes Saga Pattern.
Sie planen die Einfuehrung von CQRS in Ihrer Microservices-Architektur oder kaempfen mit Lese-Performance-Problemen bei hoher Last? Wir beraten bei der Architektur von Read/Write-Trennung, Event-Streaming mit Kafka und der Skalierung 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
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.
Database per Service: Datenbank-Strategien auf Kubernetes
Database per Service vs Shared Database auf Kubernetes: Daten-Isolation, Eventual Consistency, CQRS und StatefulSets mit YAML-Beispielen.
Outbox Pattern auf Kubernetes mit Debezium und Kafka
Transactional Outbox auf Kubernetes umsetzen: CDC mit Debezium, Kafka Connect Integration und produktionsreife YAML-Beispiele für Microservices.
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.