WG Contract API: Zoo der Dienstleistungen



Mit der Zunahme der Anzahl der Komponenten in einem Softwaresystem wächst normalerweise auch die Anzahl der Personen, die an seiner Entwicklung beteiligt sind. Um das Entwicklungstempo und die Wartungsfreundlichkeit aufrechtzuerhalten, sollten daher Ansätze zur Organisation der API besonders berücksichtigt werden.

Wenn Sie sich genauer ansehen möchten, wie das Team der Wargaming Platform mit der Komplexität eines Systems von mehr als hundert miteinander interagierenden Webdiensten umgeht, sind Sie bei cat willkommen.

Hallo allerseits! Mein Name ist Valentine und ich bin Ingenieur auf der Plattform bei Wargaming. Für diejenigen, die nicht wissen, was die Plattform ist und was sie tut, werde ich hier einen Link zur jüngsten Veröffentlichung eines meiner Kollegen hinterlassen - max_posedon

Im Moment arbeite ich seit mehr als fünf Jahren in der Firma und habe teilweise die Zeit des aktiven Wachstums von World of Tanks gefunden. Um die in diesem Artikel angesprochenen Probleme aufzudecken, muss ich mit einem kurzen Exkurs in die Geschichte der Wargaming-Plattform beginnen.

Ein bisschen Geschichte


Die wachsende Beliebtheit von „Panzern“ erwies sich als Lawinenart, und wie es in solchen Fällen normalerweise der Fall ist, begann sich die Infrastruktur rund um das Spiel schnell zu entwickeln. Infolgedessen wuchs das Spiel schnell mit verschiedenen Webdiensten über, und zum Zeitpunkt meines Beitritts zum Team lag die Punktzahl bereits bei zehn (jetzt funktionieren übrigens mehr als 100 Plattformkomponenten und kommen dem Unternehmen zugute).

Mit der Zeit kamen neue Spiele heraus, und es war nicht mehr einfach, die Feinheiten der Integration zwischen Webdiensten zu verstehen. Die Situation verschlechterte sich erst, als Teams aus anderen Wargaming-Büros an der Entwicklung der Plattform teilnahmen. Die Entwicklung hat sich verteilt, mit allen Konsequenzen in Form von Entfernung, Zeitzonen und Sprachbarriere. Und es gibt noch mehr Dienstleistungen. Es ist nicht so einfach, eine Person zu finden, die versteht, wie die Plattform insgesamt funktioniert. Informationen mussten oft in Teilen aus verschiedenen Quellen gesammelt werden.

Die Schnittstellen verschiedener Webdienste können sich in der stilistischen Leistung stark unterscheiden, was den Integrationsprozess mit der Plattform noch schwieriger macht. Direkte Abhängigkeiten zwischen Komponenten reduzierten die Entwicklungsflexibilität, indem sie die Zerlegung der Funktionalität innerhalb der Plattform erschwerten. Um die Sache noch schlimmer zu machen, kannten Spiele - Kunden der Plattform - unsere Topologie gut, da sie sich direkt in jeden Plattformdienst integrieren mussten. Dies gab ihnen die Möglichkeit, über horizontale Verbindungen für die Implementierung bestimmter Verbesserungen direkt in der Komponente einzutreten, in die sie integriert sind. Dies führte zum Auftreten doppelter Funktionen in verschiedenen Komponenten der Plattform sowie zur Unfähigkeit, die vorhandenen Funktionen auf andere Spiele auszudehnen. Es wurde klar, dass es eine Sackgasse der Entwicklung ist, weiterhin eine Plattform um jedes spezifische Spiel herum aufzubauen. Wir brauchten technische und organisatorische Änderungen, wodurch wir die wachsende Komplexität eines schnell wachsenden Systems kontrollieren und alle Plattformfunktionen für jedes Spiel geeignet machen konnten.

Damit möchte ich die historische Exkursion beenden und schließlich über eine unserer technischen Lösungen sprechen, die dazu beiträgt, die Komplexität zu kontrollieren, die durch die ständig wachsende Anzahl von Diensten verursacht wird. Darüber hinaus werden die Kosten für die Entwicklung neuer Funktionen gesenkt und die Integration in die Plattform erheblich vereinfacht.

Treffen Sie die Vertrags-API


Innerhalb der Plattform nennen wir es die Vertrags-API. Im Kern handelt es sich um ein Integrationsframework, das durch eine Reihe von Dokumentationen und Clientbibliotheken für jede Technologie aus unserem Stack (Erlang / Elixir, Java / Scala, Python) dargestellt wird. Es wird zunächst entwickelt, um die Integration von Plattformkomponenten untereinander zu vereinfachen. Zweitens, um uns bei der Lösung einer Reihe der folgenden Probleme zu helfen:

  • stilistische Unterschiede der Programmschnittstellen
  • das Vorhandensein direkter Abhängigkeiten zwischen Komponenten
  • Dokumentation auf dem neuesten Stand halten
  • Selbstbeobachtung und Debugging von End-to-End-Funktionen

Also, das Wichtigste zuerst.

Stilistische Unterschiede bei den Softwareschnittstellen


Meiner Meinung nach ist dieses Problem auf eine Kombination mehrerer Faktoren zurückzuführen:

  • Fehlen eines strengen Standards, wie die API aussehen sollte. Die Empfehlungen haben oft nicht den gewünschten Effekt, die API ist immer noch anders. Vor allem, wenn die Entwicklung von Teams aus verschiedenen Büros des Unternehmens durchgeführt wird. Jedes Team hat seine eigenen Gewohnheiten und Praktiken. Zusammengenommen sehen solche APIs oft nicht wie Teile eines Ganzen aus.
  • Fehlen eines einzigen Verzeichnisses mit den Namen und Formaten geschäftsspezifischer Entitäten. In der Regel können Sie eine Entität nicht aus dem Ergebnis einer API entnehmen und an die API eines anderen Dienstes übergeben. Dies erfordert eine Transformation.
  • Fehlen eines obligatorischen zentralen Überprüfungssystems für die API. Es gibt immer Fristen und es ist keine Zeit, Updates zu sammeln und darüber hinaus Änderungen an der API vorzunehmen, was sich tatsächlich oft als bereits halb getestet herausstellt.

Das erste, was wir beim Entwerfen der Vertrags-API getan haben, war zu sagen, dass die API von nun an zur Plattform und nicht zu einer einzelnen Komponente gehört. Dies führte dazu, dass die Entwicklung neuer Funktionen mit einer Pull-Anforderung an eine zentralisierte Speicher-API beginnt. Derzeit verwenden wir das GIT-Repository als Speicher. Der Einfachheit halber haben wir die gesamte API in separate Geschäftsfunktionen unterteilt, die Struktur dieser Funktion formalisiert und als Vertrag bezeichnet.

Seitdem sollte jede neue Geschäftsfunktion in unserer Vertrags-API in einem speziellen Format beschrieben werden und die Pull-Anfrage mit einer obligatorischen Überprüfung durchlaufen. Es gibt keine andere Möglichkeit, eine neue API in der Vertrags-API zu veröffentlichen. Im selben Repository haben wir ein Verzeichnis geschäftsspezifischer Entitäten definiert und vorgeschlagen, dass Vertragsentwickler diese wiederverwenden, anstatt diese Entitäten selbst zu beschreiben.

So haben wir eine konzeptionell integrierte Plattform-API erhalten, die wie ein einziges Produkt aussah, obwohl sie tatsächlich auf vielen Plattformkomponenten mit verschiedenen technologischen Stacks implementiert wurde.

Das Vorhandensein direkter Abhängigkeiten zwischen Komponenten


Unser Problem manifestierte sich in der Tatsache, dass jede Plattformkomponente wissen musste, wer speziell die von ihr benötigten Funktionen bedient.

Und es war nicht einmal die Schwierigkeit, dieses Verzeichnis auf dem neuesten Stand zu halten, sondern die Tatsache, dass direkte Abhängigkeiten die Migration der Geschäftsfunktionalität von einer Plattformkomponente zu einer anderen erheblich erschwerten. Das Problem war besonders akut, als wir mit der Zersetzung unserer Monolithen in kleinere Komponenten begannen. Es stellte sich heraus, dass es keine triviale Verwaltungsaufgabe ist, den Kunden davon zu überzeugen, die Arbeitsintegration aus geschäftlicher Sicht durch eine Funktionalität zu ersetzen, die aus technischer Sicht eine andere ist. Der Kunde sieht darin einfach keinen Sinn, da für ihn alles gut funktioniert. Infolgedessen wurden schlecht riechende Schichten der Abwärtskompatibilität geschrieben, die die Unterstützung der Plattform nur erschwerten und sich negativ auf die Servicequalität auswirkten. Und da wir die Plattform-API bereits standardisieren wollten, musste dieses Problem gleichzeitig gelöst werden.

Wir hatten die Wahl zwischen mehreren Optionen. Von diesen haben wir besonders sorgfältig überlegt:

  • Implementierung von Service Discovery- Protokollen für jede der Komponenten.
  • Verwenden eines Mediators zum Umleiten von Clientanforderungen an die richtige Plattformkomponente.
  • Verwenden eines Nachrichtenbrokers als Nachrichtenbus.

Aufgrund einiger Überlegungen und Experimente fiel die Wahl auf den Nachrichtenbroker, obwohl er uns als potenziellen Single Point of Failure ansah und den Aufwand für den Betrieb der Plattform erhöhte. Eine wichtige Rolle bei der Auswahl spielte die Tatsache, dass die Plattform zu diesem Zeitpunkt bereits über Erfahrung in der Arbeit mit RabbitMQ verfügte. Der Broker selbst skalierte gut und verfügte über integrierte Mechanismen zur Gewährleistung der Fehlertoleranz. Als Bonus hatten wir die Möglichkeit, eine ereignisgesteuerte Architektur ( ereignisgesteuerte Architektur oder EDA ) „unter der Haube“ zu implementieren. Dies eröffnete uns später weitere Möglichkeiten der dienststellenübergreifenden Interaktion im Vergleich zur Punkt-zu-Punkt-Interaktion.

Topologisch begann sich die Plattform von einem Graphen mit zufälliger Konnektivität in einen Stern zu verwandeln. Und Plattformkomponenten haben ihre Abhängigkeiten umgekehrt und die Möglichkeit erhalten, ausschließlich über Verträge, die in einem zentralen Repository registriert sind, miteinander zu interagieren, ohne wissen zu müssen, wer einen bestimmten Vertrag speziell implementiert. Mit anderen Worten, alle Komponenten innerhalb der Plattform konnten über einen einzigen Integrationspunkt miteinander interagieren, was das Leben der Entwickler erheblich vereinfachte.

Dokumentation auf dem neuesten Stand halten


Probleme, die mit dem Mangel an Dokumentation oder dem Verlust ihrer Relevanz verbunden sind, treten fast immer auf. Und je höher das Entwicklungstempo, desto häufiger manifestiert es sich. Im Nachhinein ist es eine schwierige Aufgabe, alle API-Spezifikationen an einem einzigen Ort und in einem einzigen Format für mehr als hundert Services in einem verteilten und multinationalen Team zu sammeln.

Bei der Entwicklung der Vertrags-API haben wir uns zum Ziel gesetzt, auch dieses Problem zu lösen. Und wir haben es geschafft. Ein streng definiertes Format für die Beschreibung des Vertrags ermöglichte es uns, einen Prozess zu erstellen, nach dem unmittelbar nach dem Erscheinen eines neuen Vertrags die automatische Zusammenstellung der Dokumentation gestartet wird. Dies gibt uns die Gewissheit, dass unsere API-Dokumentation immer auf dem neuesten Stand ist. Dieser Prozess ist vollständig automatisiert und erfordert keinen Entwicklungs- oder Verwaltungsaufwand.

Introspektion und Debugging von End-to-End-Funktionen


Als wir unsere Monolithen in kleinere Komponenten aufteilten, traten natürlich Schwierigkeiten beim Debuggen der End-to-End-Funktionalität auf. Wenn der Service einer Geschäftsfunktion auf mehrere Plattformkomponenten verteilt war, musste häufig nach Vertretern der einzelnen Komponenten gesucht werden, um das Problem zu lokalisieren und zu debuggen. Was manchmal schwierig zu erreichen war, angesichts des Zeitunterschieds von 11 Stunden bei einigen unserer Kollegen.

Mit dem Aufkommen der Vertrags-API und insbesondere dank des zugrunde liegenden Nachrichtenbrokers erhielten wir die Möglichkeit, Kopien von Nachrichten zu erhalten, die an der Ausführung einer Geschäftsfunktion beteiligt sind, ohne Nebenwirkungen auf die Interaktionsteilnehmer. Dazu muss nicht einmal bekannt sein, welche der Komponenten der Plattform für die Bearbeitung eines bestimmten Vertrags verantwortlich sind. Und nach der Lokalisierung des Problems können wir die Kennung der defekten Komponente aus den Metadaten der Problemmeldung abrufen.

