Ankündigung
Kollegen, ich plane für Mitte des Sommers die Veröffentlichung eines weiteren Artikels über das Design von Systemen für den Massendienst: „Experiment VTrade“ – der Versuch, ein Framework für Handelssysteme zu schreiben. Der Zyklus wird die Theorie und Praxis des Aufbaus von Börsen, Auktionen und Geschäften behandeln. Am Ende des Artikels lade ich dazu ein, über die für Sie interessantesten Themen abzustimmen.

Dies ist der abschließende Artikel des Zyklus über verteilte reaktive Anwendungen in Erlang/Elixir. In lassen sich die theoretischen Grundlagen der reaktiven Architektur finden. illustriert die grundlegenden Muster und Mechanismen zum Aufbau solcher Systeme.
Heute werden wir die Fragen zur Weiterentwicklung der Codebasis und der Projekte insgesamt ansprechen.
Organisation der Dienste
Im echten Leben müssen bei der Entwicklung eines Dienstes oft mehrere Interaktionsmuster in einem Controller kombiniert werden. Zum Beispiel sollte der Dienst users, der für die Verwaltung der Benutzerprofile des Projekts zuständig ist, auf req-resp-Anfragen reagieren und über Aktualisierungen der Profile über pub-sub informieren. Dieser Fall ist ziemlich einfach: Hinter dem Messaging steht ein Controller, der die Logik des Dienstes implementiert und Aktualisierungen veröffentlicht.
Die Situation wird komplizierter, wenn wir einen ausfallsicheren verteilten Dienst implementieren müssen. Stellen wir uns vor, dass sich die Anforderungen an users geändert haben:
- Jetzt muss der Dienst Anfragen auf 5 Knoten im Cluster verarbeiten,
- die Möglichkeit haben, Hintergrundverarbeitungsaufgaben auszuführen,
- und auch in der Lage sein, dynamisch die Listen der Abonnements für die Aktualisierungen der Profile zu verwalten.
Hinweis: Die Frage der konsistenten Speicherung und Replikation von Daten betrachten wir nicht. Angenommen, diese Fragen wurden zuvor gelöst und in der System existiert bereits eine zuverlässige und skalierbare Speicherungsschicht, und die Handler haben Mechanismen für die Interaktion mit dieser.
Die formale Beschreibung des Dienstes users hat sich kompliziert. Aus Sicht des Programmierers sind die Änderungen dank der Verwendung von Messaging minimal. Um das erste Anliegen zu erfüllen, müssen wir das Load Balancing an der req-resp-Schnittstelle einrichten.
Die Anforderung zur Verarbeitung von Hintergrundaufgaben tritt häufig auf. In den Benutzern kann es sich dabei um die Überprüfung von Benutzerdokumenten, die Verarbeitung hochgeladener Medien oder die Synchronisierung von Daten mit sozialen Netzwerken handeln. Diese Aufgaben müssen innerhalb des Clusters verteilt und der Fortschritt überwacht werden. Daher haben wir zwei Lösungsmöglichkeiten: entweder das Aufgabenverteilungsschema aus dem vorherigen Artikel verwenden oder, falls dies nicht passt, einen benutzerdefinierten Task-Planer schreiben, der den Pool der Bearbeiter auf die benötigte Weise verwaltet.
Punkt 3 erfordert die Erweiterung des Pub-Sub-Schemas. Und zur Umsetzung müssen wir nach der Erstellung des Pub-Sub-Austauschpunkts zusätzlich den Controller dieses Punkts im Rahmen unseres Services starten. Dadurch nehmen wir quasi die Logik der Subscription- und Unsubscription-Verarbeitung aus der Messaging-Schicht und implementieren sie in den Benutzern.
Letztendlich hat die Dekomposition der Aufgabe gezeigt, dass wir zur Erfüllung der Anforderungen 5 Instanzen des Services auf verschiedenen Knoten starten und eine zusätzliche Entität – den Pub-Sub-Controller – schaffen müssen, der für die Subscription verantwortlich ist.
Für den Start von 5 Bearbeitern ist keine Anpassung des Codes des Services erforderlich. Die einzige zusätzliche Maßnahme ist die Konfiguration der Lastverteilungsregeln am Austauschpunkt, über die wir später sprechen werden.
Außerdem gibt es eine zusätzliche Komplexität: Der Pub-Sub-Controller und der benutzerdefinierte Task-Planer müssen in einer einzigen Instanz arbeiten. Wiederum sollte der Messaging-Service, als fundamentale Komponente, einen Mechanismus zur Wahl eines Leaders bereitstellen.
Wahl eines Leaders
In verteilten Systemen ist die Wahl eines Leaders das Verfahren zur Ernennung eines einzigen Prozesses, der für die Planung der verteilten Verarbeitung einer bestimmten Last verantwortlich ist.
In Systemen, die nicht zur Zentralisierung neigen, kommen universelle Algorithmen und konsensorientierte Algorithmen wie Paxos oder Raft zum Einsatz.
Da Messaging ein Broker und ein zentrales Element ist, kennt es alle Controller des Services – Kandidaten für die Führungsrolle. Messaging kann einen Leader ohne Abstimmung ernennen.
Alle Dienste erhalten nach dem Start und der Verbindung zu dem Austauschpunkt eine Systemnachricht #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Falls LeaderPid mit pid dem aktuellen Prozess übereinstimmt, wird er zum Leader ernannt, und die Liste Servers enthält alle Knoten und ihre Parameter.
Im Moment des Erscheinens eines neuen und der Abschaltung eines funktionierenden Knotens im Cluster erhalten alle Service-Controller #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} und #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} entsprechend.
So wissen alle Komponenten über alle Änderungen Bescheid, und im Cluster gibt es zu jedem Zeitpunkt garantiert einen Führer.
Vermittler
Um komplexe verteilte Verarbeitungsprozesse zu realisieren und bestehende Architekturen zu optimieren, ist es sinnvoll, Vermittler einzusetzen.
Um den Code der Dienste nicht zu ändern und beispielsweise Aufgaben der zusätzlichen Verarbeitung, Routing oder Protokollierung von Nachrichten zu lösen, kann vor dem Dienst ein Proxy-Handler aktiviert werden, der die gesamte zusätzliche Arbeit übernimmt.
Ein klassisches Beispiel für die Optimierung von Pub-Sub ist eine verteilte Anwendung mit einem Geschäfts-Kern, der Ereignisse über Aktualisierungen generiert, wie z.B. Preisänderungen auf dem Markt, und einer Zugriffsschicht – N Server, die WebSocket-APIs für Web-Clients bereitstellen.
Wenn man es „direkt angeht“, sieht die Bedienung des Clients wie folgt aus:
- Der Client stellt eine Verbindung zur Plattform her. Auf der Serverseite, die den Verkehr terminiert, wird ein Prozess gestartet, der diese Verbindung bedient.
- Im Kontext des bedienenden Prozesses erfolgt die Autorisierung und das Abonnieren von Updates. Der Prozess ruft die Methode subscribe für die Themen auf.
- Nach der Ereignisgenerierung im Kern wird es an die Prozesse geliefert, die die Verbindungen bedienen.
Stellen wir uns vor, wir haben 50000 Abonnenten für das Thema „news“. Die Abonnenten sind gleichmäßig auf 5 Server verteilt. Infolgedessen wird jedes Update, das an den Austauschpunkt kommt, 50000-mal repliziert: 10000-mal auf jeden Server, je nach Anzahl der Abonnenten auf diesem. Nicht gerade ein effizientes Schema, oder?
Um die Situation zu verbessern, führen wir einen Proxy ein, der denselben Namen wie der Austauschpunkt hat. Der globale Namensregistrar sollte in der Lage sein, den nächsten Prozess nach Namen zurückzugeben, das ist wichtig.
Lassen Sie uns diesen Proxy auf den Servern der Zugriffsschicht starten, und alle unsere Prozesse, die die WebSocket-API bedienen, abonnieren ihn statt dem ursprünglichen Pub-Sub-Austauschpunkt im Kern. Der Proxy abonniert den Kern nur im Falle eines einzigartigen Abonnements und repliziert die eingehende Nachricht bei allen seinen Abonnenten.
Insgesamt werden zwischen dem Kern und den Zugangsservern 5 Nachrichten übermittelt, anstelle von 50000.
Routing und Lastverteilung
Req-Resp
In der aktuellen Implementierung des Messaging gibt es 7 Strategien zur Verteilung von Anfragen:
default. Die Anfrage wird an alle Controller übermittelt.round-robin. Es erfolgt eine Durchlauf- und zirkuläre Verteilung der Anfragen zwischen den Controllern.Konsens. Die Controller, die den Dienst betreuen, teilen sich in einen Führer und geführte auf. Anfragen werden nur an den Führer übermittelt.Konsens & round-robin. In der Gruppe gibt es einen Führer, aber die Anfragen werden unter allen Mitgliedern verteilt.sticky. Es wird eine Hash-Funktion berechnet und einem bestimmten Handler zugeordnet. Folgende Anfragen mit dieser Signatur gelangen zu diesem Handler.sticky-fun. Bei der Initialisierung des Austauschpunkts wird zusätzlich eine Funktion zur Berechnung des Hashes übergeben fürsticky. die Lastverteilung.Spaß. Ähnlich wie sticky-fun, jedoch kann zusätzlich umgeleitet, abgelehnt oder vorverarbeitet werden.
Die Verteilungsstrategie wird bei der Initialisierung des Austauschpunkts festgelegt.
. Neben der Lastverteilung ermöglicht Messaging das Taggen von Entitäten. Lassen Sie uns die Arten von Tags im System betrachten:
- Verbindungstag. Erlaubt es zu verstehen, über welche Verbindung die Ereignisse eingegangen sind. Wird verwendet, wenn der Controller-Prozess sich mit einem Austauschpunkt verbindet, jedoch mit verschiedenen Routing-Schlüsseln.
- Dienstetag. Ermöglicht es, Handler für einen Dienst in Gruppen zu bündeln und die Routing- und Lastverteilungsfähigkeiten zu erweitern. Für das req-resp-Muster ist das Routing linear. Wir senden eine Anfrage an den Austauschpunkt, der sie dann an den Dienst weiterleitet. Aber wenn wir die Handler in logische Gruppen aufteilen müssen, erfolgt die Aufteilung mit Hilfe von Tags. Bei Angabe eines Tags wird die Anfrage an eine bestimmte Gruppe von Controllern weitergeleitet.
- Anfragetag. Ermöglicht es, Antworten zu unterscheiden. Da unser System asynchron ist, muss beim Verarbeiten der Antworten des Dienstes die Möglichkeit bestehen, einen RequestTag beim Senden der Anfrage anzugeben. Anhand dieses Tags können wir verstehen, auf welche Anfrage unsere Antwort eingegangen ist.
Pub-sub
Für pub-sub ist alles etwas einfacher. Wir haben einen Austauschpunkt, an dem Nachrichten veröffentlicht werden. Der Austauschpunkt verteilt die Nachrichten an die Abonnenten, die sich für die gewünschten Routing-Schlüssel angemeldet haben (man kann sagen, dass dies das Pendant zu Themen ist).
Skalierbarkeit und Fehlertoleranz
Die Skalierbarkeit des Systems insgesamt hängt vom Grad der Skalierbarkeit der Schichten und Komponenten des Systems ab:
- Die Dienste skalieren, indem zusätzliche Knoten mit den Prozessoren dieses Dienstes zum Cluster hinzugefügt werden. Während des praktischen Betriebs kann eine optimale Lastverteilungsstrategie ausgewählt werden.
- Der Messaging-Dienst selbst lässt sich im Rahmen eines separaten Clusters im Allgemeinen entweder durch Verlagerung von stark beanspruchten Austauschpunkten auf separate Knoten des Clusters oder durch Hinzufügen von Proxy-Prozessen in besonders belastete Zonen des Clusters skalieren.
- Die Skalierbarkeit des gesamten Systems als Merkmal hängt von der Flexibilität der Architektur und der Möglichkeit ab, einzelne Cluster zu einer gemeinsamen logischen Einheit zusammenzufassen.
Die Einfachheit und Geschwindigkeit der Skalierung bestimmen oft den Erfolg eines Projekts. Messaging in der aktuellen Ausführung wächst mit der Anwendung. Selbst wenn wir nicht genug Cluster mit 50-60 Maschinen haben, können wir auf Föderation zurückgreifen. Leider geht das Thema Föderation über den Rahmen dieses Artikels hinaus.
Reservierung
Bei der Diskussion zur Lastverteilung haben wir bereits die Redundanz der Dienstcontroller angesprochen. Allerdings sollte auch der Messaging-Dienst redundant sein. Im Falle eines Ausfalls eines Knotens oder einer Maschine muss Messaging automatisch wiederhergestellt werden, und zwar in kürzester Zeit.
In meinen Projekten verwende ich zusätzliche Knoten, die die Last im Falle eines Ausfalls übernehmen. In Erlang gibt es eine Standardimplementierung des verteilten Modus für OTP-Anwendungen. Der verteilte Modus führt die Wiederherstellung im Falle eines Fehlers durch, indem die ausgefallene Anwendung auf einem anderen zuvor gestarteten Knoten ausgeführt wird. Der Prozess ist transparent, nach einem Ausfall wechselt die Anwendung automatisch auf den Failover-Knoten. Weitere Informationen zu dieser Funktion finden Sie hier. .
Leistung
Lassen Sie uns zumindest annähernd die Leistung von RabbitMQ und unserem benutzerdefinierten Messaging vergleichen.
Ich habe gefunden der RabbitMQ-Testreihe vom OpenStack-Team.
Im Punkt 6.14.1.2.1.2.2 des Originals wird das Ergebnis von RPC CAST angegeben:

Vorläufig werden keine zusätzlichen Einstellungen im Betriebssystem oder der Erlang-VM vorgenommen. Die Bedingungen für das Testen sind:
- erl opts: +A1 +sbtu.
- Der Test auf einem einzelnen Erlang-Knoten wird auf einem Laptop mit einem älteren i7 im mobilen Format durchgeführt.
- Die Cluster-Tests werden auf Servern mit einem 10G-Netzwerk durchgeführt.
- Der Code läuft in Docker-Containern. Netzwerk im NAT-Modus.
Testcode:
req_resp_bench(_) ->
W = perftest:comprehensive(10000,
fun() ->
messaging:request(?EXCHANGE, default, ping, self()),
receive
#'$msg'{message = pong} -> ok
after 5000 ->
throw(timeout)
end
end
),
true = lists:any(fun(E) -> E >= 30000 end, W),
ok.Szenario 1: Der Test wird auf einem Laptop mit einem älteren i7-Mobilprozessor durchgeführt. Test, Messaging und Service laufen auf demselben Knoten in demselben Docker-Container:
Sequentiell 10000 Zyklen in ~0 Sekunden (26987 Zyklen/s)
Sequentiell 20000 Zyklen in ~1 Sekunden (26915 Zyklen/s)
Sequentiell 100000 Zyklen in ~4 Sekunden (26957 Zyklen/s)
Parallel 2 100000 Zyklen in ~2 Sekunden (44240 Zyklen/s)
Parallel 4 100000 Zyklen in ~2 Sekunden (53459 Zyklen/s)
Parallel 10 100000 Zyklen in ~2 Sekunden (52283 Zyklen/s)
Parallel 100 100000 Zyklen in ~3 Sekunden (49317 Zyklen/s)Szenario 2: 3 Knoten, die auf verschiedenen Maschinen unter Docker (NAT) laufen.
Sequentiell 10000 Zyklen in ~1 Sekunden (8684 Zyklen/s)
Sequentiell 20000 Zyklen in ~2 Sekunden (8424 Zyklen/s)
Sequentiell 100000 Zyklen in ~12 Sekunden (8655 Zyklen/s)
Parallel 2 100000 Zyklen in ~7 Sekunden (15160 Zyklen/s)
Parallel 4 100000 Zyklen in ~5 Sekunden (19133 Zyklen/s)
Parallel 10 100000 Zyklen in ~4 Sekunden (24399 Zyklen/s)
Parallel 100 100000 Zyklen in ~3 Sekunden (34517 Zyklen/s)In allen Fällen überschritt die CPU-Auslastung nicht 250%
Ergebnisse
Ich hoffe, dieser Zyklus sieht nicht wie ein Bewusstseinsdump aus und dass meine Erfahrungen sowohl den Forschern verteilter Systeme als auch den Praktikern, die am Anfang ihrer Reise stehen, um verteilte Architekturen für ihre Geschäftssysteme zu schaffen und die fasziniert auf Erlang/Elixir schauen, aber unsicher sind, ob es sich lohnt…
Foto
Nur registrierte Benutzer können an der Umfrage teilnehmen. .
Welche Themen sollte ich im Rahmen des Zyklus „Experiment VTrade“ ausführlich behandeln?
Theorie: Märkte, Aufträge und deren Gültigkeitsdauer: TAG, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC
Orderbuch. Theorie und Praxis der Implementierung eines Buches mit Gruppierungen
Visualisierung des Handels: Ticks, Balken, Auflösungen. Wie man speichert und wie man verbindet
Backoffice. Planung und Entwicklung. Mitarbeiterüberwachung und Vorfalluntersuchung
API. Lassen Sie uns herausfinden, welche Schnittstellen benötigt werden und wie sie implementiert werden können
Datenhaltung: PostgreSQL, Timescale, Tarantool in Handelssystemen
Reaktivität in Handelssystemen
Sonstiges. Ich werde es in den Kommentaren schreiben
6 Benutzer haben abgestimmt. 4 Benutzer haben sich enthalten.
Quelle: habr.com
