Wir haben eine Situation, in der ich mit einem massiven Zustrom von Ereignissen zu kämpfen habe, die auf unseren Server kommen, durchschnittlich mit etwa 1000 Ereignissen pro Sekunde (Höchststand könnte ~ 2000 sein).
Das Problem
Unser System wird auf Heroku gehostet und verwendet eine relativ teure Heroku Postgres DB , die maximal 500 DB-Verbindungen ermöglicht. Wir verwenden Connection Pooling, um eine Verbindung vom Server zur Datenbank herzustellen.
Ereignisse gehen schneller ein, als der DB-Verbindungspool verarbeiten kann
Das Problem ist, dass Ereignisse schneller eintreten, als der Verbindungspool verarbeiten kann. Sobald eine Verbindung den Netzwerk-Roundtrip vom Server zur Datenbank beendet hat, kann sie wieder in den Pool freigegeben werden, und es treten mehr als n
zusätzliche Ereignisse auf.
Schließlich häufen sich die Ereignisse und warten darauf, gespeichert zu werden. Da keine Verbindungen im Pool verfügbar sind, tritt eine Zeitüberschreitung auf, und das gesamte System wird außer Betrieb gesetzt.
Wir haben den Notfall gelöst, indem wir die störenden Hochfrequenzereignisse langsamer von den Kunden gesendet haben. Wir möchten jedoch weiterhin wissen, wie wir mit diesen Szenarien umgehen können, falls wir diese Hochfrequenzereignisse verarbeiten müssen.
Einschränkungen
Andere Clients möchten möglicherweise Ereignisse gleichzeitig lesen
Andere Clients fordern ständig an, alle Ereignisse mit einem bestimmten Schlüssel zu lesen, auch wenn sie noch nicht in der Datenbank gespeichert sind.
Ein Client kann GET api/v1/events?clientId=1
alle von Client 1 gesendeten Ereignisse abfragen und abrufen, auch wenn diese Ereignisse noch nicht in der Datenbank gespeichert wurden.
Gibt es Beispiele für den Umgang mit "Klassenzimmern"?
Mögliche Lösungen
Stellen Sie die Ereignisse auf unserem Server in eine Warteschlange
Wir könnten die Ereignisse auf dem Server in eine Warteschlange einreihen (wobei die Warteschlange eine maximale Parallelität von 400 aufweist, damit der Verbindungspool nicht ausgeht).
Das ist eine schlechte Idee, weil:
- Der verfügbare Serverspeicher wird aufgebraucht. Die gestapelten Ereignisse in der Warteschlange belegen enorm viel RAM.
- Unsere Server werden alle 24 Stunden neu gestartet . Dies ist eine von Heroku auferlegte harte Grenze . Der Server kann neu gestartet werden, während Ereignisse in die Warteschlange gestellt werden, wodurch die in die Warteschlange gestellten Ereignisse verloren gehen.
- Es wird der Status auf dem Server eingeführt, wodurch die Skalierbarkeit beeinträchtigt wird. Wenn wir ein Multi-Server-Setup haben und ein Client alle in die Warteschlange eingereihten + gespeicherten Ereignisse lesen möchte, wissen wir nicht, auf welchem Server die in die Warteschlange eingereihten Ereignisse aktiv sind.
Verwenden Sie eine separate Nachrichtenwarteschlange
Ich nehme an, wir könnten eine Nachrichtenwarteschlange verwenden (wie RabbitMQ ?), In der wir die Nachrichten pumpen und auf der anderen Seite gibt es einen anderen Server, der nur die Ereignisse in der Datenbank speichert.
Ich bin nicht sicher, ob in Nachrichtenwarteschlangen Ereignisse in der Warteschlange abgefragt werden können (die noch nicht gespeichert wurden). Wenn also ein anderer Client die Nachrichten eines anderen Clients lesen möchte, kann ich nur die gespeicherten Nachrichten aus der Datenbank und die ausstehenden Nachrichten aus der Warteschlange abrufen und verketten Sie sie zusammen, damit ich sie an den Leseanforderungsclient zurücksenden kann.
Verwenden Sie mehrere Datenbanken, von denen jede einen Teil der Nachrichten mit einem zentralen DB-Koordinator-Server speichert, um sie zu verwalten
Eine andere Lösung besteht darin, mehrere Datenbanken mit einem zentralen "DB-Koordinator / Load Balancer" zu verwenden. Bei Erhalt eines Ereignisses würde dieser Koordinator eine der Datenbanken auswählen, in die die Nachricht geschrieben werden soll. Dies sollte es uns ermöglichen, mehrere Heroku-Datenbanken zu verwenden, wodurch das Verbindungslimit auf 500 x Anzahl von Datenbanken erhöht wird.
Bei einer Leseabfrage könnte dieser Koordinator SELECT
Abfragen an jede Datenbank senden, alle Ergebnisse zusammenführen und sie an den Client zurücksenden, der den Lesevorgang angefordert hat.
Das ist eine schlechte Idee, weil:
- Diese Idee klingt wie ... ähm ... Überentwicklung? Wäre auch ein Albtraum (Backups etc ..). Es ist kompliziert zu bauen und zu warten und wenn es nicht unbedingt notwendig ist, klingt es wie eine KISS- Verletzung.
- Es opfert Beständigkeit . Das Ausführen von Transaktionen über mehrere Datenbanken hinweg ist bei dieser Idee kein Problem.
ANALYZE
die Abfragen selbst ausgeführt und sie sind kein Problem. Ich habe auch einen Prototyp erstellt, um die Verbindungspoolhypothese zu testen und zu überprüfen, ob dies tatsächlich das Problem ist. Die Datenbank und der Server selbst befinden sich auf unterschiedlichen Rechnern, daher die Latenz. Außerdem möchten wir Heroku nicht aufgeben, es sei denn, dies ist absolut notwendig. Die Unbesorgtheit über den Einsatz ist ein großes Plus für uns.
select null
mit 500 Verbindungen. Ich wette, Sie werden feststellen, dass der Verbindungspool dort nicht das Problem ist.