[Översättning] Envoy threading modell

Översättning av artikeln: Envoy threading modell - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Jag tyckte den här artikeln var ganska intressant, och eftersom Envoy oftast används som en del av "istio" eller helt enkelt som "ingress controller" för kubernetes, har de flesta människor inte samma direkta interaktion med den som till exempel med typiska Nginx- eller Haproxy-installationer. Men om något går sönder skulle det vara bra att förstå hur det fungerar från insidan. Jag försökte översätta så mycket av texten till ryska som möjligt, inklusive speciella ord, för de som tycker det är smärtsamt att titta på detta lämnade jag originalen inom parentes. Välkommen till katten.

Teknisk dokumentation på låg nivå för Envoy-kodbasen är för närvarande ganska sparsam. För att råda bot på detta planerar jag att göra en serie blogginlägg om Envoys olika delsystem. Eftersom detta är den första artikeln, låt mig veta vad du tycker och vad du kan vara intresserad av i framtida artiklar.

En av de vanligaste tekniska frågorna jag får om Envoy är att be om en lågnivåbeskrivning av trådningsmodellen den använder. I det här inlägget kommer jag att beskriva hur Envoy mappar anslutningar till trådar, såväl som Thread Local Storage-systemet som det använder internt för att göra koden mer parallell och högpresterande.

Trådöversikt

[Översättning] Envoy threading modell

Envoy använder tre olika typer av strömmar:

  • Huvudsakliga: Den här tråden kontrollerar processstart och avslutning, all bearbetning av XDS (xDiscovery Service) API, inklusive DNS, hälsokontroll, allmän kluster- och runtime-hantering, statistikåterställning, administration och allmän processhantering - Linux-signaler. het omstart, etc. Allt som händer i denna tråd är asynkron och "icke-blockerande". I allmänhet koordinerar huvudtråden alla kritiska funktionsprocesser som inte kräver en stor mängd CPU för att köras. Detta gör att de flesta kontrollkoder kan skrivas som om den vore entrådig.
  • Arbetstagare: Som standard skapar Envoy en arbetstråd för varje hårdvarutråd i systemet, detta kan styras med alternativet --concurrency. Varje arbetstråd kör en "icke-blockerande" händelseloop, som är ansvarig för att lyssna på varje lyssnare; i skrivande stund (29 juli 2017) sker ingen skärning av lyssnaren, acceptera nya anslutningar, instansiera en filterstack för anslutningen och bearbetning av alla in-/utgångsoperationer (IO) under anslutningens livstid. Återigen tillåter detta att de flesta anslutningshanteringskoder kan skrivas som om den vore entrådad.
  • Filspolare: Varje fil som Envoy skriver, huvudsakligen åtkomstloggar, har för närvarande en oberoende blockeringstråd. Detta beror på det faktum att skriva till filer som cachelagras av filsystemet även när du använder O_NONBLOCK kan ibland bli blockerad (suck). När arbetartrådar behöver skriva till en fil flyttas data faktiskt till en buffert i minnet där den så småningom spolas genom tråden filspolning. Detta är ett kodområde där tekniskt sett alla arbetartrådar kan blockera samma lås medan de försöker fylla en minnesbuffert.

Anslutningshantering

Som diskuterats kort ovan, lyssnar alla arbetartrådar på alla lyssnare utan att det går sönder. Sålunda används kärnan för att graciöst skicka accepterade sockets till arbetartrådar. Moderna kärnor är generellt mycket bra på detta, de använder funktioner som input/output (IO) priority boosting för att försöka fylla en tråd med arbete innan de börjar använda andra trådar som också lyssnar på samma socket, och inte heller använder round robin låsning (Spinlock) för att behandla varje begäran.
När en anslutning väl har accepterats på en arbetstråd lämnar den aldrig den tråden. All vidarebearbetning av anslutningen hanteras helt i arbetartråden, inklusive eventuell vidarebefordran.

Detta har flera viktiga konsekvenser:

  • Alla anslutningspooler i Envoy är tilldelade en arbetstråd. Så även om HTTP/2-anslutningspooler bara gör en anslutning till varje uppströmsvärd åt gången, om det finns fyra arbetartrådar, kommer det att finnas fyra HTTP/2-anslutningar per uppströmsvärd i ett stabilt tillstånd.
  • Anledningen till att Envoy fungerar på det här sättet är att genom att hålla allt på en enda arbetstråd kan nästan all kod skrivas utan blockering och som om den vore enkeltrådad. Denna design gör det enkelt att skriva mycket kod och skalas otroligt bra till ett nästan obegränsat antal arbetartrådar.
  • En av de viktigaste fördelarna är dock att ur minnespool och anslutningseffektivitetssynpunkt är det faktiskt mycket viktigt att konfigurera --concurrency. Att ha fler arbetartrådar än nödvändigt kommer att slösa med minne, skapa fler lediga anslutningar och minska hastigheten för anslutningspoolning. På Lyft kör våra envoy sidvagnscontainrar med mycket låg samtidighet så att prestandan ungefär matchar de tjänster de sitter bredvid. Vi kör Envoy som en edge-proxy endast vid maximal samtidighet.

