Hallo Habr! RBKmoney meldet sich erneut und setzt eine Reihe von Artikeln zum Schreiben der Do-it-yourself-Zahlungsabwicklung fort.

Ich wollte sofort in die Details der Beschreibung der Implementierung eines Zahlungsgeschäftsprozesses als Zustandsmaschine eintauchen, Beispiele für eine solche Maschine mit einer Reihe von Ereignissen und Implementierungsfunktionen zeigen ... Aber es scheint, dass Sie nicht auf ein paar weitere Übersichtsartikel verzichten können. Der Themenbereich erwies sich als zu groß. In diesem Beitrag werden die Nuancen der Arbeit und Interaktion zwischen den Microservices unserer Plattform, die Interaktion mit externen Systemen und die Verwaltung der Geschäftskonfiguration erläutert.
Makrodienst
Unser System besteht aus vielen Mikrodiensten, die jeden ihrer fertigen Teile der Geschäftslogik implementieren, miteinander interagieren und zusammen einen Makrodienst bilden. Tatsächlich ist der im Rechenzentrum bereitgestellte Makrodienst, der mit Banken und anderen Zahlungssystemen verbunden ist, unsere Zahlungsverarbeitung.
Microservice-Vorlage
Wir verwenden einen einheitlichen Ansatz für die Entwicklung eines Mikrodienstes in einer beliebigen Sprache. Jeder Microservice ist ein Docker-Container, der Folgendes enthält:
- die Anwendung selbst, die in Erlang oder Java geschriebene Geschäftslogik implementiert;
- RPClib - eine Bibliothek, die die Kommunikation zwischen Microservices implementiert;
- Wir verwenden Apache Thrift. Die Hauptvorteile sind vorgefertigte Client-Server-Bibliotheken und die Möglichkeit, die Beschreibung aller öffentlichen Methoden, die jeder Microservice bietet, genau zu typisieren.
- Das zweite Merkmal der Bibliothek ist die Implementierung von Google Dapper , mit der wir Anfragen mit einer einfachen Suche in Elasticsearch schnell verfolgen können. Der erste
trace_id
, der eine Anforderung von einem externen System erhalten hat, generiert eine eindeutige trace_id
, die von jeder nachfolgenden Anforderungskette gespeichert wird. Außerdem generieren und speichern wir parent_id
und span_id
, mit denen Sie einen span_id
parent_id
und die gesamte Kette von span_id
, die an der Verarbeitung der Anforderung beteiligt sind, visuell überwachen können. - Das dritte Merkmal: Wir nutzen die Übertragung verschiedener Informationen über den Kontext der Anfrage auf Transportebene aktiv. Zum Beispiel Fristen (die erwartete Lebensdauer der auf dem Client festgelegten Anforderung) oder in deren Auftrag wir eine Methode aufrufen;
- Die Consul-Vorlage ist ein Service Discovery Agent, der Informationen zu Standort, Verfügbarkeit und Status eines Microservices verwaltet. Microservices finden sich anhand von DNS-Namen, die Zone TTL ist Null, der Dienst, der gestorben ist oder den Healthcheck nicht bestanden hat, wird nicht mehr aufgelöst und empfängt somit Datenverkehr.
- Von der Anwendung geschriebene Protokolle in einem für Elasticsearch verständlichen Format in die lokale Containerdatei und
filebeat
, die auf dem filebeat
relativ zum Container ausgeführt werden, nehmen diese Protokolle auf und senden sie an den Elasticsearch-Cluster.
- Da wir die Plattform gemäß dem Event-Sourcing-Modell implementieren, werden die resultierenden Protokollketten auch zur Visualisierung in Form verschiedener Grafana-Dashboards verwendet, wodurch wir die Zeit für die Implementierung verschiedener Metriken verkürzen können (wir verwenden auch separate Metriken).

