Bausteine ​​verteilter Anwendungen. Zweite Annäherung

Ankündigung

Liebe Kollegen, in der Mitte des Sommers plane ich die Veröffentlichung eines weiteren Zyklus von Artikeln zur Gestaltung von Massensystemen: "Experiment VTrade" – ein Versuch, ein Framework für Handelssysteme zu entwickeln. Der Zyklus wird die Theorie und Praxis des Aufbaus von Börsen, Auktionen und Geschäften behandeln. Am Ende des Artikels lade ich Sie ein, für die Themen zu stimmen, die Sie am meisten interessieren.

Bausteine ​​verteilter Anwendungen. Zweite Annäherung

Dies ist der Abschlussartikel des Zyklus über verteilte reaktive Anwendungen in Erlang/Elixir. In im ersten Artikel finden Sie die theoretischen Grundlagen der reaktiven Architektur. Der zweite Artikel veranschaulicht die grundlegenden Muster und Mechanismen zur Erstellung solcher Systeme.

Heute werden wir die Entwicklung der Codebasis und der Projekte insgesamt ansprechen.

Organisation von Services

In der realen Welt müssen beim Entwickeln eines Services oft mehrere Interaktionsmuster in einem Controller vereint werden. Zum Beispiel muss der Benutzerservice, der für die Verwaltung der Benutzerprofile im Projekt zuständig ist, sowohl auf requ-resp-Anfragen reagieren als auch über Aktualisierungen der Profile über pub-sub informieren. Dieser Fall ist relativ einfach: der Messaging-Bereich wird von einem Controller abgedeckt, der die Logik des Services implementiert und Updates veröffentlicht.

Die Situation wird komplexer, wenn wir einen hochverfügbaren verteilten Dienst implementieren müssen. Nehmen wir an, die Anforderungen an die Benutzer haben sich geändert:

  1. Jetzt muss der Dienst Anfragen auf 5 Knoten des Clusters verarbeiten,
  2. die Fähigkeit zur Ausführung von Hintergrundverarbeitungsaufgaben haben,
  3. und außerdem in der Lage sein, dynamisch Abonnent-Listen für Profilaktualisierungen zu verwalten.

Hinweis: Die Frage der konsistenten Speicherung und Replikation von Daten betrachten wir nicht. Angenommen, diese Probleme wurden zuvor gelöst und es gibt bereits eine zuverlässige und skalierbare Speicherschicht im System, während die Verarbeiter Mechanismen zur Interaktion mit ihr haben.

Die formale Beschreibung des Benutzer-Dienstes hat sich verkompliziert. Aus Sicht der Programmierung bleiben die Änderungen dank der Verwendung von Messaging minimal. Um die erste Anforderung zu erfüllen, müssen wir das Load-Balancing am Austauschpunkt für Anfragen und Antworten einrichten.

Die Anforderung zur Bearbeitung von Hintergrundaufgaben tritt häufig auf. Bei den Nutzern können dies die Überprüfung von Dokumenten, die Verarbeitung hochgeladener Medien oder die Synchronisierung von Daten mit sozialen Netzwerken sein. Diese Aufgaben müssen innerhalb des Clusters verteilt und überwacht werden. Daher haben wir zwei Lösungsansätze: entweder das Aufgabenverteilungsschema aus dem vorherigen Artikel verwenden oder, falls das nicht passt, einen benutzerdefinierten Task-Planer schreiben, der die Handler entsprechend unseren Bedürfnissen steuert.

Der Punkt 3 erfordert eine Erweiterung des Pub-Sub-Schemas. Zur Umsetzung müssen wir, nachdem wir den Pub-Sub-Austauschpunkt erstellt haben, zusätzlich den Controller dieses Punktes im Rahmen unseres Services starten. So extrahieren wir die Logik für die Verwaltung von Abonnements und Abmeldungen aus der Messaging-Ebene in die Implementierung der Nutzer.

Die Dekomposition der Aufgabe hat gezeigt, dass wir zur Erfüllung der Anforderungen 5 Instanzen des Services auf verschiedenen Knoten starten und eine zusätzliche Entität – einen Pub-Sub-Controller – schaffen müssen, der für die Abonnements verantwortlich ist.
Für den Start von 5 Handlern ist keine Änderung am Dienstcode erforderlich. Die einzige zusätzliche Maßnahme besteht in der Konfiguration der Lastenausgleichsregeln an der Austauschstelle, worüber wir später sprechen werden.
Es gibt auch eine zusätzliche Komplexität: Der Pub-Sub-Controller und der benutzerdefinierte Aufgabenplaner müssen in einer einzigen Instanz arbeiten. Der Messaging-Dienst, als grundlegendes Element, sollte einen Mechanismus zur Wahl des Führers bereitstellen.

Wahl des Führers

In verteilten Systemen ist die Wahl des Führers das Verfahren zur Ernennung eines einzelnen 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 zentrales Element ist, kennt es alle Dienstcontroller, die als Kandidaten für die Führerschaft fungieren. Messaging kann einen Führer ernennen, ohne eine Abstimmung durchzuführen.

Alle Dienste erhalten nach dem Start und der Verbindung zur Austauschstelle eine Systemnachricht #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Falls LeaderPid mit pid dem aktuellen Prozess übereinstimmt, wird er als Führer ernannt, und die Liste Servers beinhaltet alle Knoten und deren Parameter.
Bei der Einführung eines neuen und dem Ausschalten eines aktiven 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.

Somit wissen alle Komponenten über alle Änderungen Bescheid, und im Cluster gibt es jederzeit garantiert einen Leader.

Vermittler

Für die Umsetzung komplexer verteilter Verarbeitungsprozesse sowie bei der Optimierung vorhandener Architekturen ist es praktisch, Vermittler einzusetzen.
Um den Code der Dienste nicht zu ändern und beispielsweise zusätzliche Verarbeitung, Routing oder Logging von Nachrichten zu lösen, kann vor dem Service ein Proxy-Handler aktiviert werden, der alle zusätzlichen Arbeiten übernimmt.

Ein klassisches Beispiel für die Optimierung von Pub-Sub ist eine verteilte Anwendung mit einem Business-Core, der Ereignisse wie Preisänderungen auf dem Markt generiert, und einer Zugriffsschicht — N Servern, die ein WebSocket-API für Web-Clients bereitstellen.
Wenn man es „konkret“ angeht, sieht die Kundenbetreuung folgendermaßen aus:

  • Der Kunde stellt eine Verbindung zur Plattform her. Auf der Serverseite, die den Datenverkehr terminiert, wird ein Prozess gestartet, der diese Verbindung bedient.
  • Im Kontext des Dienstleistungsprozesses erfolgt die Authentifizierung und Anmeldung zu Aktualisierungen. 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 50.000 Abonnenten für das Thema 'News'. Die Abonnenten sind gleichmäßig auf 5 Server verteilt. Schließlich wird jedes Update, das am Austauschpunkt ankommt, 50.000 Mal repliziert: 10.000 Mal auf jedem Server, je nach Anzahl der Abonnenten darauf. Nicht gerade effizient, oder?
Um die Situation zu verbessern, führen wir einen Proxy ein, der denselben Namen wie der Austauschpunkt hat. Der globale Namensregistrator sollte in der Lage sein, den nächstgelegenen Prozess nach Namen zurückzugeben, was wichtig ist.

