Veröffentlicht am

CQRS Pattern auf Kubernetes: Read/Write-Trennung umsetzen

Teilen:
Authors

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

SchrittDauer
Command Service schreibt in PostgreSQL5-20ms
Event wird an Kafka publiziert5-10ms
Event-Processor konsumiert Event10-50ms
Read Store wird aktualisiert5-20ms
Gesamt25-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.

SzenarioCQRS alleinCQRS mit Event Sourcing
Audit-Trail erforderlichSeparates Audit-Log noetigEvents sind der Audit-Trail
Zustand zu beliebigem Zeitpunkt rekonstruierenNicht moeglichEvents bis Zeitpunkt X abspielen
Einfache AnwendungAusreichendOverkill
Regulatorische AnforderungenZusaetzliche ImplementierungEingebaut

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

AbfrageRead StoreDatenstruktur
Order-Detail-AnsichtElasticsearchVollstaendiges denormalisiertes Dokument
Order-Liste mit FilternElasticsearchIndex mit Facetten und Volltextsuche
Order-Anzahl pro StatusRedisSorted Set mit Zaehler pro Status
Dashboard-AggregationRedisVorberechnete 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

MetrikBeschreibungAlert-Schwelle
event_processing_lagVerzoegerung zwischen Event-Publizierung und VerarbeitungUeber 5 Sekunden
read_model_stalenessAlter des neuesten Eintrags im Read-ModellUeber 10 Sekunden
command_success_rateAnteil erfolgreicher CommandsUnter 99 Prozent
query_latency_p9999. Perzentil der Query-AntwortzeitUeber 200ms
kafka_consumer_lagUnverarbeitete Events im Kafka-TopicUeber 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

  1. Starten Sie ohne CQRS und fuehren Sie es erst ein, wenn die Lese-Performance zum Engpass wird
  2. Event-Schema versionieren: Aenderungen am Event-Format muessen rueckwaertskompatibel sein
  3. Dead Letter Queues einrichten: Events, die der Projector nicht verarbeiten kann, landen in einer DLQ statt verloren zu gehen
  4. Idempotente Projektoren: Der Event-Processor muss dasselbe Event mehrfach verarbeiten koennen, ohne den Read-Store zu korrumpieren
  5. Read-Modell rebuild-faehig halten: Bei Schema-Aenderungen muessen Sie das Read-Modell aus allen Events neu aufbauen koennen
  6. 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