Artikelvertaling:
Dit artikel leek me behoorlijk interessant en aangezien Envoy meestal wordt gebruikt als onderdeel van 'istio' of gewoon als 'ingress controller' voor Kubernetes, hebben de meeste mensen niet zo'n directe interactie ermee als bijvoorbeeld met typische installaties van Nginx of Haproxy. Toch zou het goed zijn om te begrijpen hoe het van binnenuit werkt als er iets misgaat. Ik heb geprobeerd zoveel mogelijk tekst naar het Nederlands te vertalen, inclusief speciale termen; voor degenen die dat moeilijk vinden om te zien, heb ik de oorspronkelijke termen tussen haakjes gelaten. Welkom onder de kat.
De technische documentatie op laag niveau over de codebasis van Envoy is momenteel vrij schaars. Om dit aan te pakken, ben ik van plan een serie blogartikelen te schrijven over verschillende subsystemen van Envoy. Aangezien dit het eerste artikel is, laat me alsjeblieft weten wat je ervan vindt en wat je wellicht interessant zou vinden voor de volgende artikelen.
Een van de meest voorkomende technische vragen die ik ontvang over Envoy, is een verzoek om een laagdrempelige beschrijving van het gebruikte threading model. In deze post zal ik beschrijven hoe Envoy verbindingen aan threads toewijst, evenals een beschrijving van het systeem voor thread local storage dat intern wordt gebruikt om de code meer parallel en performant te maken.
Overzicht van de threads
![[Vertaling] Envoy threading model](/wp-content/uploads/2019/04/c5028463db1eb5b07ccb1a3af1f64a03.jpeg)
Envoy gebruikt drie verschillende types threads:
- Hoofd (Main): Deze thread is verantwoordelijk voor het starten en beĆ«indigen van het proces, de verwerking van de XDS (xDiscovery Service) API, inclusief DNS, gezondheidscontrole, algemeen clusterbeheer en de runtime van de service, het resetten van statistieken, administratieve taken en algemeen procesbeheer ā Linux signalen, een warme herstart, enzovoort. Alles wat in deze thread gebeurt, is asynchroon en 'non-blocking'. Over het algemeen coƶrdineert de hoofdthread alle kritieke processen waarvan de functionaliteit geen grote hoeveelheid CPU vereist. Dit stelt ons in staat om het grootste deel van de beheercode te schrijven alsof het enkelvoudig is.
- Werker (Worker): Standaard creƫert Envoy een werkthread voor elke hardwarethread in het systeem, dit kan worden beheerd met de optie
--concurrency. Elke werkthread start een "non-blocking" event loop, die verantwoordelijk is voor het luisteren naar elke listener. Op het moment van schrijven van dit artikel (29 juli 2017) zijn er geen segmentaties (sharding) van de listener, nieuwe verbindingen accepteren, een instantie van de filterstack voor de verbinding aanmaken en alle I/O-operaties afhandelen gedurende de levensduur van de verbinding. Ook hiermee kan het merendeel van de verbindingverwerkingscode worden geschreven alsof deze ƩƩn-threaded is. - Bestand (File flusher): Elk bestand dat Envoy schrijft, voornamelijk toegang logs, heeft momenteel een onafhankelijke blocking thread. Dit komt omdat schrijven naar bestanden die door het bestandssysteem in cache zijn opgeslagen, zelfs met gebruik van
O_NONBLOCKsoms kan blokkeren (zucht). Wanneer werkthreads naar een bestand moeten schrijven, worden de gegevens feitelijk naar een buffer in het geheugen verplaatst, waar ze uiteindelijk worden weggeschreven via de file flush. Dit is een van de gebieden in de code waarin alle werkthreads technisch gezien dezelfde lock kunnen blokkeren (block) tijdens het proberen de geheugenbuffer te vullen.
Verbinden (Connection handling)
Zoals kort hierboven besproken, luisteren alle werkthreads naar alle listeners zonder enige segmentatie. Op deze manier wordt de kernel gebruikt om de ontvangen sockets efficiƫnt naar de werkthreads te sturen. Moderne kernels zijn hier over het algemeen erg goed in, zij maken gebruik van functies zoals I/O-prioriteitsverhoging om te proberen de thread van werk te vullen voordat ze andere threads gebruiken die ook naar dezelfde socket luisteren, en vermijden ook het gebruik van spinlock voor het afhandelen van elk verzoek.
Zodra de verbinding op de werkthread is aangenomen, verlaat deze nooit die thread. Alle verdere verwerking van de verbinding wordt volledig afgehandeld in de werkthread, inclusief enig doorstuurgedrag.
Dit heeft verschillende belangrijke gevolgen:
- Alle connectiepools in Envoy behoren tot de werkthread. Hierdoor, hoewel HTTP/2 connectiepools slechts ƩƩn verbinding met elke upstream host tegelijk maken, zal er bij vier werkthreads vier HTTP/2 verbindingen naar de upstream host zijn in een stabiele toestand.
- De reden waarom Envoy op deze manier werkt, is dat door alles binnen ƩƩn werkthread te houden, bijna alle code zonder blokkeringen kan worden geschreven en het lijkt alsof het ƩƩn-threaded is. Dit ontwerp vereenvoudigt het schrijven van een grote hoeveelheid code en schaalt ongelooflijk goed voor vrijwel onbeperkte aantallen werkthreads.
- Echter, een van de belangrijkste bevindingen is dat, vanuit het oogpunt van efficiƫntie van het geheugen- en verbindingspool, het eigenlijk zeer belangrijk is om de parameter
--concurrencyin te stellen. Het hebben van meer werkthreads dan nodig leidt tot geheugenverspilling, het creƫren van meer inactieve verbindingen en een lagere snelheid van toegang tot de verbindingspool. Bij Lyft draaien onze envoy sidecar-containers met zeer lage paralleliteit, zodat de prestaties ongeveer overeenkomen met de diensten naast hen. We draaien Envoy als een edge proxy alleen bij maximale paralleliteit (concurrentie).
Wat niet-blokkeren betekent (What non-blocking means)
De term āniet-blokkerendā is tot nu toe enkele keren gebruikt bij de bespreking van hoe de hoofd- en werkthreads functioneren. Alle code is geschreven met de veronderstelling dat niets ooit wordt geblokkeerd. Echter, dat is niet helemaal waar (wat is er niet helemaal waar?).
Envoy gebruikt verschillende langdurige procesblokkeringen:
- Zoals eerder vermeld, ontvangen alle werkthreads dezelfde blokkering voordat ze de logbuffer in het geheugen vullen. De vasthoudtijd van de blokkering moet zeer laag zijn, maar het is mogelijk dat deze blokkering wordt aangevochten bij hoge paralleliteit en hoge doorvoersnelheid.
- Envoy maakt gebruik van een zeer geavanceerd systeem voor het verwerken van statistieken, dat lokaal is voor de stroom. Dit zal het onderwerp zijn van een aparte post. Desondanks zal ik kort vermelden dat, als onderdeel van de lokale verwerking van stroomstatistieken, het soms nodig is om een lock te verkrijgen voor de centrale 'statistiekopslag'. Deze lock zou nooit nodig mogen zijn.
- De hoofdstream heeft periodiek coƶrdinatie nodig met alle werkspecialisten. Dit gebeurt door 'publicatie' vanuit de hoofdstream naar de werkspecialisten en soms ook van de werkspecialisten terug naar de hoofdstream. Voor verzending is een lock vereist, zodat het gepubliceerde bericht in de wachtrij kan worden geplaatst voor latere levering. Deze locks zouden nooit onder zware concurrentie mogen staan, maar ze kunnen technisch gezien nog steeds worden geblokkeerd.
- Wanneer Envoy logt naar de systeemfoutstroom (standard error), krijgt het een lock voor het hele proces. Over het algemeen wordt lokale logging door Envoy als slecht voor de prestaties beschouwd, daarom wordt er niet veel aandacht aan de verbetering besteed.
- Er zijn enkele andere willekeurige locks, maar geen van deze is kritiek voor de prestaties en zou nooit betwist moeten worden.
Thread local storage
Vanwege de manier waarop Envoy de verantwoordelijkheden van de hoofdstream scheidt van die van de werkspecialisten, bestaat er een vereiste dat complexe verwerking in de hoofdstream kan worden uitgevoerd en vervolgens met een hoge mate van parallelisme aan elke werkspecialist kan worden verstrekt. Dit gedeelte beschrijft het Envoy Thread Local Storage (TLS) systeem op hoog niveau. In het volgende gedeelte zal ik beschrijven hoe dit wordt gebruikt voor clusterbeheer.
![[Vertaling] Envoy threading model](/wp-content/uploads/2019/04/a47af1e8eb1f55a4d7609b3ff3ee9ce1.jpeg)
Zoals al beschreven is, beheert de hoofdstream praktisch alle beheersfuncties en functionaliteit van het controlevlak in het Envoy-proces. Het controlevlak is hier een beetje overbelast, maar als je het binnen het proces van Envoy zelf bekijkt en vergelijkt met de verzending die door de werkspecialisten wordt uitgevoerd, lijkt dit zinvol. Als algemene regel voert het proces van de hoofdstream enig werk uit en moet het vervolgens elke werkspecialist bijwerken op basis van de resultaten van dit werk. De werkthread hoeft echter geen vergrendeling in te stellen bij elke toegang..
Het TLS (Thread Local Storage) systeem van Envoy werkt als volgt:
- De code die op de hoofdthread draait, kan een TLS-slot voor het gehele 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 instellen. Wanneer dit is gedaan, worden de gegevens gepubliceerd in elke werkthread als een gewoon evenement van de gebeurtenislus.
- Werkthreads kunnen lezen uit hun TLS-slot en elke beschikbare lokale threadgegevens daar extraheren.
Hoewel dit een zeer eenvoudige en ongelooflijk krachtige paradigma is, die zeer vergelijkbaar is met het concept van RCU (Read-Copy-Update) vergrendeling. In wezen zien werkthreads nooit wijzigingen in de gegevens in de TLS-slots tijdens het uitvoeren van hun taken. Wijzigingen gebeuren alleen in de rustperiode tussen werk-events.
Envoy gebruikt dit op twee verschillende manieren:
- Door verschillende gegevens in elke werkthread op te slaan, wordt toegang tot deze gegevens zonder enige vergrendeling verkregen.
- Door een gedeelde pointer naar globale gegevens in een āalleen-lezenā modus in elke werkthread op te slaan. Zo heeft elke werkthread een referentieteller voor de gegevens die tijdens het werk niet kan worden verminderd. Pas wanneer alle werknemers tot rust komen en nieuwe gedeelde gegevens laden, worden de oude gegevens vernietigd. Dit is identiek aan RCU.
Cluster update threading
In dit gedeelte zal ik beschrijven hoe TLS (Thread Local Storage) wordt gebruikt voor clusterbeheer. Clusterbeheer omvat het verwerken van API xDS en/of DNS, evenals gezondheidscontroles.
![[Vertaling] Envoy threading model](/wp-content/uploads/2019/04/238f5718729e1f12f8f0d6507f16abe8.jpeg)
Cluster thread management omvat de volgende componenten en fasen:
- De clusterbeheerder is een component binnen Envoy die al de bekende upstreams van het cluster beheert, de API-interface CDS (Cluster Discovery Service), de API-interfaces SDS (Secret Discovery Service) en EDS (Endpoint Discovery Service), DNS en actieve externe gezondheidscontroles. Hij is verantwoordelijk voor het creĆ«ren van een āuiteindelijk consistenteā weergave van elke upstream van het cluster, inclusief de ontdekte hosts en de gezondheidsstatus.
- De health checker voert actieve beschikbaarheidscontroles uit en rapporteert veranderingen in de beschikbaarheidstoestand aan de clusterbeheerder.
- CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS worden uitgevoerd om te bepalen tot welk cluster men behoort. Veranderingen in de toestand worden teruggerapporteerd aan de clusterbeheerder.
- Elke worker-thread voert voortdurend een cyclus van gebeurtenisverwerking uit.
- Wanneer de clusterbeheerder vaststelt dat de toestand van het cluster is veranderd, creƫert hij een nieuwe, alleen-lezen snapshot van de clusterstatus en verzendt deze naar elke worker-thread.
- Tijdens de volgende idle-periode zal de worker-thread de snapshot bijwerken in de toegewezen TLS-slot.
- Tijdens een invoer- of uitvoerevenement dat de host voor load balancing moet bepalen, vraagt de load balancer om de TLS-slot (Thread Local Storage) om informatie over de host te verkrijgen. Dit vereist geen vergrendelingen. Merk ook op dat TLS ook gebeurtenissen kan initiƫren bij updates, zodat de load balancing-subsystemen en andere componenten de caches, gegevensstructuren, enz. kunnen recalculeren. Dit valt buiten de scope van deze post, maar wordt op verschillende plaatsen in de code gebruikt.
Met de hierboven beschreven procedure kan Envoy elke aanvraag zonder enige vergrendelingen verwerken (behalve de eerder beschreven). Behalve de complexiteit van de TLS-code hoeft het grootste deel van de code niet te begrijpen hoe multithreading werkt, en kan het in single-thread modus worden geschreven. Dit vergemakkelijkt het schrijven van het grootste deel van de code naast de uitstekende prestaties.
Andere subsystemen die gebruik maken van TLS
TLS (Thread Local Storage) en RCU (Read Copy Update) worden uitgebreid gebruikt in Envoy.
Voorbeelden van gebruik:
- Mechanisme voor het wijzigen van functionaliteit tijdens uitvoering: De huidige lijst van ingeschakelde functionaliteiten wordt berekend in de hoofdthread. Vervolgens krijgt elke worker-thread een alleen-lezen snapshot met behulp van de RCU-semantiek.
- Vervanging van routetabellen: voor de route-tabellen die door de RDS (Route Discovery Service) worden geleverd, worden route-tabellen in de hoofdthread aangemaakt. Een alleen-lezen snapshot zal later aan elke werkthread worden verleend via de RCU-semantiek (Read Copy Update). Dit maakt het wijzigen van route-tabellen atomair en efficiƫnt.
- HTTP-header caching: Het blijkt dat het berekenen van de HTTP-header voor elke aanvraag (bij ~25K+ RPS per kern) behoorlijk duur is. Envoy berekent de header centraal ongeveer elke halve seconde en biedt deze aan elke worker aan via TLS en RCU.
Er zijn andere gevallen, maar de eerdere voorbeelden zouden een goed begrip moeten bieden van waarvoor TLS wordt gebruikt.
Bekende prestatievalkuilen (Known performance pitfalls)
Hoewel Envoy over het algemeen goed presteert, zijn er enkele bekende gebieden die aandacht vereisen wanneer het wordt gebruikt met zeer hoge paralleliteit en doorvoer:
- Zoals al in dit artikel is beschreven, krijgen momenteel alle werkthreads een lock bij het schrijven naar de geheugenbuffer van het toegangsbewijs. Bij hoge paralleliteit en hoge doorvoer zal het nodig zijn om de toegangsbewijzen te bundelen voor elke werkthread ten koste van ongeordende levering bij het schrijven naar het finale bestand. Als alternatief kunnen afzonderlijke toegangsbewijzen worden aangemaakt voor elke werkthread.
- Hoewel statistieken zeer goed zijn geoptimaliseerd, zal er waarschijnlijk atomaire concurrentie zijn op individuele statistieken bij zeer hoge paralleliteit en doorvoer. Een oplossing voor dit probleem zijn tellers per werkthread met periodieke reset van centrale tellers. Dit zal in een volgend bericht worden besproken.
- De huidige architectuur functioneert niet goed als Envoy wordt uitgerold in een scenario met zeer weinig verbindingen die aanzienlijke middelen vereisen om te verwerken. Er is geen garantie dat de verbindingen gelijkmatig worden verdeeld over de werkthreads. Dit kan worden opgelost door werkverbindingen te balanceren, waarbij de mogelijkheid om verbindingen tussen werkthreads uit te wisselen wordt geĆÆmplementeerd.
Conclusie (Conclusion)
Het Envoy-threadmodel is ontworpen voor eenvoudige programmering en massale parallelisme, met het potentieel voor inefficiƫnt geheugengebruik en verbindingen als ze niet goed zijn geconfigureerd. Dit model laat het zeer goed presteren bij een extreem hoog aantal threads en dataverkeer.
Zoals ik kort op Twitter heb genoemd, kan het ontwerp ook bovenop een volledig functionele netstack in de gebruikersmodus werken, zoals DPDK (Data Plane Development Kit), wat kan resulteren in gewone servers die miljoenen verzoeken per seconde verwerken met volledige L7-verwerking. Het zal erg interessant zijn om te zien wat er de komende jaren gebouwd zal worden.
Een laatste snelle opmerking: ik ben vaak gevraagd waarom we voor C++ hebben gekozen voor Envoy. De reden is nog steeds dat het de enige breed gebruikte industriƫle taal is waarmee de architectuur in deze post kan worden gebouwd. C++ is zeker niet geschikt voor iedereen of zelfs voor veel projecten, maar voor bepaalde gebruikssituaties is het nog steeds het enige hulpmiddel om de klus te klaren.
Links naar code
Links naar de bestanden met interfaces en de implementatie van headers die in deze post worden besproken:
Bron: habr.com
