
Avec l'augmentation du nombre de composants d'un système logiciel, le nombre de personnes participant à son développement augmente généralement également. En conséquence, afin de maintenir le rythme de développement et la facilité de maintenance, les approches de l'organisation de l'API devraient faire l'objet d'une attention particulière.
Si vous voulez voir de plus près comment l'équipe de Wargaming Platform fait face à la complexité d'un système de plus d'une centaine de services Web interagissant les uns avec les autres, alors bienvenue chez cat.
Bonjour à tous! Je m'appelle Valentine et je suis ingénieur sur la plateforme de Wargaming. Pour ceux qui ne savent pas ce qu'est la plateforme et ce qu'elle fait, je vais laisser ici un
lien vers la publication récente d'un de mes collègues -
max_posedonEn ce moment, je travaille dans l'entreprise depuis plus de cinq ans et j'ai trouvé en partie la période de croissance active de World of Tanks. Pour découvrir les problèmes soulevés dans cet article, je dois commencer par une brève digression dans l'histoire de la plateforme Wargaming.
Un peu d'histoire
La popularité croissante des «tanks» s'est avérée être semblable à une avalanche, et comme c'est généralement le cas dans de tels cas, l'infrastructure autour du jeu a commencé à se développer rapidement. En conséquence, le jeu a rapidement envahi divers services Web et, au moment où je me suis joint à l'équipe, leur score atteignait déjà des dizaines (maintenant, en passant, plus de 100 composants de plate-forme fonctionnent et profitent à l'entreprise).
Au fil du temps, de nouveaux jeux sont sortis et il n'était plus facile de comprendre les subtilités des intégrations entre les services Web. La situation n'a fait qu'empirer lorsque des équipes d'autres bureaux de Wargaming ont rejoint le développement de la plateforme. Le développement est devenu distribué, avec toutes les conséquences sous forme de distance, de fuseaux horaires et de barrière linguistique. Et il y a plus de services. Trouver une personne qui comprend le fonctionnement de la plateforme dans son ensemble n'est pas si facile. Les informations devaient souvent être collectées en plusieurs parties auprès de différentes sources.
Les interfaces des différents services Web pouvaient être très différentes dans la conception stylistique, ce qui rendait le processus d'intégration avec la plate-forme encore plus difficile. Et les dépendances directes entre composants ont réduit la flexibilité de développement en compliquant la décomposition des fonctionnalités au sein de la plate-forme. Pour aggraver les choses, les jeux - clients de la plateforme - connaissaient bien notre topologie, car ils devaient s'intégrer directement à chaque service de plateforme. Cela leur a donné l'occasion, en utilisant des connexions horizontales, de faire pression pour la mise en œuvre de certaines améliorations directement dans le composant avec lequel elles sont intégrées. Cela a conduit à l'apparition de fonctionnalités en double dans divers composants de la plate-forme, ainsi qu'à l'incapacité d'étendre les fonctionnalités existantes à d'autres jeux. Il est devenu évident que continuer à construire une plate-forme autour de chaque jeu spécifique est une branche de développement sans issue. Nous avions besoin de changements techniques et organisationnels, grâce auxquels nous pouvions prendre le contrôle de la complexité croissante d'un système en croissance rapide et rendre toutes les fonctionnalités de la plate-forme adaptées à n'importe quel jeu.
Avec cela, je veux terminer l'excursion historique et, enfin, parler de l'une de nos solutions techniques, qui aide à garder le contrôle de la complexité causée par le nombre toujours croissant de services. De plus, il réduit le coût de développement de nouvelles fonctionnalités et simplifie considérablement l'intégration avec la plate-forme.
Découvrez l'API Contract
À l'intérieur de la plateforme, nous l'appelons l'API Contract. À la base, c'est un cadre d'intégration représenté par un ensemble de documentation et de bibliothèques clientes pour chaque technologie de notre pile (Erlang / Elixir, Java / Scala, Python). Il est développé, tout d'abord, afin de simplifier l'intégration des composants de la plateforme entre eux. Deuxièmement, pour nous aider à résoudre un certain nombre des problèmes suivants:
- différences stylistiques des interfaces de programme
- la présence de dépendances inter-composants directes
- tenir à jour la documentation
- introspection et débogage des fonctionnalités de bout en bout
Alors, tout d'abord.
Différences stylistiques dans les interfaces logicielles
À mon avis, ce problème est survenu à la suite d'une combinaison de plusieurs facteurs:
- Absence d'un standard strict de ce à quoi devrait ressembler l'API. L'ensemble de recommandations n'a souvent pas l'effet souhaité, l'API est toujours différente. Surtout si le développement est réalisé par des équipes de différents bureaux de l'entreprise. Chaque équipe a ses propres habitudes et pratiques. Collectivement, ces API ne ressemblent souvent pas à des parties d'un tout.
- Absence d'un répertoire unique avec les noms et les formats d'entités spécifiques à l'entreprise. En règle générale, vous ne pouvez pas prendre une entité du résultat d'une API et la transmettre à l'API d'un autre service. Cela nécessite une transformation.
- Absence d'un système d'examen centralisé obligatoire pour l'API. Il y a toujours des délais et il n'y a pas de temps pour collecter les mises à jour et, par ailleurs, apporter des modifications à l'API, qui s'avère en fait souvent déjà à moitié testée.
La première chose que nous avons faite lors de la conception de l'API Contract a été de dire que désormais l'API appartient à la plateforme, et non à un seul composant. Cela a conduit au fait que le développement de nouvelles fonctionnalités commence par une requête pull vers une API de stockage centralisée. Actuellement, nous utilisons le référentiel GIT comme stockage. Pour plus de commodité, nous avons divisé l'ensemble de l'API en fonctions métier distinctes, formalisé la structure de cette fonction et l'avons appelée Contrat.
Depuis lors, chaque nouvelle fonction métier dans notre API de contrat doit être décrite dans un format spécial et passer par la demande de tirage avec un examen obligatoire. Il n'y a pas d'autre moyen de publier une nouvelle API dans l'API Contract. Dans le même référentiel, nous avons défini un répertoire d'entités spécifiques à l'entreprise et suggéré aux développeurs de contrats de les réutiliser au lieu de décrire ces entités elles-mêmes.
Nous avons donc obtenu une API conceptuellement holistique de la plate-forme, qui ressemblait à un produit unique, malgré le fait qu'elle ait été mise en œuvre sur de nombreux composants de la plate-forme à l'aide de différentes piles technologiques.
La présence de dépendances inter-composants directes
Le problème que nous avons rencontré s'est manifesté par le fait que chaque composant de la plate-forme devait savoir qui assurait spécifiquement le service dont il avait besoin.
Et ce n'était même pas la difficulté de maintenir ce répertoire à jour, mais le fait que les dépendances directes ont considérablement compliqué la migration des fonctionnalités métier d'un composant de plateforme à un autre. Le problème était particulièrement aigu lorsque nous avons commencé la décomposition de nos monolithes en composants plus petits. Il s'est avéré que convaincre le client de remplacer l'intégration de travail par n'importe quelle fonctionnalité par la même du point de vue de l'entreprise, mais une autre du point de vue technique, n'est pas une tâche de gestion triviale. Le client n'y voit tout simplement pas l'intérêt, car tout fonctionne bien pour lui. En conséquence, des couches de compatibilité ascendante à l'odeur désagréable ont été écrites, ce qui n'a fait que compliquer le support de la plate-forme et a eu un effet néfaste sur la qualité du service. Et comme nous allions déjà standardiser l'API de la plateforme, il était nécessaire de résoudre simultanément ce problème.
Nous avons été confrontés à un choix de plusieurs options. Parmi ceux-ci, nous avons particulièrement étudié attentivement:
- Implémentation de protocoles de découverte de service sur chacun des composants.
- Utilisation d'un médiateur pour rediriger les demandes des clients vers le composant de plateforme approprié.
- Utilisation d'un courtier de messages comme bus de messagerie.
À la suite de certaines réflexions et expériences, le choix s'est porté sur le courtier de messages, malgré le fait qu'il nous voyait comme un seul point de défaillance potentiel et augmentait les frais généraux d'exploitation de la plate-forme. Un rôle important dans la sélection a été joué par le fait qu'à l'époque, la plateforme avait déjà une expertise dans le travail avec RabbitMQ. Et le courtier lui-même a bien évolué et avait des mécanismes intégrés pour assurer la tolérance aux pannes. En prime, nous avons eu l'opportunité de mettre en œuvre une
architecture événementielle (
architecture événementielle ou
EDA ) «sous le capot». Ce qui a par la suite ouvert devant nous des possibilités plus larges d'interaction interservices, par rapport à l'interaction point à point.
Donc, topologiquement, la plate-forme a commencé à passer d'un graphique à connectivité aléatoire à une étoile. Et les composants de la plate-forme ont inversé leurs dépendances et ont eu la possibilité d'interagir les uns avec les autres exclusivement via des contrats enregistrés dans un référentiel centralisé, sans avoir besoin de savoir qui implémente spécifiquement un contrat particulier. En d'autres termes, tous les composants de la plateforme ont pu interagir les uns avec les autres à l'aide d'un point d'intégration unique, ce qui a grandement simplifié la vie des développeurs.
Tenir la documentation à jour
Les problèmes liés au manque de documentation ou à la perte de sa pertinence sont presque toujours rencontrés. Et plus le rythme de développement est élevé, plus il se manifeste souvent. Et après coup, la collecte de toutes les spécifications de l'API en un seul endroit et format pour plus d'une centaine de services dans une équipe distribuée et multinationale est une tâche difficile.
Lors du développement de l'API Contract, nous nous sommes également fixé l'objectif de résoudre ce problème. Et nous l'avons fait. Un format strictement défini pour la description du contrat nous a permis de construire un processus selon lequel, immédiatement après l'apparition d'un nouveau contrat, l'assemblage automatique de la documentation est démarré. Cela nous donne confiance que notre documentation API est toujours à jour. Ce processus est entièrement automatisé et ne nécessite aucun effort de développement ou de gestion.
Introspection et débogage de la fonctionnalité de bout en bout
Lorsque nous avons divisé nos monolithes en composants plus petits, tout naturellement, des difficultés ont commencé à survenir lors du débogage des fonctionnalités de bout en bout. Si le service d'une fonction métier était réparti sur plusieurs composants de plate-forme, alors souvent pour localiser et déboguer le problème, il fallait chercher des représentants de chacun des composants. Ce qui était parfois réalisable avec difficulté, étant donné le décalage horaire de 11 heures avec certains de nos collègues.
Avec l'avènement de l'API Contract, et en particulier grâce au courtier de messages qui la sous-tend, nous avons eu la possibilité de recevoir des copies des messages impliqués dans l'exécution d'une fonction métier, sans effets secondaires sur les participants à l'interaction. Pour ce faire, il n'est même pas nécessaire de savoir lequel des composants de la plateforme est responsable du traitement d'un contrat particulier. Et après la localisation du problème, nous pouvons obtenir l'identifiant du composant cassé à partir des métadonnées du message de problème.
Qu'avons-nous développé d'autre que l'API Contract
En plus de son objectif principal et de la résolution des problèmes ci-dessus, l'API Contract nous a permis de mettre en œuvre un certain nombre de services utiles.
Passerelle pour accéder aux fonctionnalités de la plateforme
La standardisation de l'API sous forme de contrats nous a permis de développer un point d'accès unique aux fonctionnalités de la plateforme via HTTP. De plus, avec l'avènement de nouvelles fonctionnalités (contrats), nous n'avons plus besoin de modifier ce point d'accès. Il est compatible avec tous les futurs contrats. Cela vous permet de travailler avec la plate-forme en tant que produit unique en utilisant l'interface HTTP habituelle.
Service des opérations de masse
Tout contrat peut être lancé dans le cadre d'une opération de masse, avec la possibilité de suivre son statut et de recevoir ensuite un rapport sur les résultats de cette opération. Ce service, tout comme le précédent, est compatible avec tous les futurs contrats à l'avance.
Gestion des erreurs de plate-forme unifiée
Le protocole Contract API standardise également les erreurs. Cela nous a permis de mettre en œuvre un intercepteur d'erreurs, qui analyse leur gravité et informe le système de surveillance des problèmes potentiels sur les composants de la plate-forme. Et à l'avenir, il pourra décider indépendamment de la découverte d'un bug sur le composant de la plateforme. L'intercepteur d'erreurs les attrape directement auprès du courtier de messages et ne sait rien de l'objet d'un contrat ou d'une erreur, agissant uniquement sur la base de méta-informations. Cela lui permet, ainsi que tous les services décrits dans cette section, d'être compatible avec tous les futurs contrats.
Générer automatiquement des interfaces utilisateur
Des contrats strictement formalisés vous permettent de créer automatiquement des composants d'interface utilisateur. Nous avons développé un service qui vous permet de générer une interface administrative basée sur une collection de contrats, puis d'intégrer cette interface dans l'un de nos outils de plateforme. Ainsi, les administrateurs que nous avons précédemment écrits avec nos mains peuvent maintenant être générés (bien que partiellement jusqu'à présent) en mode automatique.
Journalisation de la plateforme
Cette composante n'est pas encore implémentée et est en cours de développement. Mais à l'avenir, cela permettra «à la volée» d'activer et de désactiver la journalisation de toute fonction commerciale de la plate-forme, en extrayant ces informations directement du courtier de messages, sans aucun effet secondaire qui affecte négativement les composants en interaction.
L'objectif principal de l'API Contract
Mais le but principal de l'API Contract est de réduire le coût d'intégration des composants de la plateforme.
Les développeurs sont extraits du niveau de transport par les bibliothèques que nous avons développées pour chacune de nos piles technologiques. Cela nous donne une certaine marge de manœuvre au cas où nous devions changer le courtier de messages ou même passer à une interaction point à point. L'interface externe de la bibliothèque restera inchangée.
La bibliothèque sous le capot génère un message selon certaines règles et l'envoie au courtier, après quoi, après avoir attendu un message de réponse, il renvoie le résultat au développeur. À l'extérieur, cela ressemble à une demande synchrone régulière (ou asynchrone, dépendante de l'implémentation). En guise de démonstration, je vais donner quelques exemples.
Exemple d'appel de contrat Python
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" }
Le même appel de contrat, mais en utilisant 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" }}
Au lieu du contrat «ban-management.create-ban.v1», il peut y avoir toute autre fonctionnalité de plate-forme, par exemple: «account-management.rename-account.v1» ou «notification-center.create-sms-notification.v1». Et tout cela sera disponible via ce point d'intégration unique avec la plateforme.
L'aperçu sera incomplet si vous ne démontrez pas l'API Contract du point de vue du développeur du serveur. Considérez une situation dans laquelle un développeur doit implémenter un gestionnaire pour le même contrat ban-management.create-ban.v1.
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()
Ce code sera suffisant pour commencer à servir un contrat donné. La bibliothèque du serveur décompresse et vérifie l'exactitude des paramètres de demande, puis appelle le gestionnaire de contrat avec les paramètres de demande prêts pour le traitement. Ainsi, le développeur du serveur est protégé par une bibliothèque qui, en cas de réception de paramètres de requête incorrects, enverra elle-même une erreur de validation au client et enregistrera le fait d'un problème.
Étant donné que sous le capot, l'API Contract est implémentée sur la base d'événements, nous avons la possibilité d'aller au-delà de la portée du script de demande / réponse et de mettre en œuvre un éventail plus large d'interactions interservices.
Par exemple:
- faire une demande et oublier (sans attendre de réponse)
- faire des demandes à plusieurs contrats simultanément (même sans utiliser de boucle d'événement)
- faire une demande et recevoir des réponses de plusieurs gestionnaires à la fois (si prévu par le script d'intégration)
- enregistrer un gestionnaire de réponse (déclenché si le gestionnaire de contrat a signalé l'achèvement, accepte le résultat du travail du gestionnaire de contrat, c'est-à-dire sa réponse)
Et ce n'est pas une liste complète de scénarios qui peuvent être exprimés à travers un modèle d'événement d'interaction. Ceci est une liste de ceux que nous utilisons actuellement.
Au lieu d'une conclusion
Nous utilisons l'API Contract depuis plusieurs années. Par conséquent, il n'est pas possible de parler de tous les scénarios de son utilisation dans le cadre d'un article de synthèse. Pour la même raison, je n'ai pas surchargé l'article de détails techniques. Elle s'est déjà avérée assez volumineuse. Posez des questions et j'essaierai d'y répondre directement dans les commentaires. Si un sujet est particulièrement intéressant, il sera possible de le divulguer plus en détail dans un article séparé.