Was haben wir noch zusätzlich zur Vertrags-API entwickelt?


Zusätzlich zu ihrem Hauptzweck und der Lösung der oben genannten Probleme ermöglichte uns die Vertrags-API die Implementierung einer Reihe nützlicher Dienste.

Gateway für den Zugriff auf Plattformfunktionen


Die Standardisierung der API in Form von Verträgen ermöglichte es uns, einen einzigen Zugangspunkt zur Plattformfunktionalität über HTTP zu entwickeln. Darüber hinaus müssen wir mit dem Aufkommen neuer Funktionen (Verträge) diesen Zugangspunkt in keiner Weise ändern. Es ist vorwärtskompatibel mit allen zukünftigen Verträgen. Auf diese Weise können Sie mit der Plattform als einzelnes Produkt über die übliche HTTP-Schnittstelle arbeiten.

Mass Operations Service


Jeder Vertrag kann als Teil einer Massenoperation gestartet werden, wobei der Status verfolgt und anschließend ein Bericht über die Ergebnisse dieser Operation erhalten werden kann. Dieser Service ist wie der vorherige mit allen zukünftigen Verträgen im Voraus kompatibel.

Einheitliche Plattformfehlerbehandlung


Das Vertrags-API-Protokoll standardisiert auch Fehler. Auf diese Weise konnten wir einen Fehlerabfangjäger implementieren, der deren Schweregrad analysiert und das Überwachungssystem über mögliche Probleme bei Plattformkomponenten informiert. Und in Zukunft kann er unabhängig über die Entdeckung eines Fehlers in der Plattformkomponente entscheiden. Der Fehlerabfangjäger fängt sie direkt vom Nachrichtenbroker ab und weiß nichts über den Zweck eines Vertrags oder eines Fehlers, da er nur auf der Grundlage von Metainformationen handelt. Dies ermöglicht es ihm sowie allen in diesem Abschnitt beschriebenen Dienstleistungen, mit allen zukünftigen Verträgen vorwärtskompatibel zu sein.

Benutzeroberflächen automatisch generieren


Streng formalisierte Verträge ermöglichen es Ihnen, automatisch Komponenten der Benutzeroberfläche zu erstellen. Wir haben einen Service entwickelt, mit dem Sie eine Verwaltungsschnittstelle basierend auf einer Sammlung von Verträgen erstellen und diese Schnittstelle dann in eines unserer Plattformtools einbetten können. Somit können die Administratoren, die wir zuvor mit unseren Händen geschrieben haben, jetzt (wenn auch nur teilweise) im automatischen Modus generiert werden.

Plattformprotokollierung


Diese Komponente wurde noch nicht implementiert und befindet sich in der Entwicklung. In Zukunft wird es jedoch möglich sein, die Protokollierung aller Geschäftsfunktionen auf der Plattform im laufenden Betrieb ein- und auszuschalten und diese Informationen direkt aus dem Nachrichtenbroker zu extrahieren, ohne dass Nebenwirkungen auftreten, die sich negativ auf die interagierenden Komponenten auswirken.

Der Hauptzweck der Vertrags-API


Der Hauptzweck der Vertrags-API besteht jedoch darin, die Kosten für die Integration von Plattformkomponenten zu senken.

Entwickler werden von Bibliotheken, die wir für jeden unserer Technologie-Stacks entwickelt haben, von der Transportebene abstrahiert. Dies gibt uns einen gewissen Handlungsspielraum für den Fall, dass wir den Nachrichtenbroker ändern oder sogar zur Punkt-zu-Punkt-Interaktion wechseln müssen. Die externe Schnittstelle der Bibliothek bleibt unverändert.

Die Bibliothek unter der Haube generiert eine Nachricht nach bestimmten Regeln und sendet sie an den Broker. Nach dem Warten auf eine Antwortnachricht gibt sie das Ergebnis an den Entwickler zurück. Draußen sieht es aus wie eine reguläre synchrone (oder asynchrone, implementierungsabhängige) Anforderung. Als Demonstration werde ich einige Beispiele geben.

