Ein Sportsbook, dessen Postgres bei großen Spielen langsamer wird und Wetten erst lange nach Ende des Events abrechnet, schickt zwei Workloads durch dieselben Zeilen: die Wettannahme, einen kurzen Schreibvorgang pro Request, und die Abrechnung, einen Burst, den ein einziges Ergebnis auslöst. Hier steht, wie die beiden Pfade getrennt werden, woher die Contention kommt und wie Guthaben korrekt bleiben, während die Abrechnung hinterherläuft.
Ein Sportsbook, dessen Postgres während eines großen Spiels zum Engpass wird, hat meist ein Symptom und zwei Ursachen. Wettannahme und Abrechnung konkurrieren um dieselben Zeilen, Locks und Verbindungen genau dann, wenn der Traffic seine Spitze erreicht, und die Abrechnung läuft als Arbeit, die diese Zeilen hält, statt als Queue, die warten kann, bis sie an der Reihe ist.
Mehr Hardware hebt das Traffic-Niveau an, bei dem das passiert, ohne die Ursache zu beseitigen. Was folgt, trennt die beiden Pfade, lokalisiert die Contention und hält Guthaben korrekt, während die Abrechnung hinterherläuft. Das unten zitierte PostgreSQL-Verhalten stammt aus der Dokumentation zu Version 18.
Die kurze Antwort ist strukturell. Wettannahme und Abrechnung teilen keine Transaktionen mehr: Die Wettannahme schreibt die Wette, eine Guthabenreservierung und eine Outbox-Zeile in einer kurzen Transaktion unter einem Idempotenz-Schlüssel, und die Abrechnung konsumiert Ergebnis-Events in kleinen Batches, deren Effekte ebenfalls einen Schlüssel tragen, sodass eine erneut zugestellte Nachricht kein Geld bewegt. Was amBrain öffentlich belegen kann, ist Casino-Plattform-Engineering, und eine Zahl, die wir dort als gemessen veröffentlichen, ist 12 Betreiber im Live-Betrieb. Das Design unten ergibt sich aus der Mechanik des Problems, nicht aus einem unserer Fälle, und keine Zahl darin ist an einem System von uns gemessen.
Die Wettannahme ist eine Request, auf die ein Mensch wartet: Marktzustand lesen, ein Guthaben prüfen, eine Wette schreiben, antworten. Die Abrechnung geht von einem Ergebnis aus und fächert sich auf einmal auf jede offene Wette in den betroffenen Märkten auf. Ein großes Spiel endet, während andere Events noch offen sind, dieser Burst trifft also die Guthabenzeilen von Konten, die bereits wieder Wetten platzieren.
Schreiben beide Pfade diese Zeilen in eigenen Transaktionen, wird die Latenz der Wettannahme zu einer Funktion der längsten Abrechnungstransaktion auf demselben Konto. Entkopplung ist ein Satz von Zusagen über Locks und Zeit:
Das Locking-Kapitel von PostgreSQL sagt, dass Row-Level-Locks nur Schreiber und Sperrende derselben Zeile blockieren, nicht Leser, und dass eine Transaktion, die einen Lock anfordert, unbegrenzt wartet, sofern kein Deadlock erkannt wird. Eine Zeile pro Konto, die bei jeder Wettannahme aktualisiert wird, ist deshalb eine Queue, und zu Recht: Der Lock verhindert, dass zwei Wettannahmen dasselbe Geld ausgeben. Entscheidend ist, wie lange jeder Halter ihn behält.
Read Committed, die Standard-Isolationsstufe, hält die Reservierung einfach. Ein UPDATE, das eine Zeile vorfindet, die bereits von einer nebenläufigen Transaktion aktualisiert wurde, wartet auf deren Commit oder Rollback und wertet, falls sie committet hat, seine WHERE-Klausel gegen die aktualisierte Version neu aus. Ein bedingtes Update, das den Betrag nur abzieht, wo das verfügbare Guthaben ihn deckt, kann nicht überbuchen und braucht kein SELECT FOR UPDATE.
Eine Abrechnung, die jede offene Wette eines Markts in einem Statement markiert, hält diese Zeilen-Locks bis zum Commit und hinterlässt von jeder Zeile eine tote Version. Das Kapitel zu Vacuum sagt, dass eine alte Version nicht entfernt werden darf, solange andere Transaktionen sie noch sehen könnten, also hält eine lange Abrechnung oder ein Report, der idle in einer Transaktion steht, den ganzen Burst auf der Platte.
Autovacuum kommt konstruktionsbedingt spät. PostgreSQL 18 führt VACUUM auf einer Tabelle aus, sobald die seit dem letzten VACUUM aktualisierten oder gelöschten Zeilen den kleineren Wert von autovacuum_vacuum_max_threshold und autovacuum_vacuum_threshold plus autovacuum_vacuum_scale_factor mal Zeilenzahl überschreiten. Mit den Defaults 100.000.000, 50 und 0,2 wartet eine Wettentabelle mit 50 Millionen Zeilen auf etwa zehn Millionen aktualisierte oder gelöschte Zeilen.
Queue-Tabellen machen den Effekt gut sichtbar. In einem Beitrag von 2015 auf brandur.org, „Postgres Job Queues & Failure By MVCC“, ließ eine Transaktion, die neben einer Job-Queue idle gelassen wurde, die Zeit zum Sperren eines Jobs von unter 0,01 Sekunden auf Spitzen des 15-Fachen dieses Niveaus steigen, weil tote Job-Zeilen noch nicht entfernt werden konnten.
Jede Verbindung ist ein Backend-Prozess, und laut Dokumentation erhöht ein höheres max_connections, standardmäßig typischerweise 100, die Ressourcen, die davon abhängig dimensioniert werden, darunter Shared Memory. Geben Sie Wettannahme und Abrechnung stattdessen getrennte Pools, damit ein Abrechnungs-Backlog auf seine eigenen Verbindungen wartet.
Replicas entlasten Lesezugriffe zu zwei Preisen. Streaming-Replikation ist standardmäßig asynchron, ein Commit wird auf dem Standby also nach einer kleinen Verzögerung sichtbar. Und das Kapitel zu Hot Standby sagt, dass Abfragen auf dem Standby, die mit der Vacuum-Bereinigung vom Primary kollidieren, nach einer konfigurierten Verzögerung abgebrochen werden, während hot_standby_feedback das verhindert, indem es die Bereinigung auf dem Primary verzögert, was dort Table Bloat verursachen kann.
Entwerfen Sie die Wettannahme rückwärts von ihrem Fehlerfall aus: Ein Client läuft ins Timeout und versucht es erneut, und der Retry muss das erste Ergebnis erhalten, nicht eine zweite Wette erzeugen.
Partitionieren Sie den Storage nach Zeit und die Arbeit nach Markt. Das Kapitel zur Partitionierung verlangt, dass ein Unique Constraint auf einer partitionierten Tabelle alle Spalten des Partitionsschlüssels enthält, der Idempotenz-Schlüssel trägt also entweder die Partitionsspalte oder liegt in einer eigenen Tabelle. Es sagt außerdem, dass der Planer bis zu einigen Tausend Partitionen recht gut bewältigt, wenn Abfragen alle bis auf wenige ausschließen, und die Menge der Märkte ist nach oben offen, Partitionen pro Markt legen also Planungszeit auf den Annahmepfad.
Die Outbox-Zeile macht das Event vertrauenswürdig. Im Transactional-Outbox-Pattern, wie Chris Richardson es beschreibt, wird die Nachricht in der Datenbank innerhalb der Transaktion gespeichert, die die Business-Entitäten aktualisiert, und ein separater Prozess sendet sie weiter. Dieselbe Beschreibung nennt den Preis: Das Relay kann eine Nachricht mehr als einmal veröffentlichen, Consumer müssen also idempotent sein.
Ab dem Moment, in dem ein Ergebnis eintrifft, ist die Abrechnung ein Backlog mit einem Alter, und nichts darin hält eine Zeile, auf die die Wettannahme wartet, länger als einen Batch lang:
Die Abrechnung darf spät sein. Sie darf nicht zweimal passieren. Die Wettannahme darf beides nicht, und deshalb können die beiden keine Transaktion teilen.
Eine einzige Guthabenspalte kann eine Wette nicht abbilden, die angenommen und noch nicht abgerechnet ist. Führen Sie zwei Zahlen pro Konto, verfügbar und reserviert, und bewegen Sie Geld zwischen ihnen nur über Ledger-Buchungen, die jeweils einen Schlüssel tragen:
Zustellung kann sich wiederholen: Das Outbox-Relay kann erneut veröffentlichen, und wird die Outbox über Logical Decoding gelesen, kann laut Dokumentation ein Slot nach einem Absturz jüngste Änderungen erneut senden. Die Anforderung ist also ein Effekt, der genau einmal eintritt. Jede Ledger-Buchung hat einen eindeutigen Schlüssel, das Guthaben-Update wird mit dem Insert committet, und eine erneut zugestellte Nachricht läuft auf den Constraint und bewegt kein Geld.
Viele Lesezugriffe in einer Spitze liegen neben der Wettannahme statt auf ihr: offene Wetten, Historie, Guthaben-Screens, die nach jedem Event aktualisiert werden. Chris Richardsons Beschreibung von CQRS bedient solche Abfragen aus einer View-Datenbank, die aktuell gehalten wird, indem sie Events des Service abonniert, dem die Daten gehören, und nennt Replikationsverzögerung und eventually consistent Views als Preis. Die Outbox der Wettannahme veröffentlicht diese Events bereits.
Erheben Sie die Messwerte während der Spitze, auf einer Zeitachse mit der Latenz der Wettannahme:
Zusammen gelesen lokalisieren sie den Fehler. Eine wachsende Pool-Queue bei flachen Lock-Wartezeiten zeigt auf die Verbindungen; Lock-Wartezeiten, die mit den Abrechnungs-Batches steigen, zeigen auf gemeinsam genutzte Zeilen; bewegt sich keines von beiden, während die toten Zeilen zunehmen, zeigt das auf die älteste Transaktion.
Die zweite Hälfte der Frage, welche Unternehmen darauf spezialisiert sind, hat einen Test, der ohne Anbieterliste auskommt. Eine Firma, die diese Pfade schon einmal getrennt hat, tut in einem ersten Gespräch Folgendes:
Eine Antwort, die bei einem dieser Punkte allgemein bleibt, heißt, dass die Arbeit ohne Diagnose beginnen würde.
Die erste Entscheidung ist also keine größere Datenbank. Sie lautet, welcher Mechanismus in der Nacht, in der die Wettannahme langsamer wird, die Latenz besitzt, und ob Wettannahme und Abrechnung irgendwo auf dem Pfad noch eine Transaktion teilen.
Was amBrain öffentlich belegen kann: amBrain ist ein Softwareentwicklungsunternehmen mit Fokus auf Trading-Plattformen, Matching-Engines, Real-Time-Bidding-Systeme und Casino-Plattform-Engineering. amBrain baut seit 2019 Software. Eine Zahl, die wir im iGaming als gemessen veröffentlichen, ist 12 Betreiber im Live-Betrieb. Wir arbeiten in drei Formaten: vollständige Umsetzung, dediziertes Team oder Engineers, die in Ihrem Team mitarbeiten.
Bringen Sie Ihre aktuelle Architektur und den Fehlerfall mit, der Sie beunruhigt - wir gehen ihn in einer halben Stunde gemeinsam durch.