ĂversĂ€ttning av artikeln:
Den hÀr artikeln verkade mig vara 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 med typiska installationer av Nginx eller Haproxy. Men om nÄgot gÄr fel skulle det vara bra att förstÄ hur det fungerar inuti. Jag har försökt översÀtta sÄ mycket text som möjligt till ryska, inklusive specialord, för dem som har svÄrt att se pÄ sÄdant lÀmnade jag originalen inom parentes. VÀlkommen under kat.
Teknisk dokumentation av lÄg nivÄ om Envoys kodbas Àr för nÀrvarande ganska sparsam. För att ÄtgÀrda detta planerar jag att göra en serie bloggartiklar om olika delsystem i Envoy. Eftersom detta Àr den första artikeln, vÀnligen lÄt mig veta vad du tycker och vad som skulle kunna intressera dig i de kommande artiklarna.
En av de vanligaste tekniska frÄgorna jag fÄr om Envoy Àr begÀran om en lÄg nivÄ beskrivning av den anvÀnda trÄdningsmodellen. I det hÀr inlÀgget kommer jag att beskriva hur Envoy mappar anslutningar till trÄdar, samt en beskrivning av systemet för trÄdspecifik lagring (Thread Local Storage) som anvÀnds internt för att göra koden mer parallell och högpresterande.
Ăversikt av trĂ„dar (Threading overview)
![[ĂversĂ€ttning] Envoy threading model (Envoy modell för trĂ„dar)](/wp-content/uploads/2019/04/c5028463db1eb5b07ccb1a3af1f64a03.jpeg)
Envoy anvÀnder tre olika typer av trÄdar:
- HuvudtrĂ„d (Main): Denna trĂ„d hanterar starten och stoppningen av processen, all hantering av XDS (xDiscovery Service) API:erna, inklusive DNS, hĂ€lsokontroller (health checking), övergripande klusterhantering och tjĂ€nstehantering (runtime), Ă„terstĂ€llande av statistik, administration och övergripande hantering av processer â Linux-signaler, varm omstart (hot restart) etc. Allt som hĂ€nder i denna trĂ„d Ă€r asynkront och "icke-blockerande". Generellt koordinerar huvudtrĂ„den alla kritiska funktionaliteter, vars utförande inte krĂ€ver en stor mĂ€ngd CPU. Detta gör att större delen av styrningskoden kan skrivas som om den vore enkeltrĂ„dad.
- ArbetstrÄd (Worker): Som standard skapar Envoy en arbetstrÄd (worker thread) för varje hÄrdvarutrÄd i systemet, detta kan kontrolleras med hjÀlp av alternativet
--concurrency. Varje arbetstrÄd kör en "icke-blockerande" hÀndelseschlepp (event loop) som ansvarar för att övervaka (listening) varje lyssnare (listener). Vid skrivande av denna artikel (29 juli 2017) finns ingen segmentering (sharding) av lyssnaren (listener), vilket inkluderar att ta emot nya kopplingar, skapa en instans av filterstacken för anslutningen och hantera alla in-och-utdataoperationer (IO) under anslutningens livslÀngd. à terigen gör detta att större delen av koden för hantering av anslutningar kan skrivas som om den vore en trÄdad (single-threaded) applikation. - Filskriver (File flusher): Varje fil som Envoy skriver, huvudsakligen Ätkomstloggar (access logs), har för nÀrvarande en oberoende blockerande trÄd. Detta beror pÄ att skrivning till filer, som Àr cachade av filsystemet Àven nÀr det anvÀnds
O_NONBLOCKibland kan blockera (suck). NÀr arbetstrÄdar behöver skriva till en fil, flyttas data faktiskt till en buffer i minnet, dÀr de sÄ smÄningom spolas via strömmen file flush. Detta Àr ett av de omrÄden i koden dÀr tekniskt sett kan alla arbetstrÄdar (worker threads) blockera (block) samma lÄs (lock) nÀr de försöker fylla bufferten i minnet.
Anslutningshantering (Connection handling)
Som kort nÀmnts ovan, övervakar alla arbetstrÄdar alla lyssnare (listeners) utan segmentering. DÀrför anvÀnds kÀrnan för att effektivt skicka de mottagna sockets till arbetstrÄdarna. Moderna kÀrnor Àr i allmÀnhet mycket bra pÄ detta, de anvÀnder funktioner som prioritering av in-och-utdata (IO) för att försöka fylla en ström med arbete innan de börjar anvÀnda andra strömmar som ocksÄ lyssnar pÄ samma socket, samt undvika cyklisk blockering (Spinlock) för att hantera varje begÀran.
SÄ snart en anslutning accepteras pÄ en arbetstrÄd (worker thread), lÀmnar den aldrig denna trÄd (thread). All vidare behandling av anslutningen hanteras helt och hÄllet i arbetstrÄden (worker thread), inklusive eventuellt vidarebefordringsbeteende (forwarding behavior).
Detta har ett antal viktiga konsekvenser:
- Alla anslutningspooler i Envoy tillhör arbetsflödet. DÀrför, Àven om HTTP/2 anslutningspooler endast upprÀttar en enda anslutning till varje överordnad vÀrd Ät gÄngen, om det finns fyra arbetsflöden, kommer det att finnas fyra HTTP/2 anslutningar till den överordnade vÀrden i ett stabilt tillstÄnd.
- Anledningen till att Envoy fungerar pÄ detta sÀtt Àr att genom att hÄlla allt i ett enda arbetsflöde kan nÀstan all kod skrivas utan blockeringar och som om den Àr entrÄdad. Denna design förenklar skrivandet av en stor mÀngd kod och skalas otroligt bra för nÀstan obegrÀnsat antal arbetsflöden.
- Men en av de viktigaste slutsatserna Àr att det i termer av effektivitet Àr mycket viktigt att justera parametern
--concurrency. Att ha fler arbetsflöden Àn nödvÀndigt kommer att leda till minnesförlust, skapande av fler inaktiva anslutningar och lÀgre hastighet att nÄ anslutningspoolen. I Lyft arbetar vÄra envoy sidecar-containrar med mycket lÄg parallellism, sÄ att prestandan ungefÀr motsvarar tjÀnsterna de sitter bredvid. Vi kör Envoy som en edge proxy endast vid maximal parallellism.
Vad icke-blockerande betyder
Termen "icke-blockerande" har anvÀnts flera gÄnger i diskussioner om hur huvud- och arbetsflöden fungerar. All kod Àr skriven med förutsÀttningen att ingenting nÄgonsin blockeras. Dock Àr detta inte helt sant.
Envoy anvÀnder flera lÄngvariga lÄs i processen:
- Som tidigare nÀmnts, nÀr loggar för Ätkomst skrivs, fÄr alla arbetsflöden samma lÄs innan de fyller loggbufferten i minnet. HÄlltiden för lÄsen bör vara mycket lÄg, men det Àr möjligt att denna lÄs kan ifrÄgasÀttas vid hög parallellism och hög genomströmning.
- Envoy anvÀnder ett mycket komplext system för att hantera statistik som Àr lokal för strömmen. Detta kommer att vara Àmnet för ett separat inlÀgg. Jag nÀmner dock kort att som en del av lokal statistikbehandling av strömmen, krÀvs det ibland en lÄs för det centrala "statistiklagret". Detta lÄs ska aldrig behövas.
- Huvudströmmen behöver periodiskt samordnas med alla arbetströmmar. Detta görs genom att "publicera" frÄn huvudströmmen till arbetströmmarna, och ibland frÄn arbetströmmarna tillbaka till huvudströmmen. För att skicka krÀvs det en lÄs sÄ att det publicerade meddelandet kan placeras i kö för senare leverans. Dessa lÄs bör aldrig utsÀttas för allvarlig konkurrens, men de kan tekniskt sett fortfarande blockeras.
- NÀr Envoy skriver loggar till systemströmmens felström (standardfel), fÄr den en lÄs pÄ hela processen. Generellt sett betraktas lokal loggning i Envoy som dÄlig för prestandan, sÄ det lÀggs inte mycket vikt vid att förbÀttra den.
- Det finns nÄgra andra tillfÀlliga lÄs, men ingen av dem Àr kritisk för prestanda och bör aldrig ifrÄgasÀttas.
TrÄd-lokalt lagring (Thread local storage)
PÄ grund av sÀttet som Envoy separerar huvudströmmens ansvar frÄn arbetströmmens ansvar, finns det ett krav pÄ att komplex bearbetning kan utföras i huvudströmmen och sedan ges till varje arbetström med hög grad av parallellism. I detta avsnitt beskrivs systemet för 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 klustret.
![[ĂversĂ€ttning] Envoy threading model (Envoy modell för trĂ„dar)](/wp-content/uploads/2019/04/a47af1e8eb1f55a4d7609b3ff3ee9ce1.jpeg)
Som redan nÀmnts, hanterar huvudströmmen nÀstan alla funktioner för hantering och funktionalitet för kontrollplanet i Envoy-processen. Kontrollplanet Àr hÀr nÄgot överbelastat, men om man ser pÄ det i ramen för selve Envoy-processen och jÀmför med vidarebefordran som utförs av arbetströmmarna, verkar det rimligt. Som en allmÀn regel utför huvudströmsprocessen en viss arbetsuppgift och sedan mÄste den uppdatera varje arbetström i enlighet med resultatet av detta arbete. I det hÀr fallet behöver inte arbetstrÄden sÀtta en lÄs vid varje Ätkomst.
TLS-systemet (TrÄd-lokal lagring) Envoy fungerar pÄ följande sÀtt:
- Koden som körs i huvudtrĂ„den kan allokera en TLS-plats för hela processen. Ăven om detta Ă€r abstrakt, Ă€r det i praktiken ett index i en vektor som tillhandahĂ„ller O(1) Ă„tkomst.
- HuvudtrÄden kan sÀtta godtyckliga data i sin slot. NÀr detta görs publiceras data i varje arbetstrÄd som en vanlig hÀndelse i hÀndelsecykeln.
- ArbetstrÄdarna kan lÀsa frÄn sin TLS-slot och hÀmta alla lokala trÄddatan som finns tillgÀnglig dÀr.
Ăven om detta Ă€r en mycket enkel och otroligt kraftfull paradigm som liknar RCU-konceptet (Read-Copy-Update). I grunden ser arbetstrĂ„darna aldrig nĂ„gra förĂ€ndringar i data i TLS-slotarna under arbetets gĂ„ng. Ăndringar sker endast under vila mellan arbetshĂ€ndelser.
Envoy anvÀnder detta pÄ tvÄ olika sÀtt:
- Genom att spara olika data pÄ varje arbetstrÄd görs Ätkomsten till dessa data utan nÄgon lÄsning.
- Genom att spara en gemensam pekare pÄ globala data i lÀslÀge pÄ varje arbetstrÄd. DÀrmed har varje arbetstrÄd en referensrÀknare pÄ data som inte kan minskas under arbetets gÄng. Endast nÀr alla arbetare har vilat och laddat nya gemensamma data, kommer de gamla data att förstöras. Detta Àr identiskt med RCU.
KlusteruppdateringstrÄdning (Cluster update threading)
I det hÀr avsnittet kommer jag att beskriva hur TLS (Thread local storage) anvÀnds för att hantera klustret. Hantering av klustret inkluderar behandling av API xDS och / eller DNS samt hÀlsokontroller (health checking).
![[ĂversĂ€ttning] Envoy threading model (Envoy modell för trĂ„dar)](/wp-content/uploads/2019/04/238f5718729e1f12f8f0d6507f16abe8.jpeg)
KlustertrÄdhantering omfattar följande komponenter och steg:
- Klusterhanteraren Àr en komponent inom Envoy som hanterar alla kÀnda upstream i klustret, API-grÀnssnittet CDS (Cluster Discovery Service), API-grÀnssnitten SDS (Secret Discovery Service) och EDS (Endpoint Discovery Service), DNS och aktiva externa hÀlsokontroller (health checking). Den ansvarar för att skapa en "eventuellt konsekvent" (eventually consistent) bild av varje upstream i klustret, vilket inkluderar upptÀckta vÀrdar samt hÀlsostatus (health status).
- HÀlsokontrollverktyget (health checker) utför aktiv hÀlsokontroll och rapporterar förÀndringar i tillstÄndet till klusterchefen.
- CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS anvÀnds för att avgöra klustertillhörighet. TillstÄndsÀndringar returneras till klusterchefen.
- Varje arbetstrÄd utför stÀndigt en cykel av hÀndelsebehandling.
- NÀr klusterchefen faststÀller att tillstÄndet för klustret har förÀndrats skapas en ny tillstÄndsbild som Àr skrivskyddad och skickas till varje arbetstrÄd.
- Under nÀsta viloperiod kommer arbetstrÄden att uppdatera bilden i den tilldelade TLS-platsen.
- Under en in- och utflödeshÀndelse som ska avgöra vÀrden för belastningsbalansering kommer belastningsbalanseraren att begÀra TLS-platsen (trÄdspecifik lagring) för att hÀmta vÀrdinformation. Inga lÄs krÀvs för detta. Observera ocksÄ att TLS Àven kan initiera hÀndelser vid uppdateringar, sÄ att belastningsbalanseringens subsystem och andra komponenter kan omberÀkna cacheminnen, datakonstruktioner etc. Detta gÄr utanför ramen för detta inlÀgg, men anvÀnds pÄ olika stÀllen i koden.
Genom att anvÀnda den ovan beskrivna proceduren kan Envoy hantera varje begÀran utan nÄgra lÄs (förutom de som tidigare beskrivits). Utöver komplexiteten i sjÀlva TLS-koden behöver den största delen av koden inte förstÄ hur flertrÄdighet fungerar, och den kan skrivas i en enkeltrÄdig stil. Detta underlÀttar skrivandet av större delen av koden utöver utmÀrkt prestanda.
Andra subsystem som anvÀnder TLS
TLS (trÄdspecifik lagring) och RCU (Read Copy Update) anvÀnds allmÀnt i Envoy.
AnvÀndningsexempel:
- Mekanism för att Àndra funktionalitet under körning: Den aktuella listan över aktiverade funktioner berÀknas i huvudtrÄden. DÀrefter tilldelas varje arbetstrÄd en skrivskyddad bild med hjÀlp av RCU-semantik.
- ErsÀttning av routingtavlor: för rutnÀt som tillhandahÄlls av RDS (Route Discovery Service) skapas rutnÀt i huvudtrÄden. En skrivskyddad ögonblicksbild kommer sedan att tillhandahÄllas varje arbetstrÄd med hjÀlp av RCU-semantik (Read Copy Update). Detta gör Àndringar i rutnÀt atomÀrt effektiva.
- Caching av HTTP-huvuden: Det visar sig att berÀkningen av HTTP-huvudet för varje förfrÄgan (vid körning av ~25K+ RPS per kÀrna) Àr ganska kostsam. Envoy berÀknar centralt huvudet ungefÀr varannan sekund och förser varje arbetare via TLS och RCU.
Det finns andra fall, men de tidigare exemplen bör ge en bra förstÄelse för vad TLS anvÀnds till.
KÀnda prestandafÀllor (Known performance pitfalls)
Ăven om Envoy överlag fungerar ganska bra finns det nĂ„gra kĂ€nda omrĂ„den som krĂ€ver uppmĂ€rksamhet nĂ€r det anvĂ€nds med mycket hög parallellism och kapacitet:
- Som redan beskrivits i denna artikel, fÄr för nÀrvarande alla arbetstrÄdar en lÄsning vid skrivning till minnesbufferten för loggÄtkomst. Vid hög parallellism och hög kapacitet kommer det att krÀvas en samling av loggar för varje arbetstrÄd till priset av oordnad leverans vid skrivning till den slutliga filen. Som ett alternativ kan varje arbetstrÄd upprÀtta en separat logg för Ätkomst.
- Ăven om statistiken Ă€r starkt optimerad, kommer det troligen att ske atomĂ€r konkurrens om individuell statistik vid mycket hög parallellism och kapacitet. Lösningen pĂ„ detta problem Ă€r att ha rĂ€knare för en arbetstrĂ„d med periodiska Ă„terstĂ€llningar av centrala rĂ€knare. Detta kommer att diskuteras i ett kommande inlĂ€gg.
- Den befintliga arkitekturen kommer inte att fungera bra om Envoy distribueras i ett scenario dÀr det finns mycket fÄ anslutningar som krÀver betydande resurser för bearbetning. Det finns ingen garanti för att anslutningarna kommer att fördelas jÀmnt mellan arbetstrÄdarna. Detta kan lösas genom att implementera lastbalansering av arbetstrÄdar, dÀr en funktion för att dela anslutningar mellan arbetstrÄdar implementeras.
Slutsats (Conclusion)
Envoys trÄdbaserade modell Àr utformad för att underlÀtta programmering och massiv parallellism genom potentiellt kostsamma anvÀndningar av minne och anslutningar om de inte konfigureras korrekt. Denna modell gör att den fungerar mycket bra vid ett mycket stort antal trÄdar och hög genomströmning.
Som jag kort nÀmnde pÄ Twitter kan designen ocksÄ fungera ovanpÄ en fullfjÀdrad nÀtverksstack i anvÀndarlÀge, som DPDK (Data Plane Development Kit), vilket kan leda till att vanliga servrar hanterar miljoner förfrÄgningar per sekund med fullstÀndig L7-behandling. Det ska bli mycket intressant att se vad som byggs under de kommande Ären.
En sista snabb kommentar: Jag har fÄtt frÄgan flera gÄnger om varför vi valde C++ för Envoy. Anledningen Àr fortfarande att det Àr det enda allmÀnt förekommande sprÄk pÄ industristandard, som kan bygga den arkitektur som beskrivs i det hÀr inlÀgget. C++ Àr definitivt inte lÀmpligt för alla eller ens mÄnga projekt, men för vissa anvÀndningsfall Àr det fortfarande det enda verktyget som fÄr jobbet gjort.
LĂ€nkar till kod
LÀnkar till filerna med grÀnssnitt och implementationshuvuden som diskuteras i detta inlÀgg:
KĂ€lla: habr.com
