Bausteine ​​verteilter Anwendungen. Zweite Näherung

Ankündigung

Kolleginnen und Kollegen, im Hochsommer plane ich die Veröffentlichung einer weiteren Artikelserie zum Design von Warteschlangensystemen: „Das VTrade-Experiment“ – ein Versuch, ein Framework für Handelssysteme zu schreiben. In der Reihe werden Theorie und Praxis des Aufbaus einer Börse, einer Auktion und eines Ladens untersucht. Am Ende des Artikels lade ich Sie ein, für die Themen zu stimmen, die Sie am meisten interessieren.

Bausteine ​​verteilter Anwendungen. Zweite Näherung

Dies ist der letzte Artikel der Reihe über verteilte reaktive Anwendungen in Erlang/Elixir. IN erster Artikel finden Sie die theoretischen Grundlagen der reaktiven Architektur. Zweiter Artikel veranschaulicht die grundlegenden Muster und Mechanismen zum Aufbau solcher Systeme.

Heute werden wir Fragen zur Entwicklung der Codebasis und von Projekten im Allgemeinen ansprechen.

Organisation von Dienstleistungen

Im wirklichen Leben muss man bei der Entwicklung eines Dienstes oft mehrere Interaktionsmuster in einem Controller kombinieren. Beispielsweise muss der Benutzerdienst, der das Problem der Verwaltung von Projektbenutzerprofilen löst, auf Req-Resp-Anfragen reagieren und Profilaktualisierungen über Pub-Sub melden. Dieser Fall ist ganz einfach: Hinter dem Messaging steckt ein Controller, der die Servicelogik implementiert und Updates veröffentlicht.

Komplizierter wird die Situation, wenn wir einen fehlertoleranten verteilten Dienst implementieren müssen. Stellen wir uns vor, dass sich die Anforderungen an Benutzer geändert haben:

  1. Jetzt sollte der Dienst Anfragen auf 5 Clusterknoten verarbeiten.
  2. in der Lage sein, Hintergrundverarbeitungsaufgaben auszuführen,
  3. Außerdem können Sie Abonnementlisten für Profilaktualisierungen dynamisch verwalten.

Hinweis: Das Problem der konsistenten Speicherung und Datenreplikation berücksichtigen wir nicht. Nehmen wir an, dass diese Probleme früher gelöst wurden und das System bereits über eine zuverlässige und skalierbare Speicherschicht verfügt und Handler über Mechanismen verfügen, um damit zu interagieren.

Die formale Beschreibung des Benutzerdienstes ist komplizierter geworden. Aus Sicht eines Programmierers sind die Änderungen aufgrund der Verwendung von Messaging minimal. Um die erste Anforderung zu erfüllen, müssen wir den Ausgleich am req-resp-Austauschpunkt konfigurieren.

Die Anforderung, Hintergrundaufgaben zu bearbeiten, kommt häufig vor. Bei Benutzern kann dies beispielsweise die Überprüfung von Benutzerdokumenten, die Verarbeitung heruntergeladener Multimediainhalte oder die Synchronisierung von Daten mit sozialen Medien sein. Netzwerke. Diese Aufgaben müssen irgendwie innerhalb des Clusters verteilt und der Fortschritt der Ausführung überwacht werden. Daher haben wir zwei Lösungsmöglichkeiten: entweder die Aufgabenverteilungsvorlage aus dem vorherigen Artikel verwenden oder, falls sie nicht passt, einen benutzerdefinierten Aufgabenplaner schreiben, der den Prozessorpool auf die von uns benötigte Weise verwaltet.

Punkt 3 erfordert die Pub-Sub-Vorlagenerweiterung. Und für die Implementierung müssen wir nach der Erstellung eines Pub-Sub-Austauschpunkts zusätzlich den Controller dieses Punktes in unserem Dienst starten. Es ist also so, als würden wir die Logik zur Verarbeitung von Abonnements und Abmeldungen von der Messaging-Ebene in die Implementierung von Benutzern verlagern.