Vad betyder icke-blockering?

Termen "icke-blockerande" har hittills använts flera gånger när man diskuterar hur huvud- och arbetstrådarna fungerar. All kod är skriven under antagandet att ingenting någonsin blockeras. Detta är dock inte helt sant (vad är inte helt sant?).

Envoy använder flera långa processlås:

  • Som diskuterats, när du skriver åtkomstloggar, får alla arbetartrådar samma lås innan minnesloggbufferten fylls. Låsets hålltid bör vara mycket låg, men det är möjligt för låset att bestridas vid hög samtidighet och hög genomströmning.
  • Envoy använder ett mycket komplext system för att hantera statistik som är lokal för tråden. Detta kommer att bli ämnet för ett separat inlägg. Jag ska dock kort nämna att det som en del av att bearbeta trådstatistik lokalt ibland är nödvändigt att skaffa ett lås på en central "statistikbutik". Denna låsning bör aldrig krävas.
  • Huvudtråden måste regelbundet samordnas med alla arbetartrådar. Detta görs genom att "publicera" från huvudtråden till arbetartrådar, och ibland från arbetartrådar tillbaka till huvudtråden. För att skicka kräver ett lås så att det publicerade meddelandet kan ställas i kö för senare leverans. Dessa lås bör aldrig på allvar bestridas, men de kan fortfarande tekniskt sett blockeras.
  • När Envoy skriver en logg till systemfelströmmen (standardfel) får den ett lås på hela processen. I allmänhet anses Envoys lokala loggning vara fruktansvärd ur prestandasynpunkt, så det har inte ägnats mycket uppmärksamhet åt att förbättra den.
  • Det finns några andra slumpmässiga lås, men ingen av dem är prestandakritisk och bör aldrig utmanas.

Tråd lokal lagring

På grund av hur Envoy skiljer ansvaret för huvudtråden från ansvarsområden för arbetartråden, finns det ett krav på att komplex bearbetning kan göras på huvudtråden och sedan tillhandahållas till varje arbetartråd på ett mycket samtidigt sätt. Det här avsnittet beskriver Envoy Thread Local Storage (TLS) på en hög nivå. I nästa avsnitt kommer jag att beskriva hur det används för att hantera ett kluster.
[Översättning] Envoy threading modell

Som redan beskrivits hanterar huvudtråden praktiskt taget all hanterings- och kontrollplansfunktionalitet i Envoy-processen. Kontrollplanet är lite överbelastat här, men när man tittar på det inom själva Envoy-processen och jämför det med den vidarebefordran som arbetartrådarna gör, är det vettigt. Den allmänna regeln är att huvudtrådsprocessen fungerar en del, och sedan måste den uppdatera varje arbetartråd enligt resultatet av det arbetet. i detta fall behöver inte arbetstråden skaffa ett lås på varje åtkomst.

Envoys TLS (Thread local storage) system fungerar enligt följande:

  • Kod som körs på huvudtråden kan tilldela en TLS-plats för hela processen. Även om detta är abstraherat, är det i praktiken ett index till en vektor, vilket ger O(1)-åtkomst.
  • Huvudtråden kan installera godtyckliga data i sin plats. När detta är gjort publiceras data till varje arbetstråd som en normal händelseslingshändelse.
  • Arbetartrådar kan läsa från sin TLS-plats och hämta alla trådlokala data som finns där.

Även om det är ett väldigt enkelt och otroligt kraftfullt paradigm, är det väldigt likt konceptet med RCU-blockering (Read-Copy-Update). I huvudsak ser arbetartrådar aldrig några dataändringar i TLS-platserna medan arbetet pågår. Förändring sker endast under viloperioden mellan arbetshändelserna.

Envoy använder detta på två olika sätt:

  • Genom att lagra olika data på varje arbetstråd kan data nås utan blockering.
  • Genom att behålla en delad pekare till globala data i skrivskyddat läge på varje arbetstråd. Således har varje arbetstråd ett datareferensantal som inte kan minskas medan arbetet körs. Först när alla arbetare lugnar ner sig och laddar upp ny delad data kommer den gamla datan att förstöras. Detta är identiskt med RCU.

Tråd för klusteruppdatering

I det här avsnittet kommer jag att beskriva hur TLS (Thread local storage) används för att hantera ett kluster. Klusterhantering inkluderar xDS API och/eller DNS-bearbetning, samt hälsokontroll.
[Översättning] Envoy threading modell

Klusterflödeshantering inkluderar följande komponenter och steg:

  1. Cluster Manager är en komponent inom Envoy som hanterar alla kända kluster uppströms, Cluster Discovery Service (CDS) API, Secret Discovery Service (SDS) och Endpoint Discovery Service (EDS) API:er, DNS och aktiva externa kontroller. Den är ansvarig för att skapa en "så småningom konsekvent" vy av varje uppströmskluster, som inkluderar upptäckta värdar såväl som hälsostatus.
  2. Hälsocheckaren utför en aktiv hälsokontroll och rapporterar hälsostatusändringar till klusterhanteraren.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS utförs för att fastställa klustermedlemskap. Tillståndsändringen returneras till klusterhanteraren.
  4. Varje arbetstråd kör kontinuerligt en händelseslinga.
  5. När klusterhanteraren fastställer att tillståndet för ett kluster har ändrats, skapar den en ny skrivskyddad ögonblicksbild av klustrets tillstånd och skickar den till varje arbetstråd.
  6. Under nästa tysta period kommer arbetartråden att uppdatera ögonblicksbilden i den tilldelade TLS-platsen.
  7. Under en I/O-händelse som är tänkt att bestämma värden för att lastbalansera, kommer lastbalanseraren att begära en TLS (Thread Local Storage)-plats för att få information om värden. Detta kräver inga lås. Observera också att TLS också kan utlösa uppdateringshändelser så att lastbalanserare och andra komponenter kan beräkna cacher, datastrukturer, etc. Detta ligger utanför ramen för detta inlägg, men används på olika ställen i koden.

Genom att använda ovanstående procedur kan Envoy behandla varje begäran utan blockering (förutom som beskrivits tidigare). Bortsett från komplexiteten i själva TLS-koden behöver det mesta av koden inte förstå hur multithreading fungerar och kan skrivas entrådigt. Detta gör det mesta av koden lättare att skriva förutom överlägsen prestanda.

Andra delsystem som använder sig av TLS

TLS (Tråd lokal lagring) och RCU (Read Copy Update) används ofta i Envoy.

Exempel på användning:

  • Mekanism för att ändra funktionalitet under körning: Den aktuella listan över aktiverade funktioner beräknas i huvudtråden. Varje arbetstråd ges sedan en skrivskyddad ögonblicksbild med hjälp av RCU-semantik.
  • Byte av rutttabeller: För rutttabeller som tillhandahålls av RDS (Route Discovery Service), skapas rutttabellerna på huvudtråden. Den skrivskyddade ögonblicksbilden kommer därefter att ges till varje arbetstråd med RCU (Read Copy Update) semantik. Detta gör byte av rutttabeller atomärt effektiva.
  • HTTP-header-cache: Som det visar sig är det ganska dyrt att beräkna HTTP-huvudet för varje begäran (medan man kör ~25K+ RPS per kärna). Envoy beräknar huvudet centralt ungefär varje halv sekund och tillhandahåller det till varje arbetare via TLS och RCU.

Det finns andra fall, men de tidigare exemplen bör ge en god förståelse för vad TLS används till.

Kända prestationsfallgropar

Även om Envoy överlag presterar ganska bra, finns det några anmärkningsvärda områden som kräver uppmärksamhet när den används med mycket hög samtidighet och genomströmning:

  • Som beskrivs i den här artikeln får för närvarande alla arbetstrådar ett lås när de skriver till åtkomstloggminnesbufferten. Vid hög samtidighet och hög genomströmning kommer du att behöva batcha åtkomstloggarna för varje arbetartråd på bekostnad av leverans i oordning när du skriver till den slutliga filen. Alternativt kan du skapa en separat åtkomstlogg för varje arbetstråd.
  • Även om statistiken är mycket optimerad, vid mycket hög samtidighet och genomströmning kommer det sannolikt att uppstå atomstrid om individuell statistik. Lösningen på detta problem är räknare per arbetartråd med periodisk återställning av centrala räknare. Detta kommer att diskuteras i ett efterföljande inlägg.
  • Den nuvarande arkitekturen kommer inte att fungera bra om Envoy används i ett scenario där det finns väldigt få anslutningar som kräver betydande bearbetningsresurser. Det finns ingen garanti för att anslutningarna kommer att fördelas jämnt mellan arbetartrådarna. Detta kan lösas genom att implementera balansering av arbetaranslutningar, vilket möjliggör utbyte av anslutningar mellan arbetartrådar.

Slutsats

Envoys gängningsmodell är utformad för att ge enkel programmering och massiv parallellitet på bekostnad av potentiellt slösaktigt minne och anslutningar om de inte är konfigurerade på rätt sätt. Denna modell gör att den kan prestera mycket bra vid mycket höga trådantal och genomströmning.
Som jag kort nämnde på Twitter, kan designen också köras ovanpå en komplett nätverksstack i användarläge som DPDK (Data Plane Development Kit), vilket kan resultera i att konventionella servrar hanterar miljontals förfrågningar per sekund med full L7-bearbetning. Det ska bli väldigt intressant att se vad som byggs under de närmaste åren.
En sista snabb kommentar: Jag har fått frågan många gånger varför vi valde C++ för Envoy. Anledningen kvarstår att det fortfarande är det enda allmänt använda språket i industriklass där arkitekturen som beskrivs i detta inlägg kan byggas. C++ är definitivt inte lämplig för alla eller ens många projekt, men för vissa användningsfall är det fortfarande det enda verktyget för att få jobbet gjort.

Länkar till kod

Länkar till filer med gränssnitt och headerimplementationer som diskuteras i det här inlägget:

Källa: will.com

Lägg en kommentar