Artikelübersetzung:
Dieser Artikel erschien mir recht interessant, da Envoy meistens als Teil von „istio“ oder einfach als „ingress controller“ in Kubernetes verwendet wird. Die meisten Menschen haben daher nicht die gleiche direkte Interaktion damit wie bei typischen Installationen von Nginx oder Haproxy. Wenn also etwas nicht funktioniert, wäre es gut zu verstehen, wie es intern aufgebaut ist. Ich habe versucht, so viel Text wie möglich ins Deutsche zu übersetzen, einschließlich spezieller Begriffe; für diejenigen, die solche Dinge als unangenehm empfinden, habe ich die Originalsätze in Klammern gelassen. Willkommen im Inhaltsverzeichnis.
Die technische Dokumentation auf niedriger Ebene zur Codebasis von Envoy ist derzeit recht spärlich. Um dies zu beheben, plane ich eine Reihe von Blogartikeln über die verschiedenen Unter Systeme von Envoy. Da dies der erste Artikel ist, lassen Sie mich bitte wissen, was Sie darüber denken und was Sie in den kommenden Artikeln interessieren könnte.
Eine der häufigsten technischen Fragen, die ich zu Envoy erhalte, betrifft eine detaillierte Beschreibung des verwendeten Threading-Modells. In diesem Beitrag werde ich erläutern, wie Envoy Verbindungen zu Threads zuordnet und das System für threadlokale Speicherung (Thread Local Storage), das intern verwendet wird, um den Code paralleler und leistungsfähiger zu gestalten.
Übersicht über Threads
![[Übersetzung] Envoy-Threadingmodell](/wp-content/uploads/2019/04/c5028463db1eb5b07ccb1a3af1f64a03.jpeg)
Envoy verwendet drei verschiedene Typen von Threads:
- Haupt-Thread: Dieser Thread ist für den Start und die Beendigung des Prozesses verantwortlich, verwaltet die gesamte Bearbeitung der XDS (xDiscovery Service) API, einschließlich DNS, Gesundheitsüberprüfung (Health Checking), allgemeinem Cluster-Management und Service-Lebenszyklus-Management, Rücksetzen von Statistiken, Administration und allgemeiner Prozessverwaltung — Linux-Signale, Hot-Restart usw. Alles, was in diesem Thread geschieht, ist asynchron und 'nicht blockierend'. Im Allgemeinen koordiniert der Haupt-Thread alle kritischen Funktionsprozesse, für deren Ausführung kein hoher CPU-Aufwand erforderlich ist. Dies ermöglicht es, den Großteil des Verwaltungs-Codes so zu schreiben, als wäre er einsträngig.
- Arbeiter (Worker): Standardmäßig erstellt Envoy einen Arbeits-Thread (worker thread) für jeden Hardware-Thread im System, dies kann mit der Option
--concurrencykontrolliert werden. Jeder Arbeits-Thread startet eine „nicht blockierende“ Ereignisschleife (event loop), die für das Überwachen (listening) jedes Listeners verantwortlich ist. Zum Zeitpunkt des Schreibens dieses Artikels (29. Juli 2017) gibt es kein Sharding des Listeners, das Akzeptieren neuer Verbindungen, das Erstellen einer Instanz des Filter-Stacks für die Verbindung und das Bearbeiten aller Ein- und Ausgabeoperationen (IO) während der Lebensdauer der Verbindung. Auch hier ermöglicht es, den Großteil des Codes zur Verarbeitung von Verbindungen so zu schreiben, als ob er einsteigerfreundlich ist. - Datei-Puffer (File flusher): Jede Datei, die Envoy schreibt, vor allem Zugriffprotokolle (access logs), hat derzeit einen unabhängigen blockierenden Thread. Dies liegt daran, dass das Schreiben in Dateien, die von dem Dateisystem zwischengespeichert werden, selbst bei Verwendung von
O_NONBLOCKmanchmal blockiert werden kann (seufz). Wenn Arbeits-Threads in eine Datei schreiben müssen, werden die Daten tatsächlich in einen Puffer im Speicher verschoben, wo sie schließlich über den Dateipuffer (file flush). Dies ist einer der Bereiche im Code, in dem technisch alle Worker-Threads denselben Lock blockieren können, während sie versuchen, den Arbeitsspeicher zu füllen.
Verbindungsmanagement
Wie oben kurz erwähnt, lauschen alle Worker-Threads allen Listenern ohne jegliche Segmentierung. Das Kernsystem wird daher effizient genutzt, um akzeptierte Sockets an die Worker-Threads weiterzuleiten. Moderne Kerne sind darin allgemein sehr gut und verwenden Funktionen wie die Priorisierung von Eingabe/Ausgabe (I/O), um zu versuchen, einen Thread mit Arbeit zu versorgen, bevor andere Threads, die ebenfalls denselben Socket überwachen, beansprucht werden. Außerdem wird dabei vermieden, einen Spinlock für die Bearbeitung jeder Anfrage zu verwenden.
Sobald eine Verbindung in einem Worker-Thread akzeptiert wurde, verlässt sie diesen Thread nie wieder. Alle weiteren Verarbeitungen der Verbindung werden vollständig im Worker-Thread durchgeführt, einschließlich des Verhaltens bei der Weiterleitung.
Dies hat mehrere wichtige Konsequenzen:
- Alle Verbindungspools in Envoy gehören zu einem Worker-Thread. Das bedeutet, dass, obwohl HTTP/2-Verbindungspools jeweils nur eine Verbindung zu jedem übergeordneten Host herstellen, bei vier Worker-Threads vier HTTP/2-Verbindungen zum übergeordneten Host vorhanden sind, wenn der Zustand stabil ist.
- Der Grund, warum Envoy so arbeitet, liegt darin, dass durch die Zusammenführung aller in einem Worker-Thread fast der gesamte Code ohne Sperren und so aufgebaut werden kann, als wäre er ein einzelner Thread. Dieses Design erleichtert das Schreiben einer großen Menge Code und skaliert unglaublich gut für nahezu unbegrenzt viele Worker-Threads.
- Eine der Hauptauffassungen ist jedoch, dass es aus der Perspektive der Effizienz des Speichers und der Verbindungen tatsächlich entscheidend ist, die Parameter richtig einzustellen.
--concurrency. Eine höhere Anzahl an Arbeitsströmen als notwendig kann zu speicherlecks, einer erhöhten Anzahl inaktiver Verbindungen und einer Verlangsamung des Zugriffs auf den Verbindungs-Pool führen. Bei Lyft arbeiten unsere Envoy Sidecar-Container mit sehr niedrigem Parallelismus, sodass die Leistung ungefähr der der Dienste entspricht, neben denen sie positioniert sind. Wir verwenden Envoy als Edge-Proxy-Server nur bei maximalem Parallelismus.
Was nicht blockierend bedeutet (What non-blocking means)
Der Begriff „nicht blockierend“ wurde bereits mehrere Male verwendet, um zu diskutieren, wie Haupt- und Arbeitsströme funktionieren. Der gesamte Code ist so geschrieben, dass grundsätzlich nichts jemals blockiert. Allerdings ist das nicht ganz korrekt (was ist nicht ganz korrekt?).
Envoy verwendet mehrere langanhaltende Prozesssperren:
- Wie bereits erwähnt, erhalten alle Arbeitsstränge beim Protokollieren von Zugriffsprotokollen dieselbe Sperre, bevor der Protokollpuffer im Speicher ausgefüllt wird. Die Haltezeit der Sperre sollte sehr gering sein, allerdings könnte diese Sperre bei hohem Parallelismus und hoher Durchsatzrate in Frage gestellt werden.
- Envoy verwendet ein sehr komplexes System zur Verarbeitung von Statistiken, das lokal für den jeweiligen Strang ist. Dazu wird es einen separaten Beitrag geben. Dennoch möchte ich kurz erwähnen, dass im Rahmen der lokalen Verarbeitung von Strangstatistiken manchmal eine Sperre für das zentrale "Statistik-Speicher" benötigt wird. Diese Sperre sollte jedoch niemals erforderlich sein.
- Der Hauptstrom benötigt gelegentlich eine Koordination mit allen Arbeitsströmen. Dies erfolgt durch die "Veröffentlichung" aus dem Hauptstrom in die Arbeitsströme und manchmal auch aus den Arbeitsströmen zurück in den Hauptstrom. Um eine Nachricht zu senden, ist eine Sperre erforderlich, damit die veröffentlichte Nachricht in eine Warteschlange für die spätere Zustellung gestellt werden kann. Diese Sperren sollten niemals ernsthaft konkurrieren, können jedoch technisch blockiert werden.
- Wenn Envoy in den Systemfehlerstrom (standard error) schreibt, wird der gesamte Prozess blockiert. Im Allgemeinen wird die lokale Protokollierung von Envoy hinsichtlich der Leistung als schlecht angesehen, weshalb ihr Verbesserung nicht viel Aufmerksamkeit geschenkt wird.
- Es gibt einige andere zufällige Sperren, aber keine davon ist kritisch für die Leistung und sollte niemals angefochten werden.
Thread-local storage
Da Envoy die Aufgaben des Hauptthreads von den Aufgaben der Worker-Threads trennt, besteht die Anforderung, dass komplexe Verarbeitung im Hauptthread erfolgt und dann mit hoher Parallelität an jeden Worker-Thread übergeben wird. In diesem Abschnitt wird das Envoy Thread Local Storage (TLS) System auf hoher Ebene beschrieben. Im nächsten Abschnitt werde ich erläutern, wie es zur Clusterverwaltung eingesetzt wird.
![[Übersetzung] Envoy-Threadingmodell](/wp-content/uploads/2019/04/a47af1e8eb1f55a4d7609b3ff3ee9ce1.jpeg)
Wie bereits beschrieben, verarbeitet der Hauptthread nahezu alle Management-Funktionen und die Funktionalität der Steuerungsebene im Envoy-Prozess. Die Steuerungsebene ist hier etwas überlastet, aber wenn man sie im Kontext des Envoy-Prozesses betrachtet und mit den Weiterleitungen vergleicht, die die Worker-Threads durchführen, erscheint das sinnvoll. Allgemein gesagt, führt der Hauptthread gewisse Arbeiten aus und muss dann jeden Worker-Thread gemäß dem Ergebnis dieser Arbeit aktualisieren, wobei der Worker-Thread nicht bei jedem Zugriff eine Sperre setzen muss..
Das Envoy TLS-System (Thread Local Storage) funktioniert folgendermaßen:
- Der Code, der im Hauptthread ausgeführt wird, kann einen TLS-Slot für den gesamten Prozess reservieren. Obwohl dies abstrahiert ist, handelt es sich in der Praxis um einen Index in einem Vektor, der den Zugriff in O(1) ermöglicht.
- Der Hauptthread kann beliebige Daten in seinen Slot einfügen. Sobald dies geschehen ist, werden die Daten in jedem Arbeits-Thread als gewöhnliches Ereignis des Event-Loops veröffentlicht.
- Arbeits-Threads können aus ihrem TLS-Slot lesen und jegliche dort verfügbaren lokalen Thread-Daten abrufen.
Obwohl dies eine sehr einfache und unglaublich leistungsstarke Paradigme ist, die der RCU (Read-Copy-Update)-Konzeption sehr ähnelt. Im Wesentlichen sehen Arbeits-Threads während der Ausführung ihrer Aufgaben nie Änderungen der Daten in TLS-Slots. Änderungen erfolgen nur in den Ruhephasen zwischen den Arbeitsereignissen.
Envoy nutzt dies auf zwei verschiedene Arten:
- Indem es verschiedene Daten in jedem Arbeits-Thread speichert, erfolgt der Zugriff auf diese Daten ohne jegliche Sperrung.
- Beim Beibehalten eines allgemeinen Zeigers auf globale Daten im "Nur-Lesen"-Modus in jedem Arbeitsablauf hat jeder Arbeitsablauf einen Daten-Referenzzähler, der während der Durchführung der Arbeiten nicht verringert werden kann. Erst wenn alle Mitarbeiter zur Ruhe kommen und neue gemeinsame Daten laden, werden die alten Daten gelöscht. Dies ist identisch mit RCU.
Cluster-Update-Threading
In diesem Abschnitt werde ich erläutern, wie TLS (Thread-local storage) zur Verwaltung des Clusters verwendet wird. Die Clusterverwaltung beinhaltet die Verarbeitung der xDS-API und / oder DNS sowie die Überprüfung der Betriebsbereitschaft (Health Checking).
![[Übersetzung] Envoy-Threadingmodell](/wp-content/uploads/2019/04/238f5718729e1f12f8f0d6507f16abe8.jpeg)
Die Verwaltung der Cluster-Threads umfasst die folgenden Komponenten und Phasen:
- Der Cluster-Manager ist eine Komponente innerhalb von Envoy, die alle bekannten Upstreams des Clusters verwaltet, einschließlich der APIs für CDS (Cluster Discovery Service), SDS (Secret Discovery Service) und EDS (Endpoint Discovery Service), DNS sowie aktiver externer Gesundheitsprüfungen. Er ist verantwortlich für die Erstellung einer "letztendlich konsistenten" Darstellung jedes Upstreams des Clusters, die entdeckte Hosts sowie den Gesundheitsstatus umfasst.
- Der Gesundheitsprüfer führt aktive Gesundheitsprüfungen durch und informiert den Cluster-Manager über Änderungen im Gesundheitszustand.
- CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS werden verwendet, um die Clusterzugehörigkeit zu bestimmen. Der Zustand wird an den Cluster-Manager zurückgemeldet.
- Jeder Worker-Thread führt kontinuierlich eine Ereignisverarbeitungsschleife aus.
- Wenn der Cluster-Manager feststellt, dass sich der Zustand des Clusters geändert hat, erstellt er einen neuen, schreibgeschützten Zustandssnapshot des Clusters und sendet diesen an jeden Worker-Thread.
- Während der nächsten Ruhephase wird der Arbeitsablauf einen Snapshot im zugewiesenen TLS-Slot aktualisieren.
- Während eines E/A-Ereignisses, das den Host für die Lastverteilung bestimmen soll, wird der Lastverteiler den TLS-Slot (Thread Local Storage) anfordern, um Informationen über den Host zu erhalten. Hierfür sind keine Sperren erforderlich. Beachten Sie auch, dass TLS auch Ereignisse bei Aktualisierungen auslösen kann, sodass die Lastverteilungssysteme und andere Komponenten Caches, Datenstrukturen usw. neu berechnen können. Dies überschreitet den Rahmen dieses Beitrags, wird aber an verschiedenen Stellen im Code verwendet.
Durch die oben beschriebene Vorgehensweise kann Envoy jede Anfrage ohne Sperren (außer den zuvor beschriebenen) verarbeiten. Abgesehen von der Komplexität des TLS-Codes muss der Großteil des Codes nicht verstehen, wie Multithreading funktioniert, und kann im Single-Thread-Modus geschrieben werden. Dies erleichtert das Schreiben des Großteils des Codes und führt zu hervorragender Leistung.
Andere Subsysteme, die TLS verwenden
TLS (Thread Local Storage) und RCU (Read Copy Update) sind weit verbreitet in Envoy.
Beispiele für die Nutzung:
- Mechanismus zur Änderung der Funktionalität während der Ausführung: Die aktuelle Liste der aktivierten Funktionen wird im Hauptthread berechnet. Anschließend erhält jeder Arbeits-Thread einen schreibgeschützten Snapshot unter Verwendung der RCU-Semantik.
- Ersetzen von Routentabellen: Für von RDS (Route Discovery Service) bereitgestellte Routentabellen werden diese im Hauptthread erstellt. Ein schreibgeschützter Snapshot wird dann jedem Arbeits-Thread mit der RCU (Read Copy Update)-Semantik zur Verfügung gestellt. Dies macht die Änderung der Routentabellen atomar effizient.
- Caching von HTTP-Headern: Wie sich herausstellt, ist die Berechnung des HTTP-Headers für jede Anfrage (bei ~25K+ RPS pro Kern) recht aufwändig. Envoy berechnet den Header zentralisiert etwa alle halben Sekunden und stellt ihn jedem Worker über TLS und RCU zur Verfügung.
Es gibt auch andere Fälle, aber die vorherigen Beispiele sollten ein gutes Verständnis dafür vermitteln, wofür TLS verwendet wird.
Bekannte Leistungsengpässe
Obwohl Envoy insgesamt recht gut funktioniert, gibt es einige bekannte Bereiche, die Aufmerksamkeit erfordern, insbesondere bei sehr hohen Parallelitäts- und Durchsatzraten:
- Wie bereits in diesem Artikel beschrieben, werden derzeit alle Workflows beim Schreiben in den Zugangsinformationen-Puffer blockiert. Bei hohem Parallelismus und hoher Durchsatzleistung wird es erforderlich sein, die Zugangsinformationen für jeden Workflow zu paketieren, indem eine ungeordnete Lieferung beim Schreiben in die endgültige Datei erfolgt. Alternativ kann für jeden Workflow ein separater Zugangsinformationen-Log erstellt werden.
- Obwohl die Statistiken stark optimiert sind, wird es bei sehr hohem Parallelismus und Durchsatz wahrscheinlich zu atomarer Konkurrenz bei den individuellen Statistiken kommen. Eine Lösung für dieses Problem sind Zähler für einen einzelnen Workflow mit periodischem Zurücksetzen der zentralen Zähler. Dies wird in einem der kommenden Beiträge behandelt.
- Die bestehende Architektur funktioniert nicht reibungslos, wenn Envoy in einem Szenario eingesetzt wird, in dem nur wenige Verbindungen bestehen, die erhebliche Ressourcen für die Verarbeitung benötigen. Es gibt keine Garantie, dass die Verbindungen gleichmäßig auf die Worker-Threads verteilt werden. Dies kann durch die Implementierung einer Lastverteilung der Arbeitsverbindungen gelöst werden, bei der eine Verbindung zwischen den Worker-Threads ausgetauscht werden kann.
Fazit
Das Envoy-Thread-Modell wurde entwickelt, um eine einfache Programmierung und massive Parallelität zu gewährleisten, auch wenn dies möglicherweise zu einem übermäßigen Verbrauch von Speicher und Verbindungen führt, falls es nicht richtig konfiguriert ist. Dieses Modell ermöglicht es, bei einer sehr hohen Anzahl von Threads und Durchsatz sehr gut zu funktionieren.
Wie ich bereits kurz auf Twitter erwähnt habe, kann das Design auch mit einem voll funktionsfähigen Netzwerk-Stack im Benutzermodus arbeiten, wie etwa DPDK (Data Plane Development Kit). Dies könnte dazu führen, dass normale Server Millionen von Anfragen pro Sekunde mit vollständiger L7-Verarbeitung bewältigen. Es wird sehr interessant sein zu sehen, was in den kommenden Jahren entwickelt wird.
Ein letzter kurzer Kommentar: Ich wurde oft gefragt, warum wir uns für C++ für Envoy entschieden haben. Der Grund liegt nach wie vor darin, dass es die einzige weit verbreitete Programmiersprache auf industriellem Niveau ist, mit der die in diesem Beitrag beschriebene Architektur erstellt werden kann. C++ eignet sich nicht für alle oder sogar viele Projekte, aber für bestimmte Anwendungsfälle ist es immer noch das einzige Werkzeug, um die Aufgabe zu erledigen.
Links zum Code
Links zu den Interface- und Implementierungsheader-Dateien, die in diesem Beitrag besprochen werden:
Quelle: habr.com
