Am Black Friday des letzten Jahres stand unser Team bei Veltrix vor einem Schockmoment: Unser beliebtes Schatzsuchen-System – eine Plattform, die echte Belohnungen für Nutzer vergibt – brach um 3 Uhr morgens zusammen. Was als einfache Rails-Anwendung mit einer PostgreSQL-Zählerspalte begann, entwickelte sich zum Flaschenhals und kostete uns fast 47.000 Dollar an nicht ausgezahlten Belohnungen.
Der fatale Fehler im PostgreSQL-Zähler
Unser ursprüngliches System nutzte eine einfache Zählerspalte in PostgreSQL, um die Punkte für jede Schatzsuche zu verwalten. Doch unter Last von 2.000 gleichzeitigen Schreibvorgängen eskalierten PostgreSQLs Zeilenblockaden zu Tabellenblockaden – ein klassisches Problem bei SERIAL-Spalten. Innerhalb weniger Minuten stieg unsere Fehlerquote von 0,2 % auf 18 %. Nutzer erhielten Fehlermeldungen wie „Konnte nicht serialisieren aufgrund von gleichzeitigen Aktualisierungen“, und die gesamte Leaderboard-Abfrage blockierte. Die Systeme funktionierten nicht mehr nur langsam, sie brachen komplett zusammen.
Der erste Rettungsversuch – und seine Nebenwirkungen
Wir versuchten, den PostgreSQL-Zähler durch Sharding zu entlasten: Die heiße Zeile wurde in 1.024 Partitionen aufgeteilt, eine pro Schatzsuche. Dadurch reduzierte sich die Blockadeproblematik spürbar. Doch die Lösung brachte neue Herausforderungen mit sich:
- Jede Partition benötigte eine eigene Sequenz, was die Code-Basis komplexer machte.
- Die Leaderboard-Abfragen mussten nun Ergebnisse aus 1.024 Tabellen zusammenführen, was die Latenz auf bis zu 400 Millisekunden erhöhte.
- PostgreSQL-Sequenzen wiesen nach Neustarts Lücken von bis zu 1.024 Werten auf – unsere Belohnungsabrechnungen waren plötzlich um Tausende Punkte daneben.
- Der Redis-Cache konnte die komplexen Abfragen nicht beschleunigen, da Punktabfragen aus 1.024 Tabellen nicht effizient gepipelt werden konnten.
Die Sharding-Lösung war ein zweischneidiges Schwert: Sie löste das Blockadeproblem, schuf aber neue Komplexitäten und Performance-Nachteile.
Die radikale Lösung: Event-Sourcing mit Kafka Streams
Nach dem gescheiterten Sharding-Ansatz entschieden wir uns für einen grundlegenden Architekturwechsel. Wir implementierten HuntStream, ein eventbasiertes System, das auf Apache Kafka und RocksDB basiert:
- Jede Aktion in der Schatzsuche – Punktevergabe oder Belohnungsanspruch – wurde als unveränderliches Ereignis in einem Kafka-Topic gespeichert.
- Eine materialisierte Ansicht in RocksDB konsumierte diese Ereignisse und hielt den aktuellen Leaderboard-Status im Speicher vor.
- Die materialisierte Ansicht war partitioniert nach Schatzsuche-ID, sodass Leaderboard-Abfragen nur noch eine RocksDB-Partition pro Suche benötigten.
- RocksDBs integrierter Cache hielt häufig abgerufene Leaderboards im Speicher, während seltene auf die Festplatte ausgelagert wurden.
Der Wechsel brachte entscheidende Vorteile:
- Genau einmalige Semantik: Jedes Ereignis wurde exakt einmal verarbeitet, was Inkonsistenzen bei der Punktevergabe verhinderte.
- Horizontale Skalierbarkeit: Durch Hinzufügen weiterer Container konnten wir Lastspitzen abfedern.
- Wiederherstellbarkeit: Bei Datenkorruptionen konnten wir Ereignisse neu abspielen und den Zustand rekonstruieren.
Der Nachteil? Die operative Komplexität stieg: Wir mussten nun einen Kafka-Cluster, drei Streams-Anwendungen und die RocksDB-Compaction überwachen. Doch die Vorteile überwogen bei Weitem.
Die Zahlen nach der Umstellung – und eine harte Lektion
Nach der vollständigen Migration von HuntStream verzeichneten wir dramatische Verbesserungen:
- Die Fehlerquote sank von 18 % auf 0,02 % – selbst unter identischer Last.
- Die Latenz der Leaderboard-Abfragen fiel von 400 Millisekunden auf 12 Millisekunden (p99).
- Die Kafka-Broker verarbeiteten 45.000 Ereignisse pro Sekunde, wobei 90 % der Vorgänge unter 5 Millisekunden blieben.
- Die materialisierten RocksDB-Ansichten benötigten 1,8 GB RAM pro Hunt-Instanz.
- Die Belohnungsabrechnung wurde präzise: Durch das Event-Logging konnten wir Abweichungen exakt nachvollziehen und die 1.024-Lücken-Problematik endgültig beheben.
Doch die Umstellung brachte auch unerwartete Herausforderungen mit sich:
- RocksDB-Compaction: Bei einem Traffic-Spike pausierte die Compaction für eine Hunt-Instanz acht Sekunden lang – was zu kurzfristiger Leaderboard-Stilllegung führte. Wir mussten die Compaction-Intervalle anpassen und die IOPS erhöhen.
- Verpasste Testabdeckung: Unser nachträglicher Test fokussierte sich auf Latenz, nicht auf Korrektheit. Eine Simulation mit 5.000 gleichzeitigen Schreibvorgängen hätte die späteren Probleme vermeiden können.
Was ich beim nächsten Mal anders machen würde
Einige Lehren aus diesem Projekt sind so wertvoll, dass ich sie nicht wiederholen würde:
- Kein PostgreSQL-Sharding mehr: Die Komplexität des Shardings brachte kaum Skalierungsvorteile, dafür aber neue Fehlerquellen. Eine eventbasierte Architektur ist die robustere Lösung.
- Managed Stream Processing bevorzugen: Selbstgehosteter Kafka brachte uns operative Kopfschmerzen. Beim nächsten Mal würde ich eine gemanagte Lösung wie Confluent Cloud oder Redpanda in Betracht ziehen – es sei denn, wir benötigen zwingend On-Premise-Kontrolle.
- Integrationstests für Korrektheit: Vor jedem Deployment muss eine Simulation mit extrem hoher Last durchgeführt werden, die nicht nur Performance, sondern auch Datenkonsistenz prüft.
Der Black-Friday-Zusammenbruch war ein Weckruf. Heute wissen wir: Robuste Architektur ist kein Luxus, sondern eine Notwendigkeit – besonders wenn es um Echtzeit-Belohnungen und Nutzererlebnis geht. Mit HuntStream haben wir nicht nur ein System gerettet, sondern ein neues Paradigma für hochskalierbare, zuverlässige Anwendungen geschaffen.
KI-Zusammenfassung
Black Friday trafiğinde çöken liderlik sistemiyle 47 bin dolarlık kayıp yaşayan Veltrix, nasıl PostgreSQL’den Kafka tabanlı olay kaynağı sistemine geçti? Performans verileri ve mimari detaylar.