Als Ergebnis zeigte die Zerlegung des Problems, dass wir zur Erfüllung der Anforderungen fünf Instanzen des Dienstes auf verschiedenen Knoten starten und eine zusätzliche Entität erstellen müssen – einen Pub-Sub-Controller, der für das Abonnement verantwortlich ist.
Um 5 Handler auszuführen, müssen Sie den Dienstcode nicht ändern. Die einzige zusätzliche Aktion ist die Einrichtung von Ausgleichsregeln am Austauschpunkt, worüber wir etwas später sprechen werden.
Hinzu kommt eine zusätzliche Komplexität: Der Pub-Sub-Controller und der benutzerdefinierte Taskplaner müssen in einer einzigen Kopie funktionieren. Auch hier muss der Messaging-Dienst als grundlegender Dienst einen Mechanismus zur Auswahl eines Leiters bereitstellen.

Wahl des Anführers

In verteilten Systemen ist die Leiterwahl 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, werden universelle und konsensbasierte Algorithmen wie Paxos oder Raft verwendet.
Da Messaging ein Vermittler und ein zentrales Element ist, kennt es alle Service-Controller – Kandidatenleiter. Messaging kann einen Leiter ernennen, ohne abzustimmen.

Nach dem Start und der Verbindung zum Exchange Point erhalten alle Dienste eine Systemmeldung #'$leader'{exchange = ?EXCHANGE, pid = LeaderPid, servers = Servers}. Wenn LeaderPid passt zu pid Im aktuellen Prozess wird er zum Leiter und zur Liste ernannt Servers umfasst alle Knoten und ihre Parameter.
In dem Moment, in dem ein neuer Knoten erscheint und die Verbindung zu einem funktionierenden Clusterknoten getrennt wird, empfangen alle Dienstcontroller #'$slave_up'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} и #'$slave_down'{exchange = ?EXCHANGE, pid = SlavePid, options = SlaveOpts} jeweils.

Auf diese Weise sind alle Komponenten über alle Änderungen informiert und der Cluster hat garantiert immer einen Anführer.

Vermittler

Zur Umsetzung komplexer verteilter Verarbeitungsprozesse sowie bei Problemen der Optimierung einer bestehenden Architektur bietet sich der Einsatz von Intermediären an.
Um den Dienstcode nicht zu ändern und beispielsweise Probleme der zusätzlichen Verarbeitung, Weiterleitung oder Protokollierung von Nachrichten zu lösen, können Sie vor dem Dienst einen Proxy-Handler aktivieren, der alle zusätzlichen Arbeiten erledigt.

Ein klassisches Beispiel für die Pub-Sub-Optimierung ist eine verteilte Anwendung mit einem Geschäftskern, der Aktualisierungsereignisse wie Preisänderungen auf dem Markt generiert, und einer Zugriffsschicht – N-Servern, die eine Websocket-API für Web-Clients bereitstellen.
Wenn Sie sich direkt entscheiden, sieht der Kundenservice so aus:

  • Der Client stellt Verbindungen mit der Plattform her. Auf der Seite des Servers, der den Datenverkehr beendet, wird ein Prozess gestartet, um diese Verbindung zu bedienen.
  • Im Rahmen des Serviceprozesses erfolgt die Autorisierung und das Abonnement von Updates. Der Prozess ruft die subscribe-Methode für Themen auf.
  • Sobald ein Ereignis im Kernel generiert wird, wird es an die Prozesse übermittelt, die die Verbindungen bedienen.

Stellen wir uns vor, wir haben 50000 Abonnenten für das Thema „News“. Die Abonnenten werden gleichmäßig auf 5 Server verteilt. Dadurch wird jedes Update, das am Austauschpunkt ankommt, 50000 Mal repliziert: 10000 Mal auf jedem Server, abhängig von der Anzahl der darauf befindlichen Abonnenten. Kein sehr effektives Schema, oder?
Um die Situation zu verbessern, führen wir einen Proxy ein, der denselben Namen wie der Austauschpunkt hat. Der globale Namensregistrar muss in der Lage sein, den nächstgelegenen Prozess namentlich zurückzugeben. Dies ist wichtig.

Starten wir diesen Proxy auf den Access-Layer-Servern, und alle unsere Prozesse, die die Websocket-API bedienen, abonnieren ihn und nicht den ursprünglichen Pub-Sub-Austauschpunkt im Kernel. Der Proxy abonniert den Kern nur im Falle eines eindeutigen Abonnements und repliziert die eingehende Nachricht an alle seine Abonnenten.
Infolgedessen werden 5 statt 50000 Nachrichten zwischen dem Kernel und den Zugriffsservern gesendet.

Routing und Balancing

Req-Resp