Bei der Entwicklung von Microservices verwenden wir die speziell erfundenen Einschränkungen, mit denen das Problem der hohen Verfügbarkeit der Plattform und ihrer Fehlertoleranz gelöst werden soll:
- Strenge Speichergrenzen für jeden Container, wenn Sie die Grenzen überschreiten - OOM, die meisten Microservices leben innerhalb von 256-512 MB. Dies macht die Implementierung der Geschäftslogik feiner fragmentiert, schützt vor Abdrift zum Monolithen, senkt die Kosten der Fehlerstelle und bietet einen zusätzlichen Vorteil bei der Arbeit mit billiger Hardware (die Plattform wird bereitgestellt und läuft auf kostengünstigen Einzelprozessor-Servern).
- so wenig Stateful Microservices wie möglich und so viele zustandslose Implementierungen wie möglich. Dies ermöglicht es uns, die Probleme der Fehlertoleranz, der Geschwindigkeit der Wiederherstellung und im Allgemeinen der Minimierung von Orten mit möglicherweise unverständlichem Verhalten zu lösen. Dies wird besonders wichtig, wenn die Lebensdauer des Systems verlängert wird, wenn sich ein großes Erbe ansammelt.
- lass es abstürzen und "es wird definitiv brechen" nähert sich. Wir wissen, dass ein Teil unseres Systems notwendigerweise ausfallen wird, daher gestalten wir es so, dass die allgemeine Richtigkeit der auf der Plattform gesammelten Informationen nicht beeinträchtigt wird. Hilft bei der Minimierung der Anzahl undefinierter Zustände im System.
Sicher vertraut vielen, die sich mit Dritten integrieren, die Situation. Wir erwarteten eine Antwort eines Dritten auf die Aufforderung, Geld gemäß dem Protokoll abzuschreiben, und es kam eine völlig andere Antwort, die in keiner Spezifikation beschrieben wurde und deren Interpretation unbekannt ist.
In dieser Situation töten wir die Zustandsmaschine, die diese Zahlung bedient. Alle Aktionen von außen erhalten einen Fehler von 500. Im Inneren ermitteln wir den aktuellen Status der Zahlung, bringen den Zustand der Maschine in Einklang mit der Realität und beleben die Zustandsmaschine wieder.
Protokollorientierte Entwicklung

Zum Zeitpunkt des Schreibens waren in unserer Service Discovery 636 verschiedene Prüfungen für Services registriert, die das Funktionieren der Plattform sicherstellen. Selbst wenn man berücksichtigt, dass mehrere Überprüfungen für einen Dienst durchgeführt werden und die meisten zustandslosen Dienste in mindestens einer dreifachen Instanz funktionieren, können Sie immer noch fünfzig Anwendungen erhalten, die in der Lage sein müssen, irgendwie miteinander verbunden zu werden und nicht fehlzuschlagen in der RPC-Hölle.
Die Situation wird durch die Tatsache kompliziert, dass wir drei Entwicklungssprachen auf dem Stapel haben - Erlang, Java, JS, und alle müssen in der Lage sein, transparent miteinander zu kommunizieren.
Die erste Aufgabe, die gelöst werden musste, bestand darin, die richtige Architektur für den Datenaustausch zwischen Mikrodiensten zu entwerfen. Als Basis haben wir Apache Thrift genommen. Alle Microservices tauschen Trift-Binärdateien aus, wir verwenden HTTP als Transportmittel.
Wir platzieren die Schriftartenspezifikationen in Form von separaten Repositorys in unserem Github, sodass sie jedem Entwickler zur Verfügung stehen, der Zugriff darauf hat. Anfangs verwendeten sie ein gemeinsames Repository für alle Protokolle, kamen jedoch im Laufe der Zeit zu dem Schluss, dass dies unpraktisch ist - die gemeinsame parallele Arbeit an den Protokollen führte zu ständigen Kopfschmerzen. Verschiedene Teams und sogar verschiedene Entwickler waren gezwungen, sich auf den Namen der Variablen zu einigen. Ein Versuch, sich in einen Namespace aufzuteilen, half ebenfalls nicht.
Im Allgemeinen können wir sagen, dass wir eine protokollgesteuerte Entwicklung haben. Bevor wir mit der Implementierung beginnen, entwickeln wir das zukünftige Microservice-Protokoll in Form einer Lift-Spezifikation, durchlaufen 7 Überprüfungskreise, um zukünftige Kunden dieses Microservices anzulocken, und erhalten die Möglichkeit, gleichzeitig mehrere Microservices parallel zu entwickeln, da wir alle zukünftigen Methoden kennen und bereits ihre Handler schreiben können. optional mit moki.
Ein separater Schritt im Protokollentwicklungsprozess ist eine Sicherheitsüberprüfung, bei der die Jungs aus ihrer Pentester-Sicht die Nuancen der zu entwickelnden Spezifikation betrachten.
Wir hielten es auch für angemessen, eine separate Rolle des Protokollbesitzers im Team hervorzuheben. Die Aufgabe ist schwierig, eine Person muss die Besonderheiten aller Mikrodienste berücksichtigen, aber sie zahlt sich in großem Umfang aus und es gibt nur einen einzigen Eskalationspunkt.
Ohne die endgültige Genehmigung der Pull-Anforderung durch diese Mitarbeiter kann das Protokoll nicht in einer Hauptniederlassung zusammengeführt werden. Hierfür gibt es im Github eine sehr praktische Funktion - Codebesitzer , die wir gerne nutzen.
So haben wir das Problem der Kommunikation zwischen Mikrodiensten gelöst, mögliche Probleme des Missverständnisses, welche Art von Mikrodienst auf der Plattform angezeigt wurde und warum er benötigt wird. Dieser Satz von Protokollen ist vielleicht der einzige Teil der Plattform, auf dem wir Qualität bedingungslos gegen die Kosten und die Geschwindigkeit der Entwicklung wählen, da die Implementierung eines Mikrodienstes relativ schmerzlos umgeschrieben werden kann und das Protokoll, auf dem mehrere Dutzend bereits teuer und schmerzhaft sind.
Auf dem Weg hilft eine genaue Protokollierung bei der Lösung des Dokumentationsproblems. Vernünftigerweise ausgewählte Namen von Methoden und Parametern, einige Kommentare und eine selbst dokumentierte Spezifikation sparen viel Zeit!
So sieht beispielsweise die Spezifikation der Methode eines unserer Microservices aus, sodass Sie eine Liste der Ereignisse abrufen können, die auf der Plattform aufgetreten sind:
/** */ 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 }
Thrift Console Client
Manchmal stehen wir vor der Aufgabe, bestimmte Methoden des notwendigen Mikrodienstes direkt aufzurufen, beispielsweise mit unseren Händen vom Terminal aus. Dies kann nützlich sein, um Fehler zu beheben, einen Rohdatensatz zu erhalten oder wenn die Aufgabe so selten ist, dass die Entwicklung einer separaten Benutzeroberfläche unpraktisch ist.
Aus diesem Grund haben wir ein Tool für uns entwickelt, das curl
Funktionen kombiniert, es Ihnen jedoch ermöglicht, Trift-Anforderungen in Form von JSON-Strukturen zu stellen. Wir haben ihn entsprechend woorl
- woorl
. Das Dienstprogramm ist universell einsetzbar. Es reicht aus, den Speicherort einer Aufzugsspezifikation mithilfe des Befehlszeilenparameters zu übertragen. Den Rest erledigt es selbst. Als sehr praktisches Dienstprogramm können Sie beispielsweise eine Zahlung direkt vom Terminal aus starten.
So sieht der Appell direkt auf den Microservice der Plattform aus, der für die Verwaltung von Anwendungen verantwortlich ist (z. B. zum Erstellen eines Geschäfts). Ich habe Daten auf meinem Testkonto angefordert:

Das Beobachten der Leser bemerkte wahrscheinlich eine Funktion im Screenshot. Das gefällt uns auch nicht. Es ist notwendig, die Autorisierung von Trift-Anrufen zwischen Microservices zu befestigen, es ist notwendig, TLS auf eine gute Weise zu kleben. Aber während Ressourcen wie immer nicht ausreichen. Wir haben uns auf die gesamte Umschließung des Perimeters beschränkt, in dem verarbeitende Mikrodienste leben.
Protokolle für die Kommunikation mit externen Systemen
Um externe Lift-Spezifikationen zu veröffentlichen und unsere Händler zur Kommunikation mit dem Binärprotokoll zu zwingen, hielten wir es für zu grausam für sie. Es war notwendig, ein für Menschen lesbares Protokoll zu wählen, das es uns ermöglicht, bequem in uns zu integrieren, zu debuggen und bequem zu dokumentieren. Wir haben uns für den Open API-Standard entschieden, der auch als Swagger bekannt ist .
Zurück zum Problem der Dokumentation von Protokollen: Mit Swagger können Sie dieses Problem schnell und kostengünstig lösen. Das Netzwerk verfügt über viele Implementierungen des schönen Designs der Swagger-Spezifikation in Form einer Entwicklerdokumentation. Wir haben uns alles angesehen, was wir finden konnten, und uns schließlich für ReDoc entschieden, eine JS-Bibliothek, die swagger.json als Eingabe akzeptiert, und eine solche dreispaltige Dokumentation an der Ausgabe generiert: https://developer.rbk.money/api/ .
Die Ansätze zur Entwicklung beider Protokolle, Internal Thrift und External Swagger, sind für uns absolut identisch. Dies verlängert die Entwicklungszeit, zahlt sich aber langfristig aus.
Wir mussten auch ein weiteres wichtiges Problem lösen - wir akzeptieren nicht nur Anfragen, Geld abzuschreiben, sondern senden sie auch weiter - an Banken und Zahlungssysteme.
Sie zu zwingen, unseren Lift zu implementieren, wäre eine noch unpraktikablere Aufgabe, als ihn an öffentliche APIs zu senden.
Aus diesem Grund haben wir das Konzept der Protokolladapter entwickelt und implementiert. Dies ist nur ein weiterer Mikrodienst, der unsere interne Aufzugsspezifikation auf einer Seite implementiert, die für die gesamte Plattform gleich ist, und die zweite ist ein externes Protokoll, das für eine bestimmte Bank oder PS spezifisch ist.
Die Probleme, die beim Schreiben solcher Adapter auftreten, wenn Sie mit Dritten interagieren müssen, sind ein Thema, das reich an verschiedenen Geschichten ist. In unserer Praxis haben wir verschiedene Dinge getroffen, Antworten der Form: „Sie können diese Funktion natürlich wie in dem Protokoll beschrieben implementieren, das wir Ihnen gegeben haben, aber ich gebe keine Garantien. Hier kommt unser Patient, der wird all diese Antworten, und Sie bitten ihn um Bestätigung. " Solche Situationen sind auch nicht ungewöhnlich: "Hier ist der Benutzername und das Passwort von unserem Server. Gehen Sie dorthin und konfigurieren Sie alles selbst."
Ich finde es besonders interessant, wenn wir uns in einen Zahlungspartner integriert haben, der sich zuvor in unsere Plattform integriert und erfolgreich Zahlungen über uns getätigt hat (dies geschieht häufig aufgrund der geschäftlichen Besonderheiten der Zahlungsbranche). Als Antwort auf unsere Anfrage nach einer Testumgebung antwortete der Partner, dass er keine Testumgebung als solche habe, aber er könnte Datenverkehr für die Integration mit RBC erhalten, dh mit unserer Plattform, auf der wir uns beteiligen könnten. So haben wir uns über einen Partner einmal in uns integriert.
Damit haben wir ganz einfach das Problem der Implementierung einer Massenparallelverbindung verschiedener Zahlungssysteme und anderer Dritter gelöst. In den allermeisten Fällen müssen Sie dazu nicht den Plattformcode berühren, sondern nur die Adapter schreiben und der Aufzählung weitere Zahlungsinstrumente hinzufügen.
Als Ergebnis haben wir ein solches Arbeitsschema erhalten - wir schauen außerhalb der RBKmoney API-Microservices (wir nennen sie Common API oder capi *, Sie haben sie im Konsul oben gesehen), die die Eingabedaten gemäß der öffentlichen Swagger-Spezifikation validieren, Clients autorisieren, senden Diese Methoden werden an unsere internen Liftanrufe weitergeleitet und Anfragen in der gesamten Kette an den nächsten Microservice gesendet. Darüber hinaus implementieren diese Dienste eine weitere Plattformanforderung, deren technische Spezifikation wie folgt formuliert wurde: "Das System sollte immer die Möglichkeit haben, eine Katze zu bekommen."
Wenn wir ein externes System anrufen müssen, ziehen interne Mikrodienste die Lift-Methoden des entsprechenden Protokolladapters, übersetzen sie in die Sprache einer bestimmten Bank oder eines bestimmten Zahlungssystems und senden sie aus.
Schwierigkeiten bei der Abwärtskompatibilität des Protokolls
Die Plattform entwickelt sich ständig weiter, neue Funktionen werden hinzugefügt, alte werden geändert. Unter solchen Umständen müssen Sie entweder in die Unterstützung der Abwärtskompatibilität investieren oder abhängige Microservices ständig aktualisieren. Und wenn die Situation, in der das erforderliche Feld optional wird, einfach ist, können Sie überhaupt nichts tun, und im umgekehrten Fall müssen Sie zusätzliche Ressourcen aufwenden.
Mit einer Reihe interner Protokolle wird es einfacher. Die Zahlungsbranche ändert sich selten, so dass einige grundlegend neue Interaktionsmethoden auftauchen. Nehmen wir zum Beispiel eine gemeinsame Aufgabe für uns - einen neuen Anbieter mit einem neuen Zahlungsinstrument zu verbinden. Zum Beispiel die lokale Brieftaschenverarbeitung, mit der Sie Zahlungen in Kasachstan in Tenge verarbeiten können. Dies ist eine neue Brieftasche für unsere Plattform, die sich jedoch im Prinzip nicht von derselben Qiwi-Brieftasche unterscheidet. Sie verfügt immer über eine eindeutige Kennung und Methoden, mit denen Sie die Belastung belasten / stornieren können.
Dementsprechend sieht unsere Aufzugsspezifikation für alle Brieftaschenanbieter folgendermaßen aus:
typedef string DigitalWalletID struct DigitalWallet { 1: required DigitalWalletProvider provider 2: required DigitalWalletID id } enum DigitalWalletProvider { qiwi rbkmoney }
und das Hinzufügen eines neuen Zahlungsmittels in Form einer neuen Brieftasche ergänzt einfach enum:
enum DigitalWalletProvider { qiwi rbkmoney newwallet }
Jetzt müssen alle Microservices mit dieser Spezifikation erweitert, mit dem Repository-Assistenten mit der Spezifikation synchronisiert und über CI / CD bereitgestellt werden.
Externe Protokolle sind komplizierter. Es ist fast unmöglich, jedes Update der Swagger-Spezifikation, insbesondere ohne Abwärtskompatibilität, innerhalb eines angemessenen Zeitrahmens anzuwenden. Es ist unwahrscheinlich, dass unsere Partner kostenlose Entwicklerressourcen speziell für die Aktualisierung unserer Plattform behalten.
Und manchmal ist dies einfach unmöglich. Gelegentlich stoßen wir auf Situationen wie: "Der Programmierer hat uns geschrieben und ist gegangen, hat den Quellcode mitgenommen, wie wir arbeiten, wir wissen nicht, es funktioniert und berühren ihn nicht."
Daher investieren wir in die Unterstützung der Abwärtskompatibilität mit externen Protokollen. Dies ist in unserer Architektur etwas einfacher. Da wir für jede spezifische Version der Common API separate Protokolladapter verwenden, lassen wir nur die alten Capi-Microservices funktionieren und ändern bei Bedarf nur den Teil, der wie ein Trift innerhalb der Plattform aussieht. So erscheinen und bleiben Microservices capi-v1
, capi-v2
, capi-v3
usw. für immer bei uns.
Was passiert, wenn capi-v33
wir wahrscheinlich einige alte Versionen verwerfen.
An diesem Punkt fange ich normalerweise an, Unternehmen wie Microsoft und all ihre Schwierigkeiten bei der Unterstützung der Abwärtskompatibilität von Lösungen, die seit Jahrzehnten funktionieren, sehr gut zu verstehen.
Passen Sie das System an
Zum Abschluss des Themas erfahren Sie, wie wir die geschäftsspezifischen Plattformeinstellungen verwalten.
Nur eine Zahlung zu leisten ist nicht so einfach, wie es scheint. Für jede Zahlung möchte der Geschäftskunde eine Vielzahl von Bedingungen festlegen - von der Provision bis zur prinzipiellen Möglichkeit einer erfolgreichen Implementierung je nach Tageszeit. Wir haben uns die Aufgabe gestellt, alle Bedingungen, die ein Geschäftskunde jetzt und in Zukunft haben kann, zu digitalisieren und auf jede neu gestartete Zahlung anzuwenden.
Aus diesem Grund haben wir uns entschlossen, ein eigenes DSL zu entwickeln, mit dem wir Tools für eine bequeme Verwaltung vermasselt haben, mit denen wir das Geschäftsmodell richtig beschreiben können: die Auswahl der Protokolladapter, eine Beschreibung des Buchungsplans, nach der das Geld auf die Konten innerhalb des Systems verteilt wird, Grenzwerte, Provisionen, Kategorien und andere Dinge, die für das Zahlungssystem spezifisch sind.
Wenn wir beispielsweise eine Provision von 1% für den Erwerb von Karten des Maestro und der MS auf Konten im System verteilen möchten, konfigurieren wir die Domain folgendermaßen:
{ "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" } ] } } ] } }
Einfach ausgedrückt haben wir alle Plattformeinstellungen oder die Domänenkonfiguration an einem Ort. , JSON. , , , . , , . , CVS/SVN-.
" ". , , , 1%, , , . , , . , .
cvs-like , . , — stateless, , . . .
- . , , . , , .
. , 10 , , .
, , , -, woorl-. - JSON- . - JS, , UX:

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