Beispiel für einen Python-Vertragsaufruf
from platform_client import Client client = Client(contracts_path=CONTRACTS_PATH, url=AMQP_URL, app_id='client') client.call("ban-management.create-ban.v1", { "wgid": 1234567890, "reason": "Fraudulent activity", "title": "ru.wot", "component": "game", "bantype": "access_denied", "author_id": "v_nikonovich", "expires_at": "2038-01-19 03:14:07Z" }) { u'ban_id': 31415926, u'wgid': 1234567890, u'title': u'ru.wot', u'component': u'game', u'reason': u'Fraudulent activity', u'bantype': u'access_denied', u'status': u"active", u'started_at': u"2019-02-15T15:15:15Z", u'expires_at': u"2038-01-19 03:14:07Z" } 

Der gleiche Vertragsanruf, aber mit Elixir
 :platform_client.call("ban-management.create-ban.v1", %{ "wgid" => 1234567890, "reason" => "Fraudulent activity", "title" => "ru.wot", "component" => "game", "bantype" => "access_denied", "author_id" => "v_nikonovich", "expires_at" => "2038-01-19 03:14:07Z" }) {:ok, %{ "ban_id" => 31415926, "wgid" => 1234567890, "title" => "ru.wot", "conponent" => "game", "reason" => "Fraudulent activity", "bantype" => "access_denied", "status" => "active", "started_at" => "2019-02-15T15:15:15Z", "expires_at" => "2038-01-19 03:14:07Z" }} 

Anstelle des Vertrags "ban-management.create-ban.v1" kann es auch andere Plattformfunktionen geben, zum Beispiel: "account-management.rename-account.v1" oder "notification-center.create-sms-notification.v1". Und all dies wird durch diesen einzigen Integrationspunkt in die Plattform verfügbar sein.

Die Übersicht ist unvollständig, wenn Sie die Vertrags-API aus Sicht des Serverentwicklers nicht demonstrieren. Stellen Sie sich eine Situation vor, in der ein Entwickler einen Handler für denselben Vertrag von ban-management.create-ban.v1 implementieren muss.

 from platform_server import BlockingServer, handler class CustomServer(BlockingServer): @handler('ban-management.create-ban.v1') def handle_create_ban(self, params, context): response = do_some_usefull_job(params) return response d = CustomServer(app_id="server", amqp_url=AMQP_URL, contracts_path=CONTRACTS_PATH) d.serve() 

Dieser Code reicht aus, um einen bestimmten Vertrag zu erfüllen. Die Serverbibliothek entpackt und überprüft die Anforderungsparameter auf Richtigkeit und ruft dann den Vertragshandler mit den zur Verarbeitung bereitgestellten Anforderungsparametern auf. Somit ist der Serverentwickler durch eine Bibliothek geschützt, die bei Empfang falscher Anforderungsparameter selbst einen Validierungsfehler an den Client sendet und die Tatsache eines Problems registriert.

Aufgrund der Tatsache, dass die Vertrags-API unter der Haube auf der Grundlage von Ereignissen implementiert wird, haben wir die Möglichkeit, über den Umfang des Anforderungs- / Antwortskripts hinauszugehen und ein breiteres Spektrum dienstübergreifender Interaktionen zu implementieren.

Zum Beispiel:

  • eine Anfrage stellen und vergessen (ohne auf eine Antwort zu warten)
  • Anfragen an mehrere Verträge gleichzeitig stellen (auch ohne Verwendung einer Ereignisschleife)
  • Stellen Sie eine Anfrage und erhalten Sie Antworten von mehreren Handlern gleichzeitig (sofern im Integrationsskript vorgesehen).
  • Registrieren eines Antwort-Handlers (ausgelöst, wenn der Vertrags-Handler den Abschluss gemeldet hat, das Ergebnis der Arbeit des Vertrags-Handlers akzeptiert, dh seine Antwort)

Und dies ist keine vollständige Liste von Szenarien, die durch ein Ereignismodell der Interaktion ausgedrückt werden können. Dies ist eine Liste derjenigen, die wir derzeit verwenden.

Anstelle einer Schlussfolgerung


Wir verwenden die Vertrags-API seit mehreren Jahren. Daher ist es nicht möglich, im Rahmen eines Übersichtsartikels über alle Szenarien seiner Verwendung zu sprechen. Aus dem gleichen Grund habe ich den Artikel nicht mit technischen Details überladen. Sie war schon ziemlich voluminös. Stellen Sie Fragen, und ich werde versuchen, sie direkt in den Kommentaren zu beantworten. Wenn ein Thema besonders interessant ist, kann es in einem separaten Artikel ausführlicher behandelt werden.

Source: https://habr.com/ru/post/de441708/


All Articles