[Übersetzung] Envoy-Threading-Modell

Artikelübersetzung: Envoy-Threading-Modell – https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Ich fand diesen Artikel sehr interessant, und da Envoy am häufigsten als Teil von „istio“ oder einfach als „Ingress-Controller“ von Kubernetes verwendet wird, haben die meisten Leute nicht die gleiche direkte Interaktion damit wie beispielsweise mit „typisch“. Nginx- oder Haproxy-Installationen. Wenn jedoch etwas kaputt geht, wäre es gut zu verstehen, wie es von innen funktioniert. Ich habe versucht, so viel wie möglich vom Text ins Russische zu übersetzen, einschließlich spezieller Wörter; für diejenigen, denen das Anschauen weh tut, habe ich die Originale in Klammern gelassen. Willkommen bei Katze.

Die technische Dokumentation auf niedriger Ebene für die Envoy-Codebasis ist derzeit recht spärlich. Um hier Abhilfe zu schaffen, plane ich eine Reihe von Blogbeiträgen über die verschiedenen Subsysteme von Envoy. Da dies der erste Artikel ist, lassen Sie mich bitte wissen, was Sie denken und was Sie in zukünftigen Artikeln interessieren könnte.

Eine der häufigsten technischen Fragen, die ich zu Envoy erhalte, ist die Bitte um eine detaillierte Beschreibung des verwendeten Threading-Modells. In diesem Beitrag beschreibe ich, wie Envoy Verbindungen Threads zuordnet und welches Thread-Local-Storage-System es intern verwendet, um den Code paralleler und leistungsfähiger zu machen.

Threading-Übersicht

[Übersetzung] Envoy-Threading-Modell

Envoy verwendet drei verschiedene Arten von Streams:

  • Hauptsächlich: Dieser Thread verwaltet den Start und das Beenden von Prozessen, die gesamte XDS (xDiscovery Service) API-Verarbeitung, einschließlich DNS, Integritätsprüfung, allgemeines Cluster- und Laufzeitmanagement, Zurücksetzen der Statistiken, Administration und allgemeines Prozessmanagement. Linux Signale, Neustarts im laufenden Betrieb usw. Alles, was in diesem Thread geschieht, ist asynchron und nicht blockierend. Insgesamt koordiniert der Hauptthread alle kritischen Prozesse, die keine hohe CPU-Leistung benötigen. Dadurch kann der Großteil des Steuerungscodes so geschrieben werden, als wäre er Single-Threaded.
  • Arbeiter: Standardmäßig erstellt Envoy für jeden Hardware-Thread im System einen Worker-Thread, dies kann über die Option gesteuert werden --concurrency. Jeder Arbeitsthread führt eine „nicht blockierende“ Ereignisschleife aus, die für das Abhören jedes Listeners verantwortlich ist. Zum Zeitpunkt des Schreibens (29. Juli 2017) gab es kein Sharding des Listeners, das Akzeptieren neuer Verbindungen und das Instanziieren eines Filterstapels für der Verbindung und die Verarbeitung aller Ein-/Ausgabevorgänge (IO) während der Lebensdauer der Verbindung. Auch hierdurch können die meisten Verbindungsverarbeitungscodes so geschrieben werden, als ob sie Single-Threaded wären.
  • Datei-Flusher: Jede Datei, die Envoy schreibt, hauptsächlich Zugriffsprotokolle, verfügt derzeit über einen unabhängigen Blockierungsthread. Dies liegt daran, dass das Schreiben in Dateien auch bei der Verwendung vom Dateisystem zwischengespeichert wird O_NONBLOCK kann manchmal blockiert werden (seufz). Wenn Arbeitsthreads in eine Datei schreiben müssen, werden die Daten tatsächlich in einen Puffer im Speicher verschoben, wo sie schließlich durch den Thread geleert werden Datei leeren. Dies ist ein Codebereich, in dem technisch gesehen alle Arbeitsthreads dieselbe Sperre blockieren können, während sie versuchen, einen Speicherpuffer zu füllen.

Verbindungshandling

Wie oben kurz erläutert, hören alle Arbeitsthreads ohne Sharding auf alle Listener. Somit wird der Kernel verwendet, um akzeptierte Sockets ordnungsgemäß an Arbeitsthreads zu senden. Moderne Kernel sind darin im Allgemeinen sehr gut. Sie verwenden Funktionen wie die Erhöhung der Eingabe-/Ausgabepriorität (IO), um zu versuchen, einen Thread mit Arbeit zu füllen, bevor sie andere Threads verwenden, die ebenfalls auf demselben Socket lauschen, und verwenden auch kein Round-Robin Sperren (Spinlock), um jede Anfrage zu verarbeiten.
Sobald eine Verbindung in einem Arbeitsthread akzeptiert wird, verlässt sie diesen Thread nie. Die gesamte weitere Verarbeitung der Verbindung wird vollständig im Worker-Thread abgewickelt, einschließlich etwaigem Weiterleitungsverhalten.

Dies hat mehrere wichtige Konsequenzen:

  • Alle Verbindungspools in Envoy werden einem Arbeitsthread zugewiesen. Obwohl HTTP/2-Verbindungspools jeweils nur eine Verbindung zu jedem Upstream-Host herstellen, gibt es bei vier Arbeitsthreads im stabilen Zustand vier HTTP/2-Verbindungen pro Upstream-Host.
  • Der Grund dafür, dass Envoy auf diese Weise funktioniert, besteht darin, dass fast der gesamte Code ohne Blockierung und so geschrieben werden kann, als wäre er Single-Thread, indem alles in einem einzigen Arbeitsthread gehalten wird. Dieses Design erleichtert das Schreiben einer großen Menge Code und lässt sich unglaublich gut auf eine nahezu unbegrenzte Anzahl von Arbeitsthreads skalieren.
  • Eine der wichtigsten Erkenntnisse ist jedoch, dass es aus Sicht des Speicherpools und der Verbindungseffizienz tatsächlich sehr wichtig ist, den zu konfigurieren --concurrency. Wenn mehr Arbeitsthreads als nötig vorhanden sind, wird Speicher verschwendet, es entstehen mehr Verbindungen im Leerlauf und die Verbindungspooling-Rate wird verringert. Bei Lyft laufen unsere Envoy-Sidecar-Container mit sehr geringer Parallelität, sodass die Leistung in etwa der Leistung der Dienste entspricht, neben denen sie sitzen. Wir führen Envoy als Edge-Proxy nur bei maximaler Parallelität aus.

Was bedeutet nicht blockierend?

Der Begriff „nicht blockierend“ wurde bisher mehrfach verwendet, wenn es um die Funktionsweise von Haupt- und Arbeitsthreads ging. Der gesamte Code wird unter der Annahme geschrieben, dass niemals etwas blockiert wird. Dies ist jedoch nicht ganz richtig (was ist nicht ganz richtig?).

Envoy verwendet mehrere lange Prozesssperren:

  • Wie bereits erwähnt, erhalten beim Schreiben von Zugriffsprotokollen alle Arbeitsthreads dieselbe Sperre, bevor der Protokollpuffer im Arbeitsspeicher gefüllt ist. Die Haltezeit der Sperre sollte sehr kurz sein, es ist jedoch möglich, dass die Sperre bei hoher Parallelität und hohem Durchsatz umkämpft wird.
  • Envoy verwendet ein sehr komplexes System zur Verarbeitung lokaler Statistiken für den Thread. Dies wird Gegenstand eines separaten Beitrags sein. Ich möchte jedoch kurz erwähnen, dass es im Rahmen der lokalen Verarbeitung von Thread-Statistiken manchmal erforderlich ist, eine Sperre für einen zentralen „Statistikspeicher“ zu erwerben. Diese Verriegelung sollte niemals erforderlich sein.
  • Der Hauptthread muss sich regelmäßig mit allen Arbeitsthreads koordinieren. Dies geschieht durch „Veröffentlichen“ vom Hauptthread an Arbeitsthreads und manchmal von Arbeitsthreads zurück an den Hauptthread. Für den Versand ist eine Sperre erforderlich, damit die veröffentlichte Nachricht für eine spätere Zustellung in die Warteschlange gestellt werden kann. Diese Sperren sollten niemals ernsthaft angefochten werden, können aber technisch immer noch blockiert werden.
  • Wenn Envoy ein Protokoll in den Systemfehlerstrom (Standardfehler) schreibt, erhält es eine Sperre für den gesamten Prozess. Generell gilt die lokale Protokollierung von Envoy aus Leistungssicht als schrecklich, daher wurde der Verbesserung nicht viel Aufmerksamkeit geschenkt.
  • Es gibt noch einige andere zufällige Sperren, aber keine davon ist leistungskritisch und sollte niemals in Frage gestellt werden.

Lokaler Thread-Speicher

Aufgrund der Art und Weise, wie Envoy die Verantwortlichkeiten des Hauptthreads von den Verantwortlichkeiten des Arbeitsthreads trennt, besteht die Anforderung, dass komplexe Verarbeitungen im Hauptthread durchgeführt und dann jedem Arbeitsthread in höchst gleichzeitiger Weise bereitgestellt werden können. In diesem Abschnitt wird Envoy Thread Local Storage (TLS) auf allgemeiner Ebene beschrieben. Im nächsten Abschnitt werde ich beschreiben, wie es zur Verwaltung eines Clusters verwendet wird.
[Übersetzung] Envoy-Threading-Modell

Wie bereits beschrieben, verwaltet der Hauptthread praktisch alle Verwaltungs- und Steuerungsebenenfunktionen im Envoy-Prozess. Die Steuerungsebene ist hier etwas überlastet, aber wenn man sie innerhalb des Envoy-Prozesses selbst betrachtet und sie mit der Weiterleitung vergleicht, die die Arbeitsthreads durchführen, ergibt es Sinn. Als allgemeine Regel gilt, dass der Haupt-Thread-Prozess einige Arbeiten ausführt und dann jeden Arbeitsthread entsprechend dem Ergebnis dieser Arbeit aktualisieren muss. In diesem Fall muss der Arbeitsthread nicht bei jedem Zugriff eine Sperre erwerben.

Das TLS-System (Thread Local Storage) von Envoy funktioniert wie folgt:

  • Code, der im Hauptthread ausgeführt wird, kann einen TLS-Slot für den gesamten Prozess zuweisen. Obwohl dies abstrahiert ist, handelt es sich in der Praxis um einen Index in einen Vektor, der O(1)-Zugriff ermöglicht.
  • Der Hauptthread kann beliebige Daten in seinem Slot installieren. Wenn dies erledigt ist, werden die Daten als normales Ereignisschleifenereignis in jedem Arbeitsthread veröffentlicht.
  • Arbeitsthreads können aus ihrem TLS-Slot lesen und alle dort verfügbaren Thread-lokalen Daten abrufen.

Obwohl es sich um ein sehr einfaches und unglaublich leistungsfähiges Paradigma handelt, ist es dem Konzept der RCU-Blockierung (Read-Copy-Update) sehr ähnlich. Im Wesentlichen sehen Arbeitsthreads niemals Datenänderungen in den TLS-Slots, während die Arbeit ausgeführt wird. Änderungen finden nur während der Ruhezeit zwischen Arbeitsereignissen statt.

Envoy nutzt dies auf zwei verschiedene Arten:

  • Durch das Speichern unterschiedlicher Daten in jedem Arbeitsthread kann auf die Daten ohne Blockierung zugegriffen werden.
  • Durch Beibehalten eines gemeinsamen Zeigers auf globale Daten im schreibgeschützten Modus in jedem Arbeitsthread. Somit verfügt jeder Arbeitsthread über einen Datenreferenzzähler, der während der Ausführung der Arbeit nicht verringert werden kann. Erst wenn sich alle Mitarbeiter beruhigen und neue gemeinsame Daten hochladen, werden die alten Daten zerstört. Dies ist identisch mit RCU.

Cluster-Update-Threading

In diesem Abschnitt beschreibe ich, wie TLS (Thread Local Storage) zur Verwaltung eines Clusters verwendet wird. Die Clusterverwaltung umfasst xDS-API und/oder DNS-Verarbeitung sowie Integritätsprüfungen.
[Übersetzung] Envoy-Threading-Modell

Das Cluster-Flow-Management umfasst die folgenden Komponenten und Schritte:

  1. Der Cluster Manager ist eine Komponente innerhalb von Envoy, die alle bekannten Cluster-Upstreams, die Cluster Discovery Service (CDS) API, die Secret Discovery Service (SDS) und Endpoint Discovery Service (EDS) APIs, DNS und aktive externe Prüfungen verwaltet. Gesundheitsprüfung. Es ist für die Erstellung einer „eventuell konsistenten“ Ansicht jedes Upstream-Clusters verantwortlich, die erkannte Hosts sowie den Gesundheitsstatus umfasst.
  2. Der Gesundheitsprüfer führt eine aktive Gesundheitsprüfung durch und meldet Änderungen des Gesundheitsstatus an den Cluster-Manager.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS werden durchgeführt, um die Clustermitgliedschaft zu bestimmen. Die Zustandsänderung wird an den Cluster-Manager zurückgegeben.
  4. Jeder Arbeitsthread führt kontinuierlich eine Ereignisschleife aus.
  5. Wenn der Cluster-Manager feststellt, dass sich der Status eines Clusters geändert hat, erstellt er einen neuen schreibgeschützten Snapshot des Cluster-Status und sendet ihn an jeden Arbeitsthread.
  6. Während der nächsten Ruhephase aktualisiert der Arbeitsthread den Snapshot im zugewiesenen TLS-Slot.
  7. Während eines I/O-Ereignisses, das den Host für den Lastausgleich bestimmen soll, fordert der Load Balancer einen TLS-Slot (Thread Local Storage) an, um Informationen über den Host zu erhalten. Hierzu sind keine Sperren erforderlich. Beachten Sie außerdem, dass TLS auch Aktualisierungsereignisse auslösen kann, sodass Load Balancer und andere Komponenten Caches, Datenstrukturen usw. neu berechnen können. Dies geht über den Rahmen dieses Beitrags hinaus, wird aber an verschiedenen Stellen im Code verwendet.

Mit dem oben beschriebenen Verfahren kann Envoy jede Anfrage ohne Blockierung bearbeiten (außer wie zuvor beschrieben). Abgesehen von der Komplexität des TLS-Codes selbst muss der Großteil des Codes nicht verstehen, wie Multithreading funktioniert, und kann Single-Threaded geschrieben werden. Dadurch ist der Großteil des Codes einfacher zu schreiben und die Leistung ist höher.

Andere Subsysteme, die TLS nutzen

TLS (Thread Local Storage) und RCU (Read Copy Update) werden in Envoy häufig verwendet.

Anwendungsbeispiele:

  • Mechanismus zum Ändern der Funktionalität während der Ausführung: Die aktuelle Liste der aktivierten Funktionen wird im Hauptthread berechnet. Jeder Arbeitsthread erhält dann mithilfe der RCU-Semantik einen schreibgeschützten Snapshot.
  • Routentabellen ersetzen: Für Routentabellen, die von RDS (Route Discovery Service) bereitgestellt werden, werden die Routentabellen im Hauptthread erstellt. Der schreibgeschützte Snapshot wird anschließend jedem Arbeitsthread mithilfe der RCU-Semantik (Read Copy Update) bereitgestellt. Dies macht das Ändern von Routentabellen atomar effizient.
  • HTTP-Header-Caching: Wie sich herausstellt, ist die Berechnung des HTTP-Headers für jede Anfrage (bei mehr als 25 RPS pro Kern) recht kostspielig. Envoy berechnet den Header zentral etwa jede halbe Sekunde und stellt ihn jedem Mitarbeiter über TLS und RCU zur Verfügung.

Es gibt noch andere Fälle, aber die vorherigen Beispiele sollten ein gutes Verständnis dafür vermitteln, wofür TLS verwendet wird.

Bekannte Leistungsprobleme

Während Envoy insgesamt eine recht gute Leistung erbringt, gibt es einige bemerkenswerte Bereiche, die Aufmerksamkeit erfordern, wenn es mit sehr hoher Parallelität und Durchsatz verwendet wird:

  • Wie in diesem Artikel beschrieben, erhalten derzeit alle Arbeitsthreads eine Sperre, wenn sie in den Speicherpuffer des Zugriffsprotokolls schreiben. Bei hoher Parallelität und hohem Durchsatz müssen Sie die Zugriffsprotokolle für jeden Arbeitsthread stapeln, allerdings auf Kosten der Übermittlung in der falschen Reihenfolge beim Schreiben in die endgültige Datei. Alternativ können Sie für jeden Worker-Thread ein separates Zugriffsprotokoll erstellen.
  • Obwohl die Statistiken stark optimiert sind, wird es bei sehr hoher Parallelität und hohem Durchsatz wahrscheinlich zu atomaren Konflikten um einzelne Statistiken kommen. Die Lösung für dieses Problem sind Zähler pro Worker-Thread mit periodischem Zurücksetzen der zentralen Zähler. Dies wird in einem späteren Beitrag besprochen.
  • Die aktuelle Architektur wird nicht gut funktionieren, wenn Envoy in einem Szenario eingesetzt wird, in dem es nur sehr wenige Verbindungen gibt, die erhebliche Verarbeitungsressourcen erfordern. Es gibt keine Garantie dafür, dass die Verbindungen gleichmäßig auf die Arbeitsthreads verteilt werden. Dies kann durch die Implementierung des Worker-Verbindungsausgleichs gelöst werden, der den Austausch von Verbindungen zwischen Worker-Threads ermöglicht.

Abschluss

Das Threading-Modell von Envoy ist darauf ausgelegt, eine einfache Programmierung und massive Parallelität zu ermöglichen, allerdings auf Kosten von potenziell verschwenderischem Speicher und Verbindungen, wenn es nicht richtig konfiguriert wird. Dieses Modell ermöglicht eine sehr gute Leistung bei sehr hohen Threadzahlen und hohem Durchsatz.
Wie ich auf Twitter kurz erwähnt habe, kann das Design auch auf einem vollständigen Netzwerk-Stack im Benutzermodus wie DPDK (Data Plane Development Kit) ausgeführt werden, was dazu führen kann, dass herkömmliche Server Millionen von Anfragen pro Sekunde mit vollständiger L7-Verarbeitung verarbeiten. Es wird sehr interessant sein zu sehen, was in den nächsten Jahren gebaut wird.
Ein letzter kurzer Kommentar: Ich wurde oft gefragt, warum wir C++ für Envoy gewählt haben. Der Grund bleibt, dass es immer noch die einzige weit verbreitete Industriesprache ist, in der die in diesem Beitrag beschriebene Architektur erstellt werden kann. C++ ist definitiv nicht für alle oder sogar viele Projekte geeignet, aber für bestimmte Anwendungsfälle ist es immer noch das einzige Werkzeug, das diese Aufgabe erledigt.

Links zum Code

Links zu Dateien mit Schnittstellen und Header-Implementierungen, die in diesem Beitrag besprochen werden:

Source: habr.com

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