Ich arbeite an der Entwicklung eines rundenbasierten MMORPG-Gameservers.
Die Low-Level-Engine (NICHT von uns geschrieben), die die Vernetzung, Multi-Threading, Timer, Inter-Server-Kommunikation, Hauptspiel Schleife usw., war geschrieben von C ++. Die High-Level-Spiellogik wurde von Python geschrieben.
Meine Frage betrifft das Design des Datenmodells in unserem Spiel.
Zunächst versuchen wir einfach alle Daten eines Players in den Arbeitsspeicher und die gemeinsamen Daten zu laden Cache-Server, wenn sich der Client anmeldet und einen Timer plant, in den regelmäßig Daten gepuffert werden Datencache-Server und Datencache-Server werden Daten in der Datenbank beibehalten.
Aber wir fanden, dass dieser Ansatz einige Probleme hat
1) Einige Daten müssen sofort gespeichert oder überprüft werden, z. Level up, Artikel & amp; Geldgewinn etc.
2) Laut Spiellogik müssen wir manchmal einige Offline-Spieler abfragen Daten.
3) Einige globale Spielweltdaten müssen zwischen verschiedenen Spielen geteilt werden Instanzen, die möglicherweise auf einem anderen Host oder einem anderen Prozess auf dem Server ausgeführt werden gleicher Gastgeber. Dies ist der Hauptgrund, warum wir einen Datencacheserver zwischen Spielen benötigen Logikserver und Datenbank.
4) Der Spieler muss frei zwischen den Spielinstanzen wechseln können.
Unten ist die Schwierigkeit, der wir in der Vergangenheit begegnet sind:
1) Alle Datenzugriffsoperationen sollten asynchron sein, um Netzwerk-E / A zu vermeiden Blockierung des Hauptspiellogik-Threads. Wir müssen eine Nachricht an die Datenbank senden oder Cache-Server und dann Datenantwortnachricht in Callback-Funktion und Weiter mit der Spiellogik. Es wird schnell schmerzhaft, etwas Moderates zu schreiben komplexe Spiellogik, die mehrere Male mit db und der Spiellogik sprechen muss ist in vielen Callback-Funktionen verstreut macht es schwer zu verstehen und pflegen.
2) Der Ad-hoc-Daten-Cache-Server macht Dinge komplexer, die wir nur schwer pflegen können Datenkonsistenz und effektiv aktualisieren / laden / aktualisieren Daten.
3) In-Game-Datenabfragen sind ineffizient und umständlich, die Spiellogik muss abfragen viele Informationen wie Inventar, Gegenstandsinfo, Avatarzustand usw. Einige Transaktions-Mechanismus wird auch benötigt, zum Beispiel, wenn ein Schritt den gesamten fehlgeschlagen ist Betrieb sollte Rollback sein. Wir versuchen, ein gutes Datenmodellsystem im RAM zu entwerfen, Erstellen einer Menge komplexer Indexe, um zahlreiche Informationsabfragen zu erleichtern, Hinzufügen Transaktionsunterstützung usw. Schnell erkannte ich, was wir bauen, ist ein In-Memory Datenbanksystem erfinden wir das Rad neu ...
Schließlich wende ich mich dem stackless Python zu, wir entfernten den Cacheserver. Alle Daten sind in der Datenbank gespeichert. Spiellogik-Server fragt Datenbank direkt ab. Mit Stapellosen Pythons Mikro-Tasklet und Kanal, können wir Spiellogik in einer synchronisierten schreiben Weg. Es ist viel einfacher zu schreiben und zu verstehen und die Produktivität sehr verbessert.
Tatsächlich ist der zugrunde liegende DB-Zugriff auch asynchron: Ein Client-Tasklet Ausgabeanforderung an einen anderen dedizierten DB I / O-Worker-Thread und das Tasklet lautet auf einem Kanal blockiert, aber die gesamte Hauptspiellogik ist nicht blockiert, andere Client-Tasklet wird geplant und frei ausgeführt. Wenn DB Daten antworten die Das blockierte Tasklet wird aufgeweckt und läuft weiter auf der 'Pause' Punkt '(Fortsetzung?).
Mit dem obigen Design habe ich einige Fragen:
1) Der DB-Zugriff wird häufiger als die vorherige zwischengespeicherte Lösung sein, tut der DB kann hochfrequenten Abfrage- / Aktualisierungsvorgang unterstützen? Hat etwas reifen Cache Lösung wie Redis, Memcached wird in naher Zukunft benötigt?
2) Gibt es irgendwelche gravierenden Fallstricke in meinem Design? Kannst du mir etwas Besseres geben? Vorschläge, vor allem im In-Game-Datenmanagement-Muster.
Jeder Vorschlag wäre dankbar, danke.
Ich habe mit einer MMO-Engine gearbeitet, die auf ähnliche Weise funktioniert. Es wurde jedoch in Java geschrieben, nicht in Python.
In Bezug auf Ihre erste Reihe von Punkten:
1) async db access Wir sind tatsächlich den anderen Weg gegangen und haben es vermieden, einen "Hauptspiel-Logik-Thread" zu haben. Alle Spiellogik Aufgaben wurden als neue Threads erzeugt. Der Overhead der Thread-Erstellung und -Zerstörung ging im Grundrauschen im Vergleich zu I / O vollständig verloren. Dies bewahrte auch die Semantik, jede "Aufgabe" als eine relativ einfache Methode zu haben, anstatt der wahnsinnigen Kette von Rückrufen, mit denen man sonst endet (obwohl es immer noch Fälle davon gab.) Es bedeutete auch, dass der gesamte Spielcode musste gleichzeitig sein, und wir waren zunehmend auf unveränderliche Datenobjekte mit Zeitstempeln angewiesen.
2) Ad-hoc-Cache Wir haben viele WeakReference-Objekte verwendet (ich glaube, Python hat ein ähnliches Konzept?) und auch eine Aufteilung zwischen den Datenobjekten, z. "Player" und der "Loader" (tatsächlich Datenbankzugriffsmethoden), z.B. "PlayerSQLLoader;" Die Instanzen hielten einen Zeiger auf ihren Loader, und die Loader wurden von einer globalen "Factory" -Klasse aufgerufen, die Cache-Lookups im Vergleich zu Netzwerk- oder SQL-Ladeoperationen abwickelte. Jede "Setter" -Methode in einer Datenklasse würde die Methode changed
aufrufen, die eine vererbte Voreinstellung für myLoader.changed (this);
Um das Laden von Objekten von anderen aktiven Servern zu übernehmen, haben wir "Proxy" -Objekte verwendet, die die gleiche Datenklasse verwendeten (wieder "Player"), aber die Loader-Klasse, die wir assoziierten, war ein Netzwerk-Proxy, der (synchron , aber über Gigabit lokalen Netzwerk) aktualisieren Sie die "Master" Kopie dieses Objekts auf einem anderen Server; die "Master" -Kopie wiederum würde changed
selbst aufrufen.
Unsere SQL UPDATE
Logik hatte einen Timer. Wenn die Backend-Datenbank ein UPDATE
des Objekts innerhalb der letzten ($ n) Sekunden erhalten hätte (wir hätten dies normalerweise bei 5 gehalten), würde das Objekt stattdessen zu einer "schmutzigen Liste" hinzugefügt. Eine Hintergrund-Timer-Task würde regelmäßig aufwachen und versuchen, alle Objekte, die sich noch in der "schmutzigen Liste" befinden, asynchron auf das Datenbank-Backend zu übertragen.
Da die globale Factory WeakReferences für alle im Kern befindlichen Objekte verwaltet und nach einer einzelnen instanziierten Kopie eines bestimmten Spielobjekts auf einem beliebigen Live-Server sucht, würden wir niemals versuchen, eine zweite Kopie eines Spielobjekts zu erstellen, das von a unterstützt wird Single-DB-Record, so dass die Tatsache, dass der In-RAM-Status des Spiels von dem SQL-Image für bis zu 5 oder 10 Sekunden zu einem Zeitpunkt abweichen könnte, belanglos war.
Unser gesamtes SQL-System lief im RAM (ja, ein Los von RAM) als ein Spiegel zu einem anderen Server, der tapfer versuchte, auf die Platte zu schreiben. (Diese arme Maschine hat RAID-Laufwerke im Durchschnitt alle 3-4 Monate aufgrund von "altem Alter" ausgebrannt. RAID ist gut.)
Bemerkenswerterweise mussten die Objekte in die Datenbank geleert werden, wenn sie aus dem Cache entfernt wurden, z. aufgrund der Überschreitung der Cache-RAM-Erlaubnis.
3) In-Memory-Datenbank ... Ich war über diese genaue Situation nicht hinweggekommen. Wir hatten eine "transaktionsähnliche" Logik, aber alles geschah auf der Ebene der Java-Getter / Setter.
Und in Bezug auf Ihre letzten Punkte:
1) Ja, insbesondere PostgreSQL und MySQL kommen gut damit zurecht, insbesondere wenn Sie einen RAMdisk-Spiegel der Datenbank verwenden, um den tatsächlichen Festplattenverschleiß zu minimieren. Meiner Erfahrung nach tendieren MMOs dazu, die Datenbank mehr als unbedingt nötig zu hämmern. Unsere "5-Sekunden-Regel" * wurde speziell entwickelt, um zu vermeiden, dass das Problem "richtig" gelöst werden muss. Jeder unserer Setter würde changed
aufrufen. In unserem Nutzungsmuster haben wir festgestellt, dass ein Objekt in der Regel entweder ein Feld geändert hat und dann einige Zeit lang keine Aktivität mehr stattfand oder dass es zu einem "Sturm" von Aktualisierungen gekommen ist, bei dem sich viele Felder in einer Reihe geändert haben. B. das Informieren des Objekts, dass es viele Schreibvorgänge akzeptieren würde, und sollte einen Moment warten, bevor es sich selbst in der Datenbank speichert, würde mehr Planung, Logik und größere Umschreibungen des Systems involviert haben; Stattdessen haben wir die Situation umgangen.
2) Nun, da ist mein Design oben: -)
Tatsächlich arbeitet die MMO-Engine, an der ich gerade arbeite, sogar noch mehr auf In-RAM-SQL-Datenbanken und (hoffentlich) wird es ein bisschen besser machen. Dieses System wird jedoch mit einem Entity-Component-System-Modell und nicht mit dem oben beschriebenen OOP-Modell erstellt.
Wenn Sie bereits auf einem OOP-Modell basieren, ist der Wechsel zu ECS ein Paradigmenwechsel. Wenn Sie OOP für Ihre Zwecke einsetzen können, ist es wahrscheinlich besser, bei dem zu bleiben, was Ihr Team bereits kennt.
* - "Die 5-Sekunden-Regel" ist ein umgangssprachlicher "Volksglaube" der USA, dass es nach dem Abwerfen von Essen auf dem Boden immer noch in Ordnung ist, es zu essen, wenn man es innerhalb von 5 Sekunden aufhebt.
Es ist schwierig, das gesamte Design / Datamodell zu kommentieren, ohne die Software besser zu verstehen, aber es klingt so, als könnte Ihre Anwendung von einer In-Memory-Datenbank profitieren. * Das Sichern solcher Datenbanken auf Disketten ist (relativ gesehen) eine billige Operation . Ich habe festgestellt, dass es im Allgemeinen schneller ist:
A) Erstellen Sie eine speicherinterne Datenbank, erstellen Sie eine Tabelle, fügen Sie eine Million ** Zeilen in die angegebene Tabelle ein und sichern Sie dann die gesamte Datenbank auf der Festplatte
als
B) Fügen Sie eine Million ** Zeilen in eine Tabelle in einer datenträgergebundenen Datenbank ein.
Offensichtlich werden Einfügungen / Aktualisierungen / Löschungen einzelner Datensätze auch schneller im Speicher ausgeführt. Ich hatte Erfolg mit JavaDB / Apache Derby für In-Memory-Datenbanken.
* Beachten Sie, dass die Datenbank nicht in Ihren Spieleserver eingebettet sein muss. ** Eine Million ist möglicherweise keine ideale Größe für dieses Beispiel.
Tags und Links python database python-stackless