Hallo Habr! Ich präsentiere Ihnen die Übersetzung des Artikels
„Envoy Threading Model“ von Matt Klein.
Dieser Artikel schien mir interessant genug zu sein, und da Envoy am häufigsten als Teil von „istio“ oder einfach als „Ingress Controller“ -Kubernetes verwendet wird, haben die meisten Menschen nicht die gleiche direkte Interaktion damit wie zum Beispiel mit typischen Nginx- oder Haproxy-Installationen. Wenn jedoch etwas kaputt geht, ist es gut zu verstehen, wie es von innen funktioniert. Ich habe versucht, so viel Text wie möglich ins Russische zu übersetzen, einschließlich spezieller Wörter. Für diejenigen, die es schmerzhaft sehen, habe ich die Originale in Klammern gelassen. Willkommen bei Katze.
Die technische Dokumentation auf niedriger Ebene auf der Envoy-Codebasis ist derzeit ziemlich knapp. Um dies zu beheben, plane ich eine Reihe von Blog-Artikeln über die verschiedenen Envoy-Subsysteme. Da dies der erste Artikel ist, teilen Sie mir bitte Ihre Meinung und Ihre Interessen in den folgenden Artikeln mit.
Eine der häufigsten technischen Fragen, die ich zu Envoy bekomme, ist die Anforderung einer einfachen Beschreibung des verwendeten Threading-Modells. In diesem Beitrag werde ich beschreiben, wie Envoy Verbindungen Threads zuordnet, sowie eine Beschreibung des lokalen Thread-Speichersystems, das intern verwendet wird, um den Code paralleler und leistungsfähiger zu machen.
Threading-Übersicht
Envoy verwendet drei verschiedene Arten von Streams:- Main: Dieser Thread steuert den Beginn und das Ende des Prozesses sowie die gesamte Verarbeitung der XDS-API (xDiscovery Service), einschließlich DNS, Integritätsprüfung, allgemeine Cluster- und Dienstverwaltung (Laufzeit), Zurücksetzen der Statistiken, Verwaltung und allgemeine Verwaltung Prozesse - Linux-Signale, Hot-Restart usw. Alles, was in diesem Thread passiert, ist asynchron und nicht blockierend. Im Allgemeinen koordiniert der Hauptthread alle kritischen Funktionsprozesse, für deren Abschluss keine große Anzahl von CPUs erforderlich ist. Dadurch kann der größte Teil des Steuercodes so geschrieben werden, als wäre er Single-Threaded.
- Worker: Standardmäßig erstellt Envoy für jeden Hardware-Thread im System einen Worker-Thread. Dieser kann mit der Option
--concurrency
gesteuert werden. Jeder Worker-Thread startet eine "nicht blockierende" Ereignisschleife, die für das Abhören jedes Listeners verantwortlich ist. Zum Zeitpunkt des Schreibens (29. Juli 2017) gibt es kein Sharding des Listeners, der neue empfängt Verbindungen, Erstellen einer Instanz des Filterstapels zum Verbinden und Verarbeiten aller E / A-Vorgänge über die Lebensdauer der Verbindung. Dies ermöglicht wiederum, dass der größte Teil des Verbindungsverarbeitungscodes so geschrieben wird, als wäre er Single-Threaded. - File Flusher: Jede Datei, die Envoy schreibt, hauptsächlich Zugriffsprotokolle, verfügt derzeit über einen unabhängigen Blockierungsstrom. Dies liegt an der Tatsache, dass das Schreiben in Dateien, die vom Dateisystem zwischengespeichert werden, selbst bei Verwendung von
O_NONBLOCK
manchmal blockiert werden kann (Seufzen). 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 Datei-Flush- Stream geleert werden. Dies ist ein Codebereich, in dem technisch alle Worker-Threads dieselbe Sperre blockieren können, während sie versuchen, den Speicherpuffer zu füllen.
Verbindungsbehandlung
Wie oben kurz erläutert, hören alle Worker-Threads alle Listener ohne Segmentierung ab. Daher wird der Kernel verwendet, um empfangene Sockets korrekt an Worker-Threads zu senden. Moderne Kerne sind im Allgemeinen sehr gut darin. Sie verwenden Funktionen wie das Erhöhen der Priorität von Eingabe / Ausgabe (IO), um zu versuchen, den Thread mit Arbeit zu füllen, bevor andere Threads verwendet werden, die ebenfalls denselben Socket abhören, und verwenden auch keine kreisförmige Verriegelung (Spinlock), um jede Anfrage zu bearbeiten.
Sobald eine Verbindung in einem Worker-Thread akzeptiert wurde, verlässt sie diesen Thread nie mehr. Die gesamte weitere Verarbeitung der Verbindung wird vollständig im Worker-Thread verarbeitet, einschließlich des Weiterleitungsverhaltens.
Dies hat mehrere wichtige Konsequenzen:- Alle Verbindungspools in Envoy befinden sich in einem Workflow. Obwohl HTTP / 2-Verbindungspools jeweils nur eine Verbindung zu jedem Upstream-Host herstellen, gibt es bei vier Arbeitsthreads vier HTTP / 2-Verbindungen zum Upstream-Host im eingeschwungenen Zustand.
- Der Grund, warum Envoy auf diese Weise funktioniert, liegt darin, dass durch das Speichern von allem in einem Workflow fast der gesamte Code ohne Blockierung und als Single-Thread geschrieben werden kann. Dieses Design erleichtert das Schreiben von viel Code und lässt sich unglaublich gut für eine nahezu unbegrenzte Anzahl von Workflows skalieren.
- Eine der wichtigsten Schlussfolgerungen ist jedoch, dass es aus Sicht des Speicherpools und der Verbindungseffizienz tatsächlich sehr wichtig ist, den Parameter
--concurrency
zu konfigurieren. Wenn mehr Arbeitsthreads als erforderlich vorhanden sind, geht der Speicher verloren, es werden mehr inaktive Verbindungen erstellt und der Zugriff auf den Verbindungspool wird verlangsamt. Bei Lyft arbeiten unsere Gesandten-Beiwagencontainer mit sehr geringer Parallelität, sodass die Leistung in etwa den Diensten entspricht, neben denen sie stehen. Wir führen Envoy nur als Edge-Proxy (Edge) mit maximaler Parallelität aus.
Was bedeutet Nichtblockieren?
Der Begriff "nicht blockierend" wurde bisher mehrmals verwendet, um die Funktionsweise der Haupt- und Arbeitsthreads zu diskutieren. Der gesamte Code wird geschrieben, sofern nichts blockiert wird. Dies ist jedoch nicht ganz richtig (was ist nicht ganz wahr?).
Envoy verwendet mehrere langwierige Prozesssperren:- Wie bereits erwähnt, erhalten beim Schreiben von Zugriffsprotokollen alle Arbeitsthreads dieselbe Sperre, bevor der Protokollpuffer im Speicher gefüllt wird. Die Sperrhaltezeit sollte sehr niedrig sein, aber es ist möglich, dass diese Sperre mit hoher Parallelität und hohem Durchsatz in Frage gestellt wird.
- Envoy verwendet ein sehr ausgeklügeltes System zur Verarbeitung von Statistiken, die lokal für den Stream sind. Dies wird das Thema eines separaten Beitrags sein. Ich möchte jedoch kurz erwähnen, dass es im Rahmen der lokalen Verarbeitung von Flussstatistiken manchmal erforderlich ist, eine Sperre für den zentralen „Statistikspeicher“ zu erhalten. Dieses Schloss sollte niemals benötigt werden.
- Der Haupt-Thread muss regelmäßig mit allen Workflows koordiniert werden. Dies erfolgt durch "Veröffentlichen" vom Hauptthread zu den Arbeitsthreads und manchmal von den Arbeitsthreads zurück zum Hauptthread. Zum Senden ist eine Blockierung erforderlich, damit die veröffentlichte Nachricht für die spätere Zustellung in die Warteschlange gestellt werden kann. Diese Schlösser sollten niemals ernsthaftem Wettbewerb ausgesetzt werden, können aber dennoch technisch blockiert werden.
- Wenn Envoy ein Protokoll in den Systemfehlerstrom schreibt (Standardfehler), erhält es eine Sperre für den gesamten Prozess. Insgesamt wird die lokale Protokollierung von Envoy in Bezug auf die Leistung als schrecklich angesehen, sodass der Verbesserung nicht viel Aufmerksamkeit geschenkt wird.
- Es gibt mehrere andere zufällige Sperren, aber keine davon ist leistungskritisch und sollte niemals bestritten werden.
Thread lokalen Speicher
Aufgrund der Art und Weise, wie Envoy die Hauptverantwortlichkeiten des Threads von den Aufgaben des Workflows trennt, besteht die Anforderung, dass eine komplexe Verarbeitung im Hauptthread ausgeführt und dann jedem Workflow mit einem hohen Grad an Parallelität bereitgestellt werden kann. In diesem Abschnitt wird das TLS-System (Envoy Thread Local Storage) auf hoher Ebene beschrieben. Im nächsten Abschnitt werde ich beschreiben, wie der Cluster verwaltet wird.

Wie bereits beschrieben, verarbeitet der Hauptthread fast alle Verwaltungsfunktionen und die Funktionalität der Steuerebene im Envoy-Prozess. Die Steuerebene ist hier etwas überlastet. Wenn Sie sie jedoch im Envoy-Prozess selbst betrachten und mit der Weiterleitung vergleichen, die die Worker-Threads ausführen, erscheint dies angemessen. In der Regel führt der Hauptthreadprozess einige Arbeiten aus und muss dann jeden Arbeitsthread entsprechend dem Ergebnis dieser Arbeit aktualisieren,
während der Arbeitsthread
nicht bei jedem Zugriff eine Sperre festlegen muss .
Das TLS-Envoy-System (Thread Local Storage) funktioniert wie folgt:- Code, der im Hauptthread ausgeführt wird, kann einen TLS-Steckplatz für den gesamten Prozess zuweisen. Obwohl dies abstrahiert ist, handelt es sich in der Praxis um einen Index in einem Vektor, der O (1) -Zugriff bietet.
- Der Hauptstrom kann beliebige Daten in seinem Steckplatz einstellen. In diesem Fall werden die Daten in jedem Workflow als reguläres Ereignisschleifenereignis veröffentlicht.
- Worker-Threads können aus ihrem TLS-Steckplatz lesen und alle dort verfügbaren lokalen Thread-Daten abrufen.
Obwohl dies ein sehr einfaches und unglaublich leistungsfähiges Paradigma ist, ist es dem Konzept der RCU-Blockierung (Read-Copy-Update) sehr ähnlich. Im Wesentlichen sehen Workflows zur Laufzeit keine Datenänderungen in den TLS-Slots. Änderungen treten nur während der Ruhezeit zwischen Arbeitsereignissen auf.
Envoy verwendet dies auf zwei verschiedene Arten:- Durch Speichern verschiedener Daten in jedem Workflow wird der Zugriff auf diese Daten ohne Blockierung ausgeführt.
- Durch Speichern eines globalen Zeigers auf globale Daten im schreibgeschützten Modus für jeden Arbeitsthread. Somit hat jeder Arbeitsthread einen Datenreferenzzähler, der während der Ausführung der Arbeit nicht reduziert werden kann. Nur wenn sich alle Mitarbeiter beruhigen und neue freigegebene Daten hochladen, werden die alten Daten zerstört. Es ist identisch mit RCU.
Threading für Cluster-Updates
In diesem Abschnitt werde ich beschreiben, wie TLS (Thread Local Storage) zum Verwalten eines Clusters verwendet wird. Die Clusterverwaltung umfasst die Verarbeitung von xDS- und / oder DNS-APIs sowie die Integritätsprüfung.
Das Cluster-Flow-Management umfasst die folgenden Komponenten und Schritte:- Cluster Manager ist eine Komponente in Envoy, die alle bekannten Cluster-Upstream-, CDS- (Cluster Discovery Service) APIs, SDS- (Secret Discovery Service) und EDS- (Endpoint Discovery Service), DNS- und aktiven externen Überprüfungen verwaltet Gesundheit (Gesundheitskontrolle). Er ist dafür verantwortlich, eine „letztendlich konsistente“ Darstellung jedes Upstream-Clusters zu erstellen, die die erkannten Hosts sowie den Gesundheitszustand enthält.
- Die Integritätsprüfung führt eine aktive Integritätsprüfung durch und meldet Änderungen im Integritätsstatus an den Cluster-Manager.
- CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS werden durchgeführt, um die Clustermitgliedschaft zu bestimmen. Die Statusänderung wird an den Cluster-Manager zurückgegeben.
- Jeder Workflow führt ständig eine Ereignisschleife aus.
- Wenn der Cluster-Manager feststellt, dass sich der Status des Clusters geändert hat, erstellt er einen neuen schreibgeschützten Cluster-Snapshot und sendet ihn an jeden Worker-Thread.
- Während der nächsten Ruhephase aktualisiert der Workflow den Snapshot im dedizierten TLS-Steckplatz.
- Während eines E / A-Ereignisses, das der Host für den Lastausgleich festlegen sollte, fordert der Lastausgleich einen TLS-Steckplatz (lokaler Thread-Speicher) an, um Hostinformationen abzurufen. Hierfür sind keine Schlösser erforderlich. Beachten Sie auch, dass TLS auch Ereignisse während des Upgrades auslösen kann, sodass Load Balancer und andere Komponenten Caches, Datenstrukturen usw. nachzählen können. Dies würde den Rahmen dieses Beitrags sprengen, wird jedoch an verschiedenen Stellen im Code verwendet.
Mit dem oben beschriebenen Verfahren kann Envoy jede Anforderung ohne Sperren (außer den zuvor beschriebenen) verarbeiten. Abgesehen von der Komplexität des TLS-Codes selbst muss der größte Teil des Codes nicht verstehen, wie Multithreading funktioniert, und er kann im Single-Threaded-Modus geschrieben werden. Dies erleichtert das Schreiben des größten Teils des Codes zusätzlich zu einer überlegenen Leistung.
Andere Subsysteme, die TLS verwenden
TLS (Thread Local Storage) und RCU (Read Copy Update) werden in Envoy häufig verwendet.
Anwendungsbeispiele:- Der Mechanismus zum Ändern der Funktionalität während der Ausführung: Die aktuelle Liste der aktivierten Funktionen wird im Hauptthread berechnet. Jeder Workflow erhält dann einen schreibgeschützten Snapshot unter Verwendung der RCU-Semantik.
- Routentabellen ersetzen : Für Routentabellen, die vom RDS (Route Discovery Service) bereitgestellt werden, werden Routentabellen im Hauptthread erstellt. Ein schreibgeschützter Snapshot wird später für jeden Workflow 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 Anforderung (bei Ausführung von ~ 25 KB + RPS pro Kern) recht teuer. Envoy berechnet den Header ungefähr jede halbe Sekunde zentral und stellt ihn jedem Mitarbeiter über TLS und RCU zur Verfügung.
Es gibt andere Fälle, aber frühere Beispiele sollten ein gutes Verständnis dafür vermitteln, wofür TLS verwendet wird.
Bekannte Leistungsprobleme
Obwohl Envoy insgesamt recht gut funktioniert, gibt es einige bekannte Bereiche, die bei Verwendung mit sehr hoher Parallelität und Bandbreite beachtet werden müssen:
- Wie bereits in diesem Artikel beschrieben, sind derzeit alle Arbeitsthreads gesperrt, wenn sie in den Speicherpuffer des Zugriffsprotokolls schreiben. Bei hoher Parallelität und hohem Durchsatz ist es aufgrund der ungeordneten Zustellung beim Schreiben in die endgültige Datei erforderlich, Zugriffsprotokolle für jeden Workflow zu verpacken. Alternativ können Sie für jeden Workflow ein separates Zugriffsprotokoll erstellen.
- Obwohl die Statistiken sehr optimiert sind, mit sehr hoher Parallelität und hohem Durchsatz, besteht wahrscheinlich ein atomarer Wettbewerb bei den einzelnen Statistiken. Die Lösung für dieses Problem sind Zähler pro Workflow mit regelmäßigem Zurücksetzen der zentralen Zähler. Dies wird in einem nachfolgenden Beitrag besprochen.
- Die vorhandene Architektur funktioniert nicht gut, wenn Envoy in einem Szenario bereitgestellt wird, in dem nur sehr wenige Verbindungen vorhanden sind, für die erhebliche Verarbeitungsressourcen erforderlich sind. Es gibt keine Garantie dafür, dass die Kommunikation gleichmäßig zwischen den Workflows verteilt wird. Dies kann durch Ausgleichen von Arbeitsverbindungen gelöst werden, bei denen die Fähigkeit zum Austausch von Verbindungen zwischen Arbeitsabläufen realisiert wird.
Fazit
Das Envoy-Threading-Modell bietet eine einfache Programmierung und massive Parallelität aufgrund der möglicherweise verschwenderischen Verwendung von Speicher und Verbindungen, wenn diese nicht richtig konfiguriert sind. Dieses Modell ermöglicht es, sehr gut mit einer sehr hohen Anzahl von Threads und Durchsatz zu arbeiten.
Wie ich auf Twitter kurz erwähnt habe, kann ein Design im Benutzermodus auch auf einem voll funktionsfähigen Netzwerkstapel ausgeführt werden, z. B. dem DPDK (Data Plane Development Kit), das dazu führen kann, dass reguläre Server bei vollständiger L7-Verarbeitung Millionen von Anforderungen pro Sekunde 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 dafür ist nach wie vor, dass es immer noch die einzige weit verbreitete Sprache auf industrieller Ebene ist, auf der die in diesem Beitrag beschriebene Architektur aufgebaut werden kann. C ++ ist definitiv nicht für alle oder sogar für viele Projekte geeignet, aber für bestimmte Anwendungsfälle ist es immer noch das einzige Werkzeug, um die Arbeit zu erledigen (um die Arbeit zu erledigen).
Links zum Code
Links zu Dateien mit Schnittstellen und Header-Implementierungen, die in diesem Beitrag behandelt werden: