Salut Habr! RBKmoney prend à nouveau contact et poursuit une série d'articles sur la façon d'écrire le traitement des paiements par soi-même.

Je voulais plonger immédiatement dans les détails de la description de la mise en œuvre d'un processus commercial de paiement en tant que machine d'état, montrer des exemples d'une telle machine avec un ensemble d'événements, des fonctionnalités de mise en œuvre ... Mais il semble que vous ne pouvez pas vous passer de quelques articles de revue supplémentaires. Le sujet s'est avéré trop grand. Cet article révélera les nuances de travail et l'interaction entre les microservices de notre plateforme, l'interaction avec les systèmes externes et la façon dont nous gérons la configuration de l'entreprise.
Service de macro
Notre système se compose de nombreux microservices qui, implémentant chacun de leur partie finie de la logique métier, interagissent entre eux et forment ensemble un macro service. En fait, le macro-service déployé dans le centre de données, connecté aux banques et autres systèmes de paiement, est notre traitement des paiements.
Modèle de microservice
Nous utilisons une approche unifiée pour le développement de tout microservice dans la langue dans laquelle il est écrit. Chaque microservice est un conteneur Docker qui contient:
- l'application elle-même qui implémente la logique métier écrite en Erlang ou Java;
- RPClib - une bibliothèque qui implémente la communication entre les microservices;
- nous utilisons Apache Thrift, ses principaux avantages sont des bibliothèques client-serveur prêtes à l'emploi et la possibilité de typifier strictement la description de toutes les méthodes publiques fournies par chaque microservice;
- la deuxième caractéristique de la bibliothèque est notre implémentation de Google Dapper , qui nous permet de tracer rapidement les demandes avec une simple recherche dans Elasticsearch. Le premier microservice qui a reçu une demande d'un système externe génère un
trace_id
unique, qui est enregistré par chaque chaîne de demande suivante. De plus, nous générons et enregistrons parent_id
et span_id
, ce qui vous permet de construire un arbre de requête, en surveillant visuellement toute la chaîne de microservices impliqués dans le traitement de la demande; - la troisième caractéristique - nous utilisons activement le transfert au niveau du transport de différentes informations sur le contexte de la demande. Par exemple, les délais (la durée de vie attendue de la demande définie sur le client), ou pour le compte de qui nous appelons une méthode;
- Le modèle Consul est un agent de découverte de service qui conserve des informations sur l'emplacement, la disponibilité et l'état d'un microservice. Les microservices se trouvent par noms DNS, la zone TTL est nulle, le service qui est mort ou qui n'a pas passé le contrôle de santé cesse de se résoudre et reçoit ainsi du trafic;
- les journaux que l'application écrit dans un format compréhensible par Elasticsearch dans le fichier conteneur local et le
filebeat
fichier, qui s'exécute sur la machine hôte par rapport au conteneur, récupère ces journaux et les envoie au cluster Elasticsearch;
- puisque nous implémentons la plateforme selon le modèle Event Sourcing, les chaînes de journaux résultantes sont également utilisées pour la visualisation sous la forme de différents tableaux de bord Grafana, ce qui réduit le temps de mise en œuvre de différentes métriques (nous utilisons également des métriques distinctes).

Lors du développement de microservices, nous utilisons les limitations que nous avons spécialement inventées, qui sont conçues pour résoudre à la fois le problème de la haute disponibilité de la plateforme et de sa tolérance aux pannes:
- limites de mémoire strictes pour chaque conteneur, lorsque vous allez au-delà des limites - MOO, la plupart des microservices vivent dans 256-512M. Cela rend la mise en œuvre de la logique métier plus finement fragmentée, protège contre la dérive vers le monolithe, réduit le coût du point de défaillance, offre un avantage supplémentaire pour travailler sur du matériel bon marché (la plate-forme est déployée et fonctionne sur des serveurs monoprocesseurs peu coûteux);
- aussi peu de microservices avec état que possible et autant d'implémentations sans état que possible. Cela nous permet de résoudre les problèmes de tolérance aux pannes, de rapidité de récupération et, en général, de minimiser les lieux à comportement potentiellement incompréhensible. Cela devient particulièrement important avec une augmentation de la durée de vie du système lorsqu'un important héritage est accumulé;
- laissez-le tomber en panne et les approches "ça va définitivement casser". Nous savons que n'importe quelle partie de notre système échouera nécessairement, nous le concevons de telle sorte que cela n'affecte pas l'exactitude générale des informations accumulées dans la plate-forme. Aide à minimiser le nombre d'états non définis dans le système.
Sûrement familier à beaucoup de ceux qui s'intègrent à des tiers, la situation. Nous nous attendions à une réponse d'un tiers à la demande d'annuler de l'argent conformément au protocole, et une réponse complètement différente est arrivée, qui n'est décrite dans aucune spécification, et on ne sait pas comment l'interpréter.
Dans cette situation, nous tuons la machine d'état servant à ce paiement, toutes les actions de l'extérieur recevront une erreur de 500. Et à l'intérieur, nous découvrons l'état actuel du paiement, alignons l'état de la machine avec la réalité et ravivons la machine d'état.
Développement orienté protocole

Au moment de la rédaction du présent rapport, 636 chèques différents étaient enregistrés dans notre Service Discovery pour des services qui garantissent le fonctionnement de la plateforme. Même en tenant compte du fait que plusieurs vérifications sont effectuées sur un service, et aussi que la plupart des services sans état fonctionnent dans au moins une triple instance, vous obtenez toujours cinquante applications qui doivent pouvoir se connecter d'une manière ou d'une autre et ne pas échouer. dans l'enfer RPC.
La situation est compliquée par le fait que nous avons trois langages de développement sur la pile - Erlang, Java, JS, et ils doivent tous pouvoir communiquer de manière transparente entre eux.
La première tâche qui devait être résolue était de concevoir la bonne architecture pour l'échange de données entre les microservices. Comme base, nous avons pris Apache Thrift. Tous les microservices échangent des binaires trift; nous utilisons HTTP comme transport.
Nous plaçons les spécifications de police sous forme de référentiels séparés dans notre github, afin qu'elles soient disponibles pour tout développeur qui y a accès. Initialement, ils ont utilisé un référentiel commun pour tous les protocoles, mais au fil du temps, ils sont arrivés à la conclusion que cela n'était pas pratique - le travail parallèle conjoint sur les protocoles s'est transformé en un mal de tête constant. Différentes équipes et même différents développeurs ont été obligés de s'entendre sur le nom des variables, une tentative de division en espace de noms n'a pas non plus aidé.
En général, nous pouvons dire que nous avons un développement basé sur le protocole. Avant de commencer toute mise en œuvre, nous développons le futur protocole de microservice sous la forme d'une spécification d'ascenseur, parcourons 7 cercles de révision, attirant les futurs clients de ce microservice, et avons l'opportunité de commencer simultanément à développer plusieurs microservices en parallèle, car nous connaissons toutes ses futures méthodes et nous pouvons déjà écrire leurs gestionnaires, en utilisant éventuellement moki.
Une étape distincte dans le processus de développement du protocole est une revue de sécurité, où les gars regardent de leur point de vue pentester les nuances de la spécification en cours d'élaboration.
Nous avons également jugé approprié de souligner un rôle distinct du propriétaire du protocole dans l'équipe. La tâche est difficile, une personne doit garder à l'esprit les spécificités de tous les microservices, mais elle est payante dans un ordre important et la présence d'un seul point d'escalade.
Sans l'approbation finale de la demande d'extraction par ces employés, le protocole ne peut pas être fusionné dans une branche principale. Il y a une fonctionnalité très pratique dans le github pour cela - les propriétaires de code , nous l'utilisons avec plaisir.
Ainsi, nous avons résolu le problème de la communication entre les microservices, les problèmes possibles de malentendu sur le type de microservice apparu dans la plate-forme et pourquoi il était nécessaire. Cet ensemble de protocoles est peut-être la seule partie de la plate-forme où nous choisissons inconditionnellement la qualité par rapport au coût et à la vitesse de développement, car la mise en œuvre d'un microservice peut être réécrite relativement sans douleur, et le protocole sur lequel plusieurs dizaines sont déjà chers et douloureux.
En cours de route, une journalisation précise permet de résoudre le problème de la documentation. Des noms de méthodes et de paramètres raisonnablement choisis, quelques commentaires et une spécification auto-documentée font gagner beaucoup de temps!
Par exemple, voici à quoi ressemble la spécification de la méthode de l'un de nos microservices, vous permettant d'obtenir une liste des événements qui se sont produits sur la plateforme:
/** */ typedef i64 EventID /* Event sink service definitions */ service EventSink { /** * , * , , `range`. * `0` `range.limit` . * * `range.after` , * , , * `EventNotFound`. */ Events GetEvents (1: EventRange range) throws (1: EventNotFound ex1, 2: base.InvalidRequest ex2) /** * * . */ base.EventID GetLastEventID () throws (1: NoLastEvent ex1) } /* Events */ typedef list<Event> Events /** * , -, . */ struct Event { /** * . * , * (total order). */ 1: required base.EventID id /** * . */ 2: required base.Timestamp created_at /** * -, . */ 3: required EventSource source /** * , ( ) * -, . */ 4: required EventPayload payload /** * . * . */ 5: optional base.SequenceID sequence } // Exceptions exception EventNotFound {} exception NoLastEvent {} /** * , - */ exception InvalidRequest { /** */ 1: required list<string> errors }
Client de console Thrift
Parfois, nous sommes confrontés à la tâche d'appeler certaines méthodes du microservice nécessaire directement, par exemple, à la main depuis le terminal. Cela peut être utile pour le débogage, l'obtention d'un ensemble de données brutes ou lorsque la tâche est si rare que le développement d'une interface utilisateur distincte n'est pas pratique.
Par conséquent, nous avons développé un outil pour nous-mêmes qui combine les fonctions de curl
, mais vous permet de faire des requêtes trift sous la forme de structures JSON. Nous l'avons appelé en conséquence - woorl
. L'utilitaire est universel, il suffit de lui transférer l'emplacement de toute spécification de levage à l'aide du paramètre de ligne de commande, il fera le reste lui-même. Un utilitaire très pratique, vous pouvez lancer un paiement directement depuis le terminal, par exemple.
Voici à quoi ressemble un appel directement au microservice de la plateforme, qui est responsable de la gestion des applications (par exemple, pour créer un magasin). J'ai demandé des données sur mon compte test:

Les lecteurs observateurs ont probablement remarqué une caractéristique de la capture d'écran. Nous n'aimons pas ça non plus. Il est nécessaire de sécuriser l'autorisation d'appels trift entre microservices, il faut coller TLS dans le bon sens. Mais alors que les ressources, comme toujours, ne suffisent pas. Nous nous sommes limités à l'enceinte totale du périmètre dans lequel vivent les microservices de traitement.
Protocoles pour communiquer avec des systèmes externes
Pour publier des spécifications d'ascenseur vers l'extérieur et pour forcer nos marchands à communiquer en utilisant le protocole binaire, nous l'avons jugé trop cruel pour eux. Il était nécessaire de choisir un protocole lisible par l'homme qui nous permettrait de nous intégrer facilement à nous, de déboguer et de pouvoir documenter facilement. Nous avons choisi la norme Open API, également connue sous le nom de Swagger .
Revenant au problème de la documentation des protocoles, Swagger vous permet de résoudre rapidement et à moindre coût ce problème. Le réseau possède de nombreuses implémentations de la belle conception de la spécification Swagger sous forme de documentation pour les développeurs. Nous avons examiné tout ce que nous pouvions trouver et avons finalement choisi ReDoc , une bibliothèque JS qui accepte swagger.json en entrée, et génère une telle documentation en trois colonnes à la sortie: https://developer.rbk.money/api/ .
Les approches du développement des deux protocoles, Thrift interne et Swagger externe, sont absolument identiques pour nous. Cela ajoute du temps au développement, mais est rentable à long terme.
Nous devions également résoudre un autre problème important - nous acceptons non seulement les demandes d'annulation de fonds, mais nous les envoyons également plus loin - aux banques et aux systèmes de paiement.
Les forcer à mettre en œuvre notre ascenseur serait une tâche encore plus impraticable que de le soumettre à des API publiques.
Par conséquent, nous avons conçu et mis en œuvre le concept d'adaptateurs de protocole. Ceci est juste un autre microservice qui implémente notre spécification de levage interne d'un côté, qui est le même pour toute la plate-forme, et le second est un protocole externe spécifique à une banque ou PS particulière.
Les problèmes qui surviennent lors de l'écriture de tels adaptateurs lorsque vous devez interagir avec des tiers est un sujet riche en histoires différentes. Dans notre pratique, nous avons rencontré différentes choses, des réponses de la forme: "vous, bien sûr, pouvez mettre en œuvre cette fonction comme décrit dans le protocole que nous vous avons donné, mais je ne donne aucune garantie. Voici notre malade en 2 semaines, qui est pour toutes ces réponses, et vous lui demandez confirmation. " De plus, de telles situations ne sont pas rares: "voici le nom d'utilisateur et le mot de passe de notre serveur, allez-y et configurez tout vous-même."
Je trouve cela particulièrement intéressant lorsque nous nous sommes intégrés à un partenaire de paiement, qui, à son tour, s'était auparavant intégré à notre plate-forme et avait effectué avec succès des paiements par notre intermédiaire (cela arrive souvent, les spécificités commerciales de l'industrie du paiement). En réponse à notre demande d'un environnement de test, le partenaire a répondu qu'il n'avait pas d'environnement de test en tant que tel, mais qu'il pouvait obtenir du trafic pour l'intégration avec RBC, c'est-à-dire avec notre plateforme, où nous pourrions nous impliquer. C'est ainsi que nous, à travers un partenaire, nous nous sommes intégrés une fois.
Ainsi, nous avons tout simplement résolu le problème de la mise en œuvre d'une connexion parallèle de masse de divers systèmes de paiement et d'autres tiers. Dans la grande majorité des cas, vous n'avez pas besoin de toucher le code de la plate-forme pour cela, il suffit d'écrire les adaptateurs et d'ajouter plus d'instruments de paiement à l'énumération.
En conséquence, nous avons obtenu un tel schéma de travail - nous regardons en dehors des microservices de l'API RBKmoney (nous les appelons API commune, ou capi *, vous les avez vu dans le consul ci-dessus), qui valident les données d'entrée selon la spécification publique Swagger, autorisent les clients, diffusent ces méthodes à nos appels internes et envoient les demandes en aval au prochain microservice. De plus, ces services implémentent une autre exigence de plate-forme, dont la spécification technique a été formulée comme suit: "le système devrait toujours avoir la possibilité d'obtenir un chat".
Lorsque nous devons appeler un système externe, des microservices internes tirent les méthodes de levage de l'adaptateur de protocole correspondant, ils les traduisent dans la langue d'une banque ou d'un système de paiement spécifique et les envoient.
Problèmes de compatibilité descendante du protocole
La plateforme évolue constamment, de nouvelles fonctions sont ajoutées, les anciennes sont modifiées. Dans de telles circonstances, vous devez investir dans la prise en charge de la compatibilité descendante ou mettre à jour constamment les microservices dépendants. Et si la situation où le champ requis devient facultatif est simple, vous ne pouvez rien faire du tout, dans le cas contraire, vous devez dépenser des ressources supplémentaires.
Avec un ensemble de protocoles internes, les choses vont plus facilement. L'industrie du paiement change rarement, de sorte que des méthodes d'interaction fondamentalement nouvelles apparaissent. Prenons, par exemple, une tâche commune pour nous - connecter un nouveau fournisseur avec un nouvel instrument de paiement. Par exemple, le traitement de portefeuille local, qui vous permet de traiter les paiements au Kazakhstan en tenge. Il s'agit d'un nouveau portefeuille pour notre plate-forme, mais en principe, il ne diffère pas du même portefeuille Qiwi - il a toujours un identifiant et des méthodes uniques qui vous permettent d'en débiter / d'annuler le débit.
En conséquence, notre spécification d'ascenseur pour tous les fournisseurs de portefeuilles ressemble à ceci:
typedef string DigitalWalletID struct DigitalWallet { 1: required DigitalWalletProvider provider 2: required DigitalWalletID id } enum DigitalWalletProvider { qiwi rbkmoney }
et l'ajout d'un nouveau moyen de paiement sous la forme d'un nouveau portefeuille complète simplement l'énumération:
enum DigitalWalletProvider { qiwi rbkmoney newwallet }
Il ne reste plus qu'à bump tous les microservices utilisant cette spécification, en se synchronisant avec l'assistant de référentiel avec la spécification et en les déployant via CI / CD.
Les protocoles externes sont plus compliqués. Chaque mise à jour de la spécification Swagger, en particulier sans compatibilité descendante, est presque impossible à appliquer dans un délai raisonnable - il est peu probable que nos partenaires conservent des ressources de développement gratuites spécifiquement pour la mise à jour de notre plate-forme.
Et parfois, cela est tout simplement impossible, nous rencontrons parfois des situations comme: "le programmeur nous a écrit et est parti, a pris les sources avec nous, comment nous travaillons, nous ne savons pas, cela fonctionne et ne le touchez pas."
Par conséquent, nous investissons dans la prise en charge de la compatibilité descendante sur les protocoles externes. Dans notre architecture, c'est un peu plus facile - puisque nous utilisons des adaptateurs de protocole séparés pour chaque version spécifique de l'API commune, nous laissons simplement les anciens microservices capi fonctionner, ne changeant que la partie qui ressemble à une bagatelle à l'intérieur de la plate-forme si nécessaire. Les microservices capi-v1
, capi-v2
, capi-v3
et ainsi de suite apparaissent et restent avec nous pour toujours.
Que se passera capi-v33
il lorsque capi-v33
nous devrons probablement déprécier certaines anciennes versions.
À ce stade, je commence généralement à très bien comprendre les entreprises telles que Microsoft et toute leur peine à prendre en charge la compatibilité descendante des solutions qui fonctionnent depuis des décennies.
Personnalisez le système
Et, pour terminer le sujet, nous vous expliquerons comment nous gérons les paramètres de plate-forme spécifiques à l'entreprise.
Faire un paiement n'est pas aussi simple qu'il y paraît. Pour chaque paiement, le client professionnel souhaite associer un grand nombre de conditions - de la commission à, en principe, la possibilité d'une mise en œuvre réussie en fonction de l'heure de la journée. Nous nous sommes fixé pour tâche de numériser l'ensemble des conditions qu'un client professionnel peut proposer maintenant et à l'avenir et d'appliquer cet ensemble à chaque paiement nouvellement lancé.
En conséquence, nous avons décidé de développer notre propre DSL, auquel nous avons foiré des outils de gestion pratique qui nous permettent de décrire le modèle commercial de la bonne manière: le choix des adaptateurs de protocole, une description du plan de publication, selon laquelle l'argent sera dispersé dans les comptes du système, en fixant des limites, des commissions, des catégories et d'autres choses spécifiques au système de paiement.
Par exemple, lorsque nous voulons prendre une commission de 1% pour l'acquisition sur les cartes du maestro et MS et la disperser sur les comptes à l'intérieur du système, nous configurons le domaine comme ceci:
{ "cash_flow": { "decisions": [ { "if_": { "any_of": [ { "condition": { "payment_tool": { "bank_card": { "definition": { "payment_system_is": "maestro" } } } } }, { "condition": { "payment_tool": { "bank_card": { "definition": { "payment_system_is": "mastercard" } } } } } ] }, "then_": { "value": [ { "source": { "system": "settlement" }, "destination": { "provider": "settlement" }, "volume": { "share": { "parts": { "p": 1, "q": 100 }, "of": "operation_amount" } }, "details": "1% processing fee" } ] } } ] } }
, , . , JSON. , , , . , , . , CVS/SVN-.
" ". , , , 1%, , , . , , . , .
cvs-like , . , — stateless, , . . .
- . , , . , , .
. , 10 , , .
, , , -, woorl-. - JSON- . - JS, , UX:

, , , .
, , .
, , SaltStack.
, !