Vor ungefähr 10 Jahren haben wir uns für Python 2 entschieden, um unsere monolithische Lernplattform zu entwickeln. Aber die Branche hat sich seitdem dramatisch verändert. Python 2 wurde am 1. Januar 2020 offiziell begraben. Im vorherigen Artikel haben wir erklärt, warum wir uns entschieden haben, nicht auf Python 3 zu migrieren.
Millionen von Menschen nutzen unsere Plattform jeden Monat.
Wir sind ein gewisses Risiko eingegangen, als wir beschlossen, unser Backend in Go neu zu schreiben und die Architektur zu ändern.
Wir haben uns aus mehreren Gründen für Go entschieden:
- Hohe Kompilierungsgeschwindigkeit.
- RAM speichern.
- Eine große Auswahl an IDEs mit Go-Unterstützung.
Wir haben jedoch einen Ansatz gewählt, der das Risiko minimiert.
GraphQL Federation
Wir haben uns entschlossen, unsere neue Architektur um die GraphQL Apollo Federation herum aufzubauen . GraphQL wurde von den Facebook-Entwicklern als Alternative zur REST-API erstellt. Bei Federation geht es darum, ein einziges Gateway für mehrere Dienste zu erstellen. Jeder Dienst kann ein eigenes GraphQL-Schema haben. Ein gemeinsames Gateway kombiniert seine Schemas, generiert eine einzelne API und ermöglicht Anforderungen für mehrere Dienste gleichzeitig.
Bevor wir weiter gehen, möchte ich Folgendes hervorheben:
- Im Gegensatz zu REST-APIs verfügt jeder GraphQL-Server über ein eigenes typisiertes Datenschema. Sie können eine beliebige Kombination genau der Daten mit beliebigen Feldern abrufen, die Sie benötigen.
- Mit dem REST-API-Gateway können Sie eine Anforderung nur an einen Backend-Service senden. Das GraphQL-Gateway generiert einen Abfrageplan für eine beliebige Anzahl von Backend-Diensten und ermöglicht es Ihnen, eine Auswahl davon in einer einzigen generischen Antwort zurückzugeben.
Nachdem wir das GraphQL-Gateway in unser System aufgenommen haben, erhalten wir ungefähr Folgendes:
Bild - URL: https://lh6.googleusercontent.com/6GBj9z5WVnQnhqI19oNTRncw0LYDJM4U7FpWeGxVMaZlP46IAIcKfYZKTtHcl-bDFomedAoxSa9pFo6pdhL2daxyWNX2ZKVQIgqIIBWHxnXEouzcQhO9_mdf1tODwtti5OEOOFeb
- Gateway (auch bekannt als Service graphql-Gateway) ist verantwortlich für die Erstellung und Senden von Abfrageplan GraphQL-Anfragen zu unseren anderen Dienstleistungen - nicht nur den Monolithen. Unsere Go-Dienste haben ihre eigenen GraphQL-Schemata. Wir verwenden gqlgen (dies ist eine GraphQL-Bibliothek für Go) , um Antworten auf Anfragen zu generieren .
Da die GraphQL Federation ein gemeinsames GraphQL-Schema bereitstellt und das Gateway alle einzelnen Dienstschemata in einem bündelt, interagiert unser Monolith wie jeder andere Dienst damit. Dies ist ein grundlegender Punkt.
Als nächstes werden wir darüber sprechen, wie wir den Apollo GraphQL- Server so angepasst haben , dass er sicher von unserem Monolithen (Python) zu einer Microservice-Architektur (Go) aufsteigt.
Side-by-Side-Tests
GraphQL "denkt" mit Mengen von Objekten und Feldern bestimmter Typen. Der Code, der weiß, was mit der eingehenden Anforderung zu tun ist, wie und welche Daten aus den Feldern extrahiert werden sollen, wird als Resolver bezeichnet.
Betrachten wir den Migrationsprozess anhand eines Beispiels für den Datentyp für Zuweisungen:
| 123 | Typ Zuordnung {Erstellungsdatum: Zeit ……….} |
Es ist klar, dass wir in Wirklichkeit viel mehr Felder haben, aber für jedes Feld wird alles gleich aussehen.
Angenommen, wir möchten, dass dieses Monolithfeld in unserem neuen Service in Go dargestellt wird. Wie können wir sicher sein, dass der neue Service on Demand dieselben Daten wie der Monolith zurückgibt? Dazu verwenden wir einen ähnlichen Ansatz wie die Scientist- Bibliothek : Wir fordern Daten sowohl vom Monolithen als auch vom neuen Dienst an, vergleichen dann jedoch die Ergebnisse und geben nur einen von ihnen zurück.
Schritt 1: manueller Modus
Wenn der Benutzer nach dem Wert des Felds createdDate fragt, greift unser GraphQL-Gateway zuerst auf den Monolithen zu (der in Python geschrieben ist, denken Sie daran).
Im ersten Schritt müssen wir sicherstellen, dass das Feld dem neuen Zuweisungsdienst hinzugefügt werden kann, der bereits in Go geschrieben wurde. Die Datei mit der Erweiterung .graphql sollte den folgenden Resolver-Code enthalten:
| 12345 | Typ Zuordnung erweitern Schlüssel(Felder: "id") {id: ID! extern createdDate: Time @migrate (von: "python", state: "manual")} |
Hier verwenden wir Federation, um zu sagen, dass der Dienst dem Zuweisungstyp ein Feld "createdDate" hinzufügt. Auf das Feld wird über die ID zugegriffen. Wir fügen auch eine "geheime Zutat" hinzu - die Migrationsrichtlinie. Wir haben Code geschrieben, der diese Anweisungen versteht und mehrere Schemas erstellt, die das GraphQL-Gateway verwendet, um zu entscheiden, ob eine Anforderung weitergeleitet werden soll.
Im manuellen Modus wird die Anforderung nur an den Monolithcode adressiert. Wir müssen diese Möglichkeit bei der Entwicklung eines neuen Dienstes berücksichtigen. Um den Wert des Felds createdDate abzurufen, können wir weiterhin direkt (im primären Modus) auf den Monolithen zugreifen oder das GraphQL-Gateway im manuellen Modus nach dem Schema abfragen. Beide Optionen sollten funktionieren.
Schritt 2: Side-by-Side-Modus
Nachdem wir den Resolver-Code für das Feld createdDate geschrieben haben, wechseln wir in den Side-by-Side-Modus:
| 12345 | Typ Zuordnung erweitern Schlüssel(Felder: "id") {id: ID! extern createdDate: Time @migrate (von: "python", state: "side-by-side")} |
Und jetzt greift das Gateway sowohl auf den Monolithen (Python) als auch auf den neuen Dienst (Go) zu. Es vergleicht die Ergebnisse, protokolliert die Fälle, in denen es Unterschiede gibt, und gibt das Ergebnis vom Monolithen an den Benutzer zurück.
Dieser Modus vermittelt wirklich viel Vertrauen, dass unser System während des Migrationsprozesses nicht fehlerhaft sein wird. Im Laufe der Jahre haben Millionen von Benutzern und "Kilotonnen" Daten unser Frontend und Backend durchlaufen. Indem wir beobachten, wie dieser Code unter realen Bedingungen funktioniert, können wir sicherstellen, dass auch seltene Fälle und zufällige Ausreißer erfasst und dann stabil und korrekt verarbeitet werden.
Während des Testens erhalten wir solche Berichte.
Versuchen Sie, dieses Bild während des Layouts ohne großen Qualitätsverlust zu vergrößern.
Sie konzentrieren sich auf Fälle, in denen Unstimmigkeiten beim Betrieb des Monolithen und des neuen Dienstes festgestellt werden.
Anfangs sind wir oft auf solche Fälle gestoßen. Im Laufe der Zeit haben wir gelernt, solche Probleme zu identifizieren, sie auf Kritikalität zu bewerten und gegebenenfalls zu beseitigen.
Bei der Arbeit mit unseren Entwicklungsservern verwenden wir Tools, die Farbunterschiede hervorheben. Dies erleichtert die Analyse von Problemen und das Testen von Lösungen.
Was ist mit Mutationen?
Sie fragen sich vielleicht, ob wir in Python und Go dieselbe Logik ausführen. Was passiert mit dem Code, der die Daten ändert, anstatt sie nur abzufragen? In GraphQL wird dies als Mutation bezeichnet.
Unsere Side-by-Side-Tests berücksichtigen keine Mutationen. Wir haben uns einige Ansätze dazu angesehen - sie erwiesen sich als komplexer als wir dachten. Wir haben jedoch einen Ansatz entwickelt, der zur Lösung des Problems der Mutationen beiträgt.
Schritt 2.5: Kanarienvogelmodus
Wenn wir ein Feld oder eine Mutation haben, die bis zur Produktionsphase erfolgreich überlebt hat, aktivieren wir den kanarischen Modus.
| 12345 | Typ Zuordnung erweitern Schlüssel(Felder: "id") {id: ID! extern createdDate: Time @migrate (von: "python", state: "canary")} |
Kanarische Felder und Mutationen werden für einen kleinen Prozentsatz unserer Benutzer zum Go-Service hinzugefügt. Darüber hinaus testen interne Benutzer der Plattform das kanarische Schema. Dies ist eine ziemlich sichere Methode, um komplexe Änderungen zu testen. Wir können den Kanarischen Kreislauf schnell deaktivieren, wenn etwas nicht wie erwartet funktioniert.
Wir verwenden jeweils nur einen Kanarienvogelkreis. In der Praxis befinden sich nicht viele Felder und Mutationen gleichzeitig im kanarischen Modus. Ich denke also, dass es in Zukunft keine Probleme mehr geben wird. Dies ist ein guter Kompromiss, da das Schema ziemlich groß ist (über 5000 Felder) und die Gateway-Instanzen drei Schemas im Speicher speichern müssen - primär, manuell und kanarisch.
Schritt 3: Migrierter Modus
In diesem Schritt sollte sich das Feld createdDate im migrierten Modus befinden:
| 12345 | Typ Zuordnung erweitern Schlüssel(Felder: "id") {id: ID! extern createdDate: Time @migrate (von: "python", state: "migrated")} |
In diesem Modus sendet das GraphQL-Gateway nur Anforderungen an einen neuen Dienst, der in Go geschrieben wurde. Aber wir können jederzeit sehen, wie der Monolith dieselbe Anfrage verarbeitet. Dies erleichtert das Bereitstellen und Zurücksetzen von Änderungen, wenn ein Fehler auftritt.
Schritt 4: Abschluss der Migration
Nach einer erfolgreichen Bereitstellung benötigen wir den Monolith-Code für dieses Feld nicht mehr und entfernen die @ migrate-Direktive aus dem Resolver-Code:
Von nun an interpretiert das Gateway den Ausdruck Assignment.createdDate so, dass er einen Feldwert von einem neuen in Go geschriebenen Dienst erhält.
So ist inkrementelle Migration!
Und wie weit sind wir gegangen?
Erst in diesem Jahr haben wir unsere Side-by-Side-Testinfrastruktur fertiggestellt. Dies ermöglichte es uns, eine Reihe von Go-Codes sicher, langsam aber sicher neu zu schreiben. Während des ganzen Jahres haben wir die hohe Verfügbarkeit der Plattform vor dem Hintergrund des wachsenden Datenverkehrs in unserem System aufrechterhalten. Zum Zeitpunkt dieses Schreibens werden ~ 40% unserer GraphQL-Felder in Go-Dienste verschoben. Der von uns beschriebene Ansatz hat also im Migrationsprozess gut funktioniert.
Auch nach Abschluss des Projekts können wir diesen Ansatz für andere Aufgaben im Zusammenhang mit der Änderung unserer Architektur verwenden.
PS Steve Coffman hielt einen Vortrag zu diesem Thema (auf Google Open Source Live ). Sie können die Aufnahme ansehen dieses YouTube-Gespräch (oder sehen Sie sich einfach die Präsentation an ).
Cloud-Server von Macleod sind schnell und sicher.
Registrieren Sie sich über den obigen Link oder indem Sie auf das Banner klicken und erhalten Sie 10% Rabatt für den ersten Monat der Anmietung eines Servers einer beliebigen Konfiguration!