Wir werden diesen Proxy auf den Access-Servern starten, und alle unsere Prozesse, die das Websocket-API bedienen, werden sich darauf anmelden, anstatt auf den ursprünglichen Pub-Sub-Austauschpunkt im Kern. Der Proxy abonniert den Kern nur im Falle einer einzigartigen Anmeldung und repliziert die eingehende Nachricht an alle seine Abonnenten.
Insgesamt werden zwischen dem Kern und den Access-Servern 5 Nachrichten übermittelt, anstatt 50.000.

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 zyklische Verteilung der Anfragen zwischen den Controllern.
  • Konsens. Die Controller, die den Dienst betreuen, teilen sich in einen Führer und Nachfolger. Anfragen werden nur an den Führer weitergegeben.
  • Konsens & round-robin. In der Gruppe gibt es einen Führer, aber die Anfragen werden unter allen Mitgliedern verteilt.
  • sticky. Eine Hash-Funktion wird berechnet und einem bestimmten Handler zugewiesen. Folgende Anfragen mit diesem Signature landen bei demselben Handler.
  • sticky-fun. Bei der Initialisierung des Nachrichtenaustauschpunkts wird zusätzlich eine Hash-Berechnung Funktion übergeben für sticky das Load-Balancing.
  • . Bei. Ähnlich wie sticky-fun, jedoch kann er zusätzlich weitergeleitet, abgelehnt oder vorbearbeitet werden.

Die Verteilungsstrategie wird bei der Initialisierung des Nachrichtenaustauschpunkts festgelegt.

Neben dem Load-Balancing ermöglicht das Messaging das Tagging von Entitäten. Schauen wir uns die Arten von Tags im System an:

  • Verbindungstag. Ermöglicht zu verstehen, über welche Verbindung die Ereignisse eingegangen sind. Wird verwendet, wenn der Controller-Prozess mit einem einzigen Austauschpunkt, aber unterschiedlichen Routing-Schlüsseln verbunden ist.
  • Service-Tag. Er ermöglicht es, Handler für einen Dienst in Gruppen zusammenzufassen und die Routing- und Lastverteilungsfunktionen zu erweitern. Für das Req-Resp-Muster erfolgt das Routing linear. Wir senden die Anfrage an den Austauschpunkt, wo sie an den Dienst weitergegeben wird. Wenn wir jedoch die Handler in logische Gruppen aufteilen müssen, erfolgt die Aufteilung durch die Tags. Mit Angabe eines Tags wird die Anfrage an eine bestimmte Gruppe von Controllern geleitet.
  • Request-Tag. Es ermöglicht, Antworten zu unterscheiden. Da unser System asynchron ist, benötigen wir die Möglichkeit, einen RequestTag beim Senden einer Anfrage anzugeben, um die Antworten des Dienstes zu verarbeiten. Anhand dieses Tags können wir verstehen, auf welche Anfrage die Antwort bei uns eingetroffen 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 entsprechenden Routing-Schlüssel angemeldet haben (man könnte sagen, dass dies einer Art Thema entspricht).

Skalierbarkeit und Fehlertoleranz

Die Skalierbarkeit des Systems insgesamt hängt von der Skalierbarkeit der Schichten und Komponenten des Systems ab:

  • Die Dienste skalieren durch das Hinzufügen zusätzlicher Knoten mit den Handlern dieses Dienstes in den Cluster. Im Rahmen der praktischen Nutzung kann die optimale Lastverteilungspolitik ausgewählt werden.
  • Der Messaging-Dienst innerhalb eines separaten Clusters skaliert im Allgemeinen entweder durch Auslagerung stark belasteter Austauschpunkte auf separate Clusterknoten oder durch Hinzufügen von Proxy-Prozessen in besonders belastete Bereiche des Clusters.
  • Die Skalierbarkeit des gesamten Systems hängt als Merkmal von der Flexibilität der Architektur und der Möglichkeit ab, einzelne Cluster zu einer gemeinsamen logischen Einheit zusammenzuführen.

Der Erfolg eines Projekts hängt oft von der Einfachheit und Geschwindigkeit der Skalierung ab. Das Messaging in der aktuellen Ausführung wächst zusammen mit der Anwendung. Selbst wenn wir einen Cluster mit 50 bis 60 Maschinen benötigen, können wir auf Föderation zurückgreifen. Leider geht das Thema Föderation über den Rahmen dieses Artikels hinaus.