In der aktuellen Messaging-Implementierung gibt es 7 Strategien zur Anforderungsverteilung:

  • default. Die Anfrage wird an alle Controller gesendet.
  • round-robin. Anfragen werden gezählt und zyklisch zwischen den Controllern verteilt.
  • consensus. Die Kontrolleure, die den Dienst erfüllen, sind in Anführer und Sklaven unterteilt. Anfragen werden nur an den Leiter gesendet.
  • consensus & round-robin. Die Gruppe hat einen Leiter, die Anfragen werden jedoch an alle Mitglieder verteilt.
  • sticky. Die Hash-Funktion wird berechnet und einem bestimmten Handler zugewiesen. Nachfolgende Anfragen mit dieser Signatur gehen an denselben Handler.
  • sticky-fun. Bei der Initialisierung des Austauschpunkts wird die Hash-Berechnungsfunktion für sticky ausgleichend.
  • fun. Ähnlich wie bei Sticky-Fun können Sie es jedoch zusätzlich umleiten, ablehnen oder vorbearbeiten.

Die Verteilungsstrategie wird bei der Initialisierung des Austauschpunkts festgelegt.

Zusätzlich zum Ausgleich ermöglicht Ihnen die Nachrichtenübermittlung das Markieren von Entitäten. Schauen wir uns die Arten von Tags im System an:

  • Verbindungstag. Ermöglicht Ihnen zu verstehen, über welche Verbindung die Ereignisse zustande kamen. Wird verwendet, wenn ein Controller-Prozess eine Verbindung zum gleichen Austauschpunkt herstellt, jedoch mit unterschiedlichen Routing-Schlüsseln.
  • Service-Tag. Ermöglicht Ihnen, Handler in Gruppen für einen Dienst zusammenzufassen und Routing- und Balancing-Funktionen 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. Wenn wir die Handler jedoch in logische Gruppen aufteilen müssen, erfolgt die Aufteilung mithilfe von Tags. Bei der Angabe eines Tags wird die Anfrage an eine bestimmte Gruppe von Controllern gesendet.
  • Tag anfordern. Ermöglicht die Unterscheidung zwischen Antworten. Da unser System asynchron ist, müssen wir zum Verarbeiten von Serviceantworten in der Lage sein, beim Senden einer Anfrage ein RequestTag anzugeben. Daraus können wir die Antwort nachvollziehen, auf welche Anfrage wir gekommen sind.

Pub-sub

Bei Pub-Sub ist alles etwas einfacher. Wir haben einen Austauschpunkt, an dem Nachrichten veröffentlicht werden. Der Austauschpunkt verteilt Nachrichten an Abonnenten, die die von ihnen benötigten Routing-Schlüssel abonniert haben (wir können sagen, dass dies analog zu Themen ist).

Skalierbarkeit und Fehlertoleranz

Die Skalierbarkeit des Gesamtsystems hängt vom Skalierbarkeitsgrad der Schichten und Komponenten des Systems ab:

  • Dienste werden skaliert, indem dem Cluster zusätzliche Knoten mit Handlern für diesen Dienst hinzugefügt werden. Im Probebetrieb können Sie die optimale Auswuchtpolitik wählen.
  • Der Messaging-Dienst selbst innerhalb eines separaten Clusters wird im Allgemeinen entweder durch die Verlagerung besonders belasteter Austauschpunkte auf separate Cluster-Knoten oder durch das Hinzufügen von Proxy-Prozessen zu besonders belasteten Bereichen des Clusters skaliert.
  • Die Skalierbarkeit des Gesamtsystems als Merkmal hängt von der Flexibilität der Architektur und der Fähigkeit ab, einzelne Cluster zu einer gemeinsamen logischen Einheit zusammenzufassen.

Der Erfolg eines Projekts hängt oft von der Einfachheit und Geschwindigkeit der Skalierung ab. Messaging wächst in der aktuellen Version mit der Anwendung. Selbst wenn uns ein Cluster von 50–60 Maschinen fehlt, können wir auf eine Föderation zurückgreifen. Leider geht das Thema Föderation über den Rahmen dieses Artikels hinaus.

Reservierung

Bei der Analyse des Lastausgleichs haben wir bereits die Redundanz von Service-Controllern besprochen. Allerdings muss auch die Nachrichtenübermittlung zurückhaltend sein. Im Falle eines Knoten- oder Maschinenabsturzes sollte die Nachrichtenübermittlung automatisch und in kürzester Zeit wiederhergestellt werden.

In meinen Projekten verwende ich zusätzliche Knoten, die im Falle eines Sturzes die Last aufnehmen. Erlang verfügt über eine Standardimplementierung im verteilten Modus für OTP-Anwendungen. Der verteilte Modus führt im Fehlerfall eine Wiederherstellung durch, indem die ausgefallene Anwendung auf einem anderen zuvor gestarteten Knoten gestartet wird. Der Prozess ist transparent; nach einem Fehler wechselt die Anwendung automatisch zum Failover-Knoten. Weitere Informationen zu dieser Funktionalität finden Sie hier hier.

Leistung

Versuchen wir, die Leistung von Rabbitmq und unserem benutzerdefinierten Messaging zumindest grob zu vergleichen.
Ich fand offizielle Ergebnisse RabbitMQ-Tests vom OpenStack-Team.

In Absatz 6.14.1.2.1.2.2. Das Originaldokument zeigt das Ergebnis des RPC CAST:
Bausteine ​​verteilter Anwendungen. Zweite Näherung

Wir werden vorab keine weiteren Einstellungen am OS-Kernel oder der Erlang-VM vornehmen. Bedingungen für die Prüfung:

  • erl wählt: +A1 +sbtu.
  • Der Test innerhalb eines einzelnen Erlang-Knotens wird auf einem Laptop mit einem alten i7 in der mobilen Version ausgeführt.
  • Clustertests 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.

1-Skript: Der Test wird auf einem Laptop mit einer alten i7-Mobilversion durchgeführt. Der Test, die Nachrichtenübermittlung und der Dienst werden auf einem Knoten in einem Docker-Container ausgeführt:

Sequential 10000 cycles in ~0 seconds (26987 cycles/s)
Sequential 20000 cycles in ~1 seconds (26915 cycles/s)
Sequential 100000 cycles in ~4 seconds (26957 cycles/s)
Parallel 2 100000 cycles in ~2 seconds (44240 cycles/s)
Parallel 4 100000 cycles in ~2 seconds (53459 cycles/s)
Parallel 10 100000 cycles in ~2 seconds (52283 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (49317 cycles/s)

2-Skript: 3 Knoten, die auf verschiedenen Maschinen unter Docker (NAT) laufen.

Sequential 10000 cycles in ~1 seconds (8684 cycles/s)
Sequential 20000 cycles in ~2 seconds (8424 cycles/s)
Sequential 100000 cycles in ~12 seconds (8655 cycles/s)
Parallel 2 100000 cycles in ~7 seconds (15160 cycles/s)
Parallel 4 100000 cycles in ~5 seconds (19133 cycles/s)
Parallel 10 100000 cycles in ~4 seconds (24399 cycles/s)
Parallel 100 100000 cycles in ~3 seconds (34517 cycles/s)

In allen Fällen überschritt die CPU-Auslastung nicht 250 %.

Ergebnisse

Ich hoffe, dass dieser Zyklus nicht wie ein Gedanken-Dump aussieht und meine Erfahrung sowohl für Forscher verteilter Systeme als auch für Praktiker, die ganz am Anfang des Aufbaus verteilter Architekturen für ihre Geschäftssysteme stehen und Erlang/Elixir mit Interesse betrachten, von echtem Nutzen sein wird , aber ich habe Zweifel, ob es sich lohnt...

Foto @chuttersnap

An der Umfrage können nur registrierte Benutzer teilnehmen. Einloggenbitte.

Welche Themen sollte ich im Rahmen der VTrade Experiment-Reihe ausführlicher behandeln?

  • Theorie: Märkte, Aufträge und ihr Timing: DAY, GTD, GTC, IOC, FOK, MOO, MOC, LOO, LOC

  • Buch der Bestellungen. Theorie und Praxis der Umsetzung eines Buches mit Gruppierungen

  • Visualisierung des Handels: Ticks, Balken, Auflösungen. Aufbewahrung und Kleben

  • Backoffice. Planung und Entwicklung. Mitarbeiterüberwachung und Untersuchung von Vorfällen

  • API. Lassen Sie uns herausfinden, welche Schnittstellen benötigt werden und wie man sie implementiert

  • Informationsspeicherung: PostgreSQL, Timescale, Tarantool in Handelssystemen

  • Reaktivität in Handelssystemen

  • Andere. Ich schreibe es in die Kommentare

6 Benutzer haben abgestimmt. 4 Benutzer enthielten sich der Stimme.

Source: habr.com

Kommentar hinzufügen