Ein Team lernte auf die harte Tour, dass verteilte Systeme nicht immer die beste Wahl sind. Der Versuch, Konsistenz und Skalierbarkeit durch eine CQRS-Architektur mit PostgreSQL und Kafka zu erreichen, führte zu unerwarteten Performance-Katastrophen. Die Lösung erforderte einen radikalen Paradigmenwechsel: die Zusammenlegung von Events und Aggregaten in einer einzigen Datenbank.
Die ursprüngliche Architektur und ihre Fallstricke
Das System nutzte das CQRS-Muster mit zwei getrennten Speicherschichten: Events wurden in einem Apache Kafka-Cluster gespeichert, während die Aggregatdaten in einer PostgreSQL-Datenbank lagen. Der Outbox-Mechanismus schrieb Events über Debezium in Kafka, während die Read-Seite diese Events konsumierte, um Materialized Views aufzubauen. Das Ziel war eine eventual consistency mit garantiertem Datenverlustschutz. Doch die Realität sah anders aus.
Bei 800 Anfragen pro Sekunde (RPS) betrug die Verzögerung der Materialized Views bereits 2,3 Sekunden. Bei einer Last von 250.000 RPS häuften sich bis zu 4,2 Millionen unprozessierte Events an. Der Consumer stürzte alle 15 Minuten mit Zookeeper-Zeitüberschreitungen ab. Drei Nächte hintereinander weckte PagerDuty das Team um 3 Uhr morgens. Die Ursache lag nicht in der Logik, sondern in der Architektur selbst: Jeder Schreibvorgang dauerte 40 Millisekunden, jeder Lesevorgang 200 Millisekunden – und die Netzwerk- und Datenbankgrenzen addierten unnötige Latenz.
Vergebliche Optimierungsversuche
Das Team unternahm drei größere Anläufe, um die Performance zu verbessern – doch jede Maßnahme verschärfte andere Probleme. Zunächst wurde Kafka auf Version 3.5 aktualisiert und transactionale Producer eingeführt. Die Anzahl der unprozessierten Events sank um 12%, blieb aber bei 3,7 Millionen. Als nächstes wurde die Read-Seite auf eine tiered Architektur mit 12 Kubernetes-Pods in drei Availability Zones skaliert. Doch die CPU-Auslastung der zugrundeliegenden Nodes stieg auf 45%, während die Lese-Latenz im 99. Perzentil auf 900 Millisekunden anwuchs.
Der dritte Versuch bestand darin, Debezium durch Kafka Connect mit JDBC-Quelle zu ersetzen, in der Annahme, Schema-Evolutionen seien der Flaschenhals. Doch dies führte zu 20-sekündigen Schema-Validierungs-Pausen und einer weiteren Verschlechterung: Die Anzahl unprozessierter Events stieg auf 5,1 Millionen. Jede Optimierung verbesserte einen Parameter, verschlechterte aber gleichzeitig einen anderen. Keiner der Ansätze griff das eigentliche Problem an: die künstliche Trennung von Events und Daten durch zwei Datenbanken und zwei Netzwerke.
Der radikale Architekturwechsel
Nach sechs Wochen vergeblicher Optimierungen wurde die Entscheidung getroffen: Die Grenze zwischen Events und Aggregaten musste fallen. Die Events wurden direkt in derselben PostgreSQL-Instanz gespeichert, die auch die Aggregatdaten enthielt. Dazu nutzte man jsonb-Spalten mit einem GIN-Index auf {aggregate_id, event_sequence}.
Kafka wurde durch PostgreSQLs logische Replikation ersetzt. Ein einzelner Go-Dienst übernahm die Rolle des Event-Brokers und leitete die Events über gRPC-Streams an nachgelagerte Consumer weiter. Der Schreibpfad verkürzte sich auf einen einzigen Round-Trip: Client → PostgreSQL → Replikations-Slot → gRPC. Der Lesepfad konnte nun direkt über Foreign Tables innerhalb derselben PostgreSQL-Instanz auf die Events zugreifen, wodurch Materialized Views in nur 50 Millisekunden statt 2,3 Sekunden aktualisiert wurden.
Die Änderungen brachten Trade-offs mit sich: Die automatische Skalierung von Kafka durch Spill-over auf die Festplatte ging verloren, und PostgreSQL musste bei 5 Terabyte pro Node sharded werden. Doch die Vorteile überwogen bei Weitem. Die p99-Latenz sank um 35%, die Latenz-Metriken verschwanden aus dem Dashboard, und die Gesamtbetriebskosten sanken um 28% – unter anderem durch den Wegfall von zwei verwalteten Diensten (Kafka und Debezium) und sechs Prometheus-Exportern.
Die harten Zahlen nach dem Umbau
Ein 48-stündiger Lasttest unter Produktionsbedingungen zeigte dramatische Verbesserungen. Die p99-Schreiblatenz sank von 48 auf 12 Millisekunden, die p99-Lese-Latenz (Materialized Views) von 900 auf 45 Millisekunden. Die Replikations-Slot-Verzögerung blieb während des gesamten Tests bei null. Die CPU-Auslastung der PostgreSQL-Knoten stieg zwar von 35% auf 60%, doch der Arbeitsspeicherdruck blieb dank optimierter Shared Buffers (25% des RAM) stabil.
Die Infrastruktur wurde von 24 Kafka-Brokern auf 12 PostgreSQL-Knoten reduziert. Die monatlichen Kosten für verwaltete Dienste sanken von 18.000 auf 13.000 US-Dollar. Ein neues Problem trat jedoch auf: Bei einem Failover des Primärsystems kam es zu logischen Replikationsverzögerungen. Die Lösung bestand darin, einen Replica als Hot Standby mit pg_rewind zu betreiben, was die Failover-Zeit um 20 Millisekunden erhöhte, aber die Verzögerung während des Wechsels auf null hielt.
Die wichtigste Lektion: Architekturentscheidungen auf Daten stützen
Der größte Fehler des Teams war, von Anfang an separate Systeme für Events und Aggregate zu verwenden. Erst als die Last über 800 RPS stieg und bereits 42.000 US-Dollar an Cloud-Kosten verbrannt waren, wurde klar, dass die Architektur selbst das Problem war. In Zukunft würde man Events von Tag eins an in derselben Datenbank speichern und logische Replikation als Event-Bus nutzen.
Roh-Event-Streams sollten zwar weiterhin in einem Objektspeicher für Replay-Zwecke abgelegt werden, doch die künstlichen Netzwerk- und Datenbankgrenzen gehören der Vergangenheit an. Die zentrale Erkenntnis: Service-Grenzen müssen durch messbare Daten gerechtfertigt sein – nicht durch veraltete Best Practices oder Cargo-Kult-Architekturen. Das Team hat die Rechnung erst nach dem Brand gezahlt. Beim nächsten Mal wird sie vor dem ersten Commit stehen.
Der Aufbau fehlertoleranter Systeme beginnt mit der richtigen Grundlage. Wer vermeiden will, dass Architekturfehler zu nächtlichen PagerDuty-Alarmierungen führen, sollte frühzeitig auf Einfachheit und Messbarkeit setzen – nicht auf Komplexität und Versprechen.
KI-Zusammenfassung
CQRS mimarisinde olayları ve verileri ayrı tutmak performans krizine yol açabilir. Bir ekip, olayları PostgreSQL'de saklayarak nasıl %28 maliyet düşürdü ve sistem gecikmelerini sıfırladı.