Redundanz

Bei der Analyse der Lastverteilung haben wir bereits die Redundanz der Service-Controller besprochen. Allerdings muss auch das Messaging redundant sein. Im Falle eines Ausfalls eines Knotens oder einer Maschine muss das Messaging automatisch wiederhergestellt werden, und zwar in kürzester Zeit.

In meinen Projekten nutze 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 ermöglicht es, nach einem Ausfall die abgestürzte Anwendung auf einem anderen bereits gestarteten Knoten wiederherzustellen. Der Prozess ist transparent; nach einem Ausfall wird die Anwendung automatisch auf den Failover-Knoten übertragen. Weitere Informationen zu dieser Funktionalität finden Sie hier. hier.

Leistung

Lassen Sie uns zumindest annähernd die Leistung von RabbitMQ und unserer maßgeschneiderten Messaging-Lösung vergleichen.
Ich habe gefunden offizielle Ergebnisse der RabbitMQ-Tests vom OpenStack-Team.

Im Abschnitt 6.14.1.2.1.2.2 des Originaldokuments wird das Ergebnis des RPC CAST dargestellt:
Bausteine ​​verteilter Anwendungen. Zweite Annäherung

Vorab werden wir keine weiteren Einstellungen im Betriebssystem-Kernel oder in der Erlang VM vornehmen. Testbedingungen:

  • erl opts: +A1 +sbtu.
  • Der Test innerhalb eines Erlang-Knotens wird auf einem Laptop mit einem älteren i7 im mobilen Format durchgeführt.
  • Die Cluster-Tests finden auf Servern mit 10G-Netzwerken statt.
  • Der Code läuft in Docker-Containern. Das Netzwerk ist 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 älteren i7 Laptop durchgeführt. Test, Messaging und Service laufen auf einem Knoten in einem Docker-Container:

Sequentielle 10000 Zyklen in ~0 Sekunden (26987 Zyklen/s)
Sequentielle 20000 Zyklen in ~1 Sekunden (26915 Zyklen/s)
Sequentielle 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 auf verschiedenen Maschinen unter Docker (NAT) gestartet.

Sequentielle 10000 Zyklen in ~1 Sekunden (8684 Zyklen/s)
Sequentielle 20000 Zyklen in ~2 Sekunden (8424 Zyklen/s)
Sequentielle 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 wirkt nicht wie ein Bewusstseinsdump und meine Erfahrungen werden sowohl den Forschern verteilter Systeme als auch den Praktikern, die am Anfang des Aufbaus verteilter Architekturen für ihre Geschäftssysteme stehen und interessiert auf Erlang/Elixir schauen, aber unsicher sind, ob es sich lohnt, echten Nutzen bringen.

Foto @chuttersnap

Nur registrierte Benutzer können an der Umfrage teilnehmen. Bitte melden Sie sich an.Sind Sie an Contour interessiert?

Welche Themen sollte ich im Rahmen des Zyklus „Experiment VTrade“ am ausführlichsten behandeln?

  • Theorie: Märkte, Aufträge und deren Gültigkeitsdauer: DAY, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Orderbuch. Theorie und Praxis der Umsetzung eines Buchs mit Gruppierungen.

  • Visualisierung des Handels: Ticks, Balken, Auflösungen. Wie speichern und zusammenfügen?

  • Backoffice. Planung und Entwicklung. Mitarbeiterkontrolle und Vorfalluntersuchung.

  • API. Wir klären, welche Schnittstellen benötigt werden und wie man sie umsetzt.

  • Informationsspeicherung: PostgreSQL, Timescale, Tarantool in Handelssystemen.

  • Reaktivität in Handelssystemen.

  • Sonstiges. Ich werde in den Kommentaren schreiben.

6 Benutzer haben abgestimmt. 4 Benutzer haben sich enthalten.

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster