[Vertaling] Envoy-inrijgmodel

Vertaling van het artikel: Envoy-draadmodel - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Ik vond dit artikel behoorlijk interessant, en aangezien Envoy meestal wordt gebruikt als onderdeel van de “istio” of gewoon als de “ingress controller” van kubernetes, hebben de meeste mensen er niet dezelfde directe interactie mee als bijvoorbeeld met typische Nginx- of Haproxy-installaties. Als er echter iets kapot gaat, is het goed om te begrijpen hoe het van binnenuit werkt. Ik heb geprobeerd zoveel mogelijk van de tekst in het Russisch te vertalen, inclusief speciale woorden; voor degenen die het pijnlijk vinden om hiernaar te kijken, heb ik de originelen tussen haakjes gelaten. Welkom bij kat.

Technische documentatie op laag niveau voor de Envoy-codebase is momenteel vrij schaars. Om dit te verhelpen, ben ik van plan een reeks blogposts te schrijven over de verschillende subsystemen van Envoy. Aangezien dit het eerste artikel is, kunt u mij in toekomstige artikelen laten weten wat u ervan vindt en waarin u mogelijk geïnteresseerd bent.

Een van de meest voorkomende technische vragen die ik over Envoy ontvang, is het vragen om een ​​beschrijving op laag niveau van het draadsnijmodel dat het gebruikt. In dit bericht beschrijf ik hoe Envoy verbindingen met threads in kaart brengt, evenals het Thread Local Storage-systeem dat het intern gebruikt om code meer parallel en krachtiger te maken.

Overzicht draadsnijden

[Vertaling] Envoy-inrijgmodel

Envoy gebruikt drie verschillende soorten streams:

  • Voornaamst: Deze thread regelt het opstarten en beëindigen van processen, alle verwerkingen van de XDS (xDiscovery Service) API, inclusief DNS, gezondheidscontrole, algemeen cluster- en runtimebeheer, resetten van statistieken, administratie en algemeen procesbeheer - Linux-signalen, hot restart, enz. Alles dat wat er in deze thread gebeurt, is asynchroon en "niet-blokkerend". Over het algemeen coördineert de hoofdthread alle kritieke functionaliteitsprocessen waarvoor geen grote hoeveelheid CPU nodig is. Hierdoor kan de meeste besturingscode worden geschreven alsof deze single-threaded is.
  • Arbeider: Standaard maakt Envoy een werkthread voor elke hardwarethread in het systeem. Dit kan worden beheerd met behulp van de optie --concurrency. Elke werkthread voert een “niet-blokkerende” gebeurtenislus uit, die verantwoordelijk is voor het luisteren naar elke luisteraar; op het moment van schrijven (29 juli 2017) is er geen sprake van sharding van de luisteraar, het accepteren van nieuwe verbindingen, het instantiëren van een filterstack voor de verbinding, en het verwerken van alle invoer/uitvoer (IO)-bewerkingen tijdens de levensduur van de verbinding. Nogmaals, hierdoor kan de meeste verbindingsafhandelingscode worden geschreven alsof deze een enkele thread heeft.
  • Vijlspoeler: Elk bestand dat Envoy schrijft, voornamelijk toegangslogboeken, heeft momenteel een onafhankelijke blokkerende thread. Dit komt door het feit dat er zelfs tijdens het gebruik wordt geschreven naar bestanden die door het bestandssysteem in de cache zijn opgeslagen O_NONBLOCK kan soms geblokkeerd raken (zucht). Wanneer werkthreads naar een bestand moeten schrijven, worden de gegevens feitelijk naar een buffer in het geheugen verplaatst, waar ze uiteindelijk door de thread worden gespoeld bestand doorspoelen. Dit is een codegebied waar technisch gezien alle werkthreads hetzelfde slot kunnen blokkeren terwijl ze proberen een geheugenbuffer te vullen.

Verbindingsafhandeling

Zoals hierboven kort besproken, luisteren alle werkthreads naar alle luisteraars zonder enige sharding. De kernel wordt dus gebruikt om geaccepteerde sockets op een elegante manier naar werkthreads te sturen. Moderne kernels zijn hier over het algemeen erg goed in, ze gebruiken functies zoals input/output (IO) prioriteitsversterking om te proberen een thread met werk te vullen voordat ze andere threads gaan gebruiken die ook op dezelfde socket luisteren, en ook geen gebruik maken van round robin vergrendeling (Spinlock) om elk verzoek te verwerken.
Zodra een verbinding op een werkthread is geaccepteerd, verlaat deze deze thread nooit meer. Alle verdere verwerking van de verbinding wordt volledig in de werkthread afgehandeld, inclusief eventueel doorstuurgedrag.

Dit heeft een aantal belangrijke gevolgen:

  • Alle verbindingspools in Envoy zijn toegewezen aan een werkthread. Dus hoewel HTTP/2-verbindingspools slechts één verbinding tegelijk maken met elke upstream-host, zullen er, als er vier werkthreads zijn, vier HTTP/2-verbindingen per upstream-host in een stabiele toestand zijn.
  • De reden dat Envoy op deze manier werkt, is dat door alles op één werkthread te houden, bijna alle code zonder blokkering kan worden geschreven alsof het een enkele thread is. Dit ontwerp maakt het gemakkelijk om veel code te schrijven en kan ongelooflijk goed worden geschaald naar een vrijwel onbeperkt aantal werkthreads.
  • Een van de belangrijkste punten is echter dat het vanuit het oogpunt van geheugenpool en verbindingsefficiëntie eigenlijk heel belangrijk is om de --concurrency. Als er meer werkthreads zijn dan nodig is, wordt er geheugen verspild, worden meer inactieve verbindingen gecreëerd en wordt de snelheid van het poolen van verbindingen verminderd. Bij Lyft werken onze gezant-zijspancontainers met een zeer lage gelijktijdigheid, zodat de prestaties ongeveer overeenkomen met de services waar ze naast zitten. We voeren Envoy alleen uit als edge-proxy met maximale gelijktijdigheid.

Wat betekent niet-blokkeren?

De term 'niet-blokkerend' is tot nu toe meerdere keren gebruikt bij de bespreking van de werking van de hoofdthread en de werkthread. Alle code is geschreven in de veronderstelling dat er nooit iets wordt geblokkeerd. Dit is echter niet helemaal waar (wat is niet helemaal waar?).

Envoy gebruikt verschillende lange procesvergrendelingen:

  • Zoals besproken krijgen alle werkthreads bij het schrijven van toegangslogboeken dezelfde vergrendeling voordat de logbuffer in het geheugen wordt gevuld. De vergrendelingstijd moet zeer laag zijn, maar het is mogelijk dat de vergrendeling wordt betwist met een hoge gelijktijdigheid en hoge doorvoer.
  • Envoy gebruikt een zeer complex systeem om statistieken te verwerken die lokaal zijn voor de thread. Dit zal het onderwerp zijn van een apart bericht. Ik zal echter kort vermelden dat het, als onderdeel van het lokaal verwerken van threadstatistieken, soms nodig is om een ​​centrale "statistiekenopslag" te vergrendelen. Deze vergrendeling zou nooit nodig moeten zijn.
  • De hoofdthread moet periodiek worden gecoördineerd met alle werkthreads. Dit wordt gedaan door te "publiceren" van de hoofdthread naar werkthreads, en soms van werkthreads terug naar de hoofdthread. Voor het verzenden is een vergrendeling vereist, zodat het gepubliceerde bericht in de wachtrij kan worden geplaatst voor latere bezorging. Deze sloten mogen nooit serieus worden betwist, maar technisch gezien kunnen ze nog steeds worden geblokkeerd.
  • Wanneer Envoy een logboek naar de systeemfoutenstroom (standaardfout) schrijft, wordt het hele proces vergrendeld. Over het algemeen wordt de lokale logboekregistratie van Envoy vanuit prestatieoogpunt als verschrikkelijk beschouwd, dus er is niet veel aandacht besteed aan het verbeteren ervan.
  • Er zijn nog een paar andere willekeurige vergrendelingen, maar geen daarvan is prestatiekritisch en mag nooit worden aangevochten.

Lokale opslag van threads

Vanwege de manier waarop Envoy de verantwoordelijkheden van de hoofdthread scheidt van de verantwoordelijkheden van de werkthread, is er een vereiste dat complexe verwerking op de hoofdthread kan worden uitgevoerd en vervolgens op een zeer gelijktijdige manier aan elke werkthread kan worden verstrekt. In dit gedeelte wordt Envoy Thread Local Storage (TLS) op een hoog niveau beschreven. In de volgende sectie zal ik beschrijven hoe het wordt gebruikt om een ​​cluster te beheren.
[Vertaling] Envoy-inrijgmodel

Zoals reeds beschreven, verzorgt de hoofdthread vrijwel alle beheer- en controlevlakfunctionaliteit in het Envoy-proces. Het besturingsvlak is hier een beetje overbelast, maar als je ernaar kijkt binnen het Envoy-proces zelf en het vergelijkt met het doorsturen van de werkthreads, is het logisch. De algemene regel is dat het hoofdthreadproces wat werk doet, en dat het vervolgens elke werkthread moet bijwerken op basis van het resultaat van dat werk. in dit geval hoeft de werkthread niet bij elke toegang te worden vergrendeld.

Het TLS-systeem (Thread local storage) van Envoy werkt als volgt:

  • Code die op de hoofdthread draait, kan een TLS-slot voor het hele proces toewijzen. Hoewel dit abstract is, is het in de praktijk een index in een vector, die O(1)-toegang biedt.
  • De hoofdthread kan willekeurige gegevens in zijn slot installeren. Wanneer dit is gebeurd, worden de gegevens naar elke werkthread gepubliceerd als een normale gebeurtenislusgebeurtenis.
  • Werkthreads kunnen vanuit hun TLS-slot lezen en alle thread-lokale gegevens ophalen die daar beschikbaar zijn.

Hoewel het een heel eenvoudig en ongelooflijk krachtig paradigma is, lijkt het sterk op het concept van RCU-blokkering (Read-Copy-Update). In wezen zien werkthreads nooit gegevenswijzigingen in de TLS-slots terwijl het werk wordt uitgevoerd. Verandering vindt alleen plaats tijdens de rustperiode tussen werkgebeurtenissen.

Envoy gebruikt dit op twee verschillende manieren:

  • Door verschillende gegevens op elke werkthread op te slaan, zijn de gegevens zonder enige blokkering toegankelijk.
  • Door op elke werkthread een gedeelde verwijzing naar globale gegevens in de alleen-lezenmodus te behouden. Elke werkthread heeft dus een aantal gegevensreferenties dat niet kan worden verlaagd terwijl het werk wordt uitgevoerd. Alleen als alle werknemers kalmeren en nieuwe gedeelde gegevens uploaden, zullen de oude gegevens worden vernietigd. Dit is identiek aan RCU.

Threading van clusterupdates

In deze sectie zal ik beschrijven hoe TLS (Thread local storage) wordt gebruikt om een ​​cluster te beheren. Clusterbeheer omvat xDS API- en/of DNS-verwerking, evenals statuscontrole.
[Vertaling] Envoy-inrijgmodel

Clusterstroombeheer omvat de volgende componenten en stappen:

  1. De Cluster Manager is een onderdeel binnen Envoy dat alle bekende cluster-upstreams, de Cluster Discovery Service (CDS) API, de Secret Discovery Service (SDS) en Endpoint Discovery Service (EDS) API's, DNS en actieve externe controles beheert. Het is verantwoordelijk voor het creëren van een "uiteindelijk consistent" beeld van elk upstream-cluster, dat zowel ontdekte hosts als de gezondheidsstatus omvat.
  2. De statuschecker voert een actieve statuscontrole uit en rapporteert wijzigingen in de statusstatus aan de clustermanager.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS worden uitgevoerd om het clusterlidmaatschap te bepalen. De statuswijziging wordt teruggestuurd naar de clustermanager.
  4. Elke werkthread voert continu een gebeurtenislus uit.
  5. Wanneer de clustermanager vaststelt dat de status van een cluster is gewijzigd, wordt er een nieuwe alleen-lezen momentopname gemaakt van de status van het cluster en wordt deze naar elke werkthread verzonden.
  6. Tijdens de volgende stille periode zal de werkthread de momentopname in het toegewezen TLS-slot bijwerken.
  7. Tijdens een I/O-gebeurtenis die moet bepalen op welke host de belasting moet worden verdeeld, vraagt ​​de load balancer om een ​​TLS-slot (Thread local storage) om informatie over de host te verkrijgen. Hiervoor zijn geen sloten nodig. Houd er ook rekening mee dat TLS ook update-gebeurtenissen kan activeren, zodat load balancers en andere componenten caches, datastructuren, enz. opnieuw kunnen berekenen. Dit valt buiten het bestek van dit bericht, maar wordt op verschillende plaatsen in de code gebruikt.

Met behulp van de bovenstaande procedure kan Envoy elk verzoek verwerken zonder enige blokkering (behalve zoals eerder beschreven). Afgezien van de complexiteit van de TLS-code zelf, hoeft het grootste deel van de code niet te begrijpen hoe multithreading werkt en kan deze in single-threading worden geschreven. Dit maakt het grootste deel van de code gemakkelijker te schrijven en levert superieure prestaties op.

Andere subsystemen die gebruik maken van TLS

TLS (Thread local storage) en RCU (Read Copy Update) worden veel gebruikt in Envoy.

Voorbeelden van gebruik:

  • Mechanisme voor het wijzigen van de functionaliteit tijdens de uitvoering: De huidige lijst met ingeschakelde functionaliteit wordt berekend in de hoofdthread. Elke werkthread krijgt vervolgens een alleen-lezen momentopname met behulp van RCU-semantiek.
  • Routetabellen vervangen: Voor routetabellen die worden geleverd door RDS (Route Discovery Service), worden de routetabellen gemaakt op de hoofdthread. De alleen-lezen momentopname wordt vervolgens aan elke werkthread verstrekt met behulp van RCU-semantiek (Read Copy Update). Dit maakt het wijzigen van routetabellen atomair efficiënt.
  • Caching van HTTP-headers: Het blijkt dat het berekenen van de HTTP-header voor elk verzoek (terwijl ~25K+ RPS per core wordt uitgevoerd) behoorlijk duur is. Envoy berekent de header ongeveer elke halve seconde centraal en verstrekt deze aan elke medewerker via TLS en RCU.

Er zijn andere gevallen, maar de voorgaande voorbeelden moeten een goed inzicht geven in waarvoor TLS wordt gebruikt.

Bekende prestatievalkuilen

Hoewel Envoy over het algemeen vrij goed presteert, zijn er een paar opmerkelijke gebieden die aandacht vereisen wanneer het wordt gebruikt met een zeer hoge gelijktijdigheid en doorvoer:

  • Zoals beschreven in dit artikel krijgen momenteel alle werkthreads een vergrendeling bij het schrijven naar de geheugenbuffer van het toegangslogboek. Bij hoge gelijktijdigheid en hoge doorvoer moet u de toegangslogboeken voor elke werkthread in een batch verwerken, wat ten koste gaat van levering buiten de juiste volgorde bij het schrijven naar het definitieve bestand. Als alternatief kunt u voor elke werkthread een afzonderlijk toegangslogboek maken.
  • Hoewel de statistieken in hoge mate geoptimaliseerd zijn, zal er bij zeer hoge gelijktijdigheid en doorvoer waarschijnlijk atomaire twist ontstaan ​​over individuele statistieken. De oplossing voor dit probleem zijn tellers per werkthread met periodieke reset van de centrale tellers. Dit zal in een volgende post worden besproken.
  • De huidige architectuur zal niet goed werken als Envoy wordt ingezet in een scenario waarin er zeer weinig verbindingen zijn die aanzienlijke verwerkingsbronnen vereisen. Er is geen garantie dat verbindingen gelijkmatig over de werkthreads worden verdeeld. Dit kan worden opgelost door het balanceren van werkverbindingen te implementeren, waardoor de uitwisseling van verbindingen tussen werkthreads mogelijk wordt.

Conclusie

Het threadingmodel van Envoy is ontworpen om programmeergemak en enorm parallellisme te bieden, ten koste van mogelijk verspillend geheugen en verbindingen als het niet correct wordt geconfigureerd. Met dit model kan het zeer goed presteren bij zeer hoge draadaantallen en doorvoer.
Zoals ik kort op Twitter vermeldde, kan het ontwerp ook draaien bovenop een volledige netwerkstack in gebruikersmodus, zoals DPDK (Data Plane Development Kit), wat ertoe kan leiden dat conventionele servers miljoenen verzoeken per seconde verwerken met volledige L7-verwerking. Het zal heel interessant zijn om te zien wat er de komende jaren wordt gebouwd.
Nog een laatste korte opmerking: mij is vaak gevraagd waarom we C++ voor Envoy hebben gekozen. De reden blijft dat het nog steeds de enige veelgebruikte taal van industriële kwaliteit is waarin de in dit bericht beschreven architectuur kan worden gebouwd. C++ is zeker niet geschikt voor alle of zelfs veel projecten, maar voor bepaalde gebruiksscenario's is het nog steeds het enige hulpmiddel om de klus te klaren.

Links naar code

Links naar bestanden met interfaces en header-implementaties die in dit bericht worden besproken:

Bron: www.habr.com

Voeg een reactie