Oversettelse av artikkelen:
Denne artikkelen virker ganske interessant for meg, og siden Envoy ofte brukes som en del av «istio» eller bare som «ingress controller» for Kubernetes, har de fleste ikke så mye direkte interaksjon med den som med typiske installasjoner av Nginx eller Haproxy. Men hvis noe går galt, ville det vært bra å forstå hvordan det fungerer på innsiden. Jeg har prøvd å oversette så mye tekst som mulig til norsk, inkludert spesialord, og for de som har det vanskelig med det, har jeg beholdt originalene i parentes. Velkommen under.
Den tekniske dokumentasjonen på lavt nivå om Envoy-koden er for øyeblikket ganske sparsom. For å forbedre dette planlegger jeg å lage en serie med blogginnlegg om ulike undersystemer av Envoy. Siden dette er den første artikkelen, vennligst gi meg beskjed om hva du synes og hva som kunne vært interessant i de følgende artiklene.
Et av de mest vanlige tekniske spørsmålene jeg får om Envoy, er forespørselen om en lavnivå beskrivelse av den brukte trådmodellen (threading model). I dette innlegget vil jeg beskrive hvordan Envoy tilordner tilkoblinger til tråder, samt en beskrivelse av systemet for lokal trådlager (Thread Local Storage), som brukes internt for å gjøre koden mer parallell og høytytende.
Oversikt over tråder (Threading overview)
![[Oversettelse] Envoy trådmodell (Envoy threading model)](/wp-content/uploads/2019/04/c5028463db1eb5b07ccb1a3af1f64a03.jpeg)
Envoy bruker tre ulike typer tråder:
- Hovedtråd (Main): Denne tråden håndterer oppstart og nedstengning av prosessen, all behandling av XDS (xDiscovery Service) API, inkludert DNS, helsesjekk (health checking), generell administrasjon av klynger og tjenesteprosess (runtime), tilbakestilling av statistikk, administrasjon og generell prosesshåndtering — Linux-signaler, varm omstart (hot restart) osv. Alt som skjer i denne tråden er asynkront og «ikke-blokkerende». Generelt koordinerer hovedtråden alle kritiske funksjonaliteter som ikke krever mye CPU. Dette muliggjør at mesteparten av administrasjonskoden kan skrives som om den var entrådet.
- Arbeidstråd (Worker): Som standard oppretter Envoy en arbeidstråd (worker thread) for hver fysisk tråd i systemet; dette kan kontrolleres med alternativet
--concurrencyHver arbeidsstrøm starter en «ikke-blokkerende» hendelsessløyfe (event loop) som er ansvarlig for å lytte til hver lytter (listener). Per artikkelens skrivepunkt (29. juli 2017) er det ingen segmentering (sharding) av lytteren, aksept av nye forbindelser, oppretting av instanser av filterstakken for tilkoblingen, og behandling av alle inn- og utdataoperasjoner (IO) i løpet av tilkoblingens levetid. Igjen, dette tillater at mesteparten av koden for håndtering av tilkoblinger kan skrives som om den var en-trådet. - Filskriver (File flusher): Hver fil som Envoy skriver, hovedsakelig tilgangslogger (access logs), har nå en uavhengig blokkerende tråd. Dette er fordi skriving til filer som er bufret av filsystemet, selv når
O_NONBLOCKbrukes, noen ganger kan blokkere (sukk). Når arbeidsstrømmer må skrive til en fil, flyttes dataene faktisk til en buffer i minnet, hvor de til slutt dumpes via strømmen file flush. Dette er ett av områdene i koden hvor teknisk sett kan alle arbeidsstrømmer (worker threads) blokkere (block) den samme låsen (lock) mens de prøver å fylle minnebufferen.
Håndtering av tilkoblinger (Connection handling)
Som kort diskutert ovenfor, lytter alle arbeidsstrømmer til alle lyttere (listeners) uten noen segmentering. Dermed brukes kjernen til å effektivt sende de aksepterte soklene til arbeidsstrømmer. Moderne kjerner er generelt veldig gode til dette; de bruker funksjoner som prioritering av inn- og utdata (IO) for å forsøke å fylle opp en strøm med arbeid før de begynner å bruke andre strømmer som også lytter til den samme sokkelen, og unngår også å bruke spinlås (Spinlock) for å håndtere hver forespørsel.
Så snart tilkoblingen er akseptert på en arbeidsstrøm (worker thread), forlater den aldri denne tråden (thread). All videre behandling av tilkoblingen håndteres fullt ut i arbeidsstrømmen (worker thread), inkludert enhver videresending (forwarding behavior).
Dette har flere viktige konsekvenser:
- Alle tilkoblingspooler i Envoy tilhører arbeidsstrømmen. Selvom HTTP/2-tilkoblingspooler kun oppretter én tilkobling til hver overordnede vert om gangen, vil det være fire HTTP/2-tilkoblinger til den overordnede verten i steady state hvis det er fire arbeidsstrømmer.
- Grunnen til at Envoy fungerer på denne måten, er at ved å holde alt i én arbeidsstrøm, kan nesten all koden skrives uten blokkering, som om den var enkelttrådet. Denne designen forenkler skrivingen av store mengder kode og skalerer utrolig bra for nesten et ubegrenset antall arbeidsstrømmer.
- Imidlertid er en av hovedkonklusjonene at når det gjelder effektivitet, er minne- og tilkoblingspoolen faktisk veldig viktig å konfigurere innstillingen
--concurrency. Å ha flere arbeidsstrømmer enn nødvendig vil føre til minneavfall, opprettelse av flere inaktive tilkoblinger og redusere hastigheten på tilkoblingspoolen. I Lyft fungerer våre envoy sidecar-containere med svært lav parallellisme, slik at ytelsen omtrent samsvarer med tjenestene de sitter ved siden av. Vi kjører Envoy som en grenseproxy (edge) kun ved maksimal parallellisme (concurrency).
Hva ikke-blokkerende betyr
Begrepet «ikke-blokkerende» har blitt brukt flere ganger i diskusjonen om hvordan hoved- og arbeidsstrømmene fungerer. All koden er skrevet med antakelsen om at ingenting noensinne blokkeres. Men dette er ikke helt korrekt.
Envoy bruker flere langvarige prosessblokkeringer:
- Som nevnt tidligere, når tilgangslogger skrives, får alle arbeidsstrømmer den samme blokkeringen før de fyller loggbufferen i minnet. Tiden for å holde blokkeringen bør være veldig lav, men det er mulig at denne blokkeringen vil bli omstridt ved høy parallellisme og høy gjennomstrømning.
- Envoy bruker et svært komplekst system for å behandle statistikk, som er lokalt for strømmene. Dette vil være temaet for et eget innlegg. Likevel vil jeg kort nevne at som en del av lokal behandling av strømstatistikk, er det noen ganger nødvendig å låse det sentrale "statistikkdepotet". Denne låsen bør aldri være nødvendig.
- Hovedstrømmen trenger regelmessig å koordinere med alle arbeidsstrømmer. Dette gjøres ved å "publisere" fra hovedstrømmen til arbeidsstrømmer, og noen ganger fra arbeidsstrømmer tilbake til hovedstrømmen. Det kreves en lås for å kunne sette det publiserte meldingen i kø for senere levering. Disse låsene bør aldri være gjenstand for alvorlig konkurranse, men de kan likevel teknisk bli blokkert.
- Når Envoy skriver logg til systemfeilstrømmen (standard error), får den en lås på hele prosessen. Generelt anses lokal loggføring av Envoy som forferdelig med hensyn til ytelse, så det legges ikke mye vekt på forbedringer.
- Det er flere andre sporadiske låser, men ingen av dem er kritiske for ytelsen og bør aldri være gjenstand for konkurranse.
Lokal lagring av tråder (Thread local storage)
På grunn av måten Envoy separerer ansvaret til hovedstrømmen fra arbeidsstrømmen, er det et krav om at kompleks behandling kan gjøres i hovedstrømmen og deretter gis til hver arbeidsstrøm med høy grad av parallellisme. I dette avsnittet beskrives systemet for Envoy Thread Local Storage (TLS) på et høyt nivå. I neste avsnitt vil jeg beskrive hvordan det brukes til å håndtere klyngen.
![[Oversettelse] Envoy trådmodell (Envoy threading model)](/wp-content/uploads/2019/04/a47af1e8eb1f55a4d7609b3ff3ee9ce1.jpeg)
Som allerede beskrevet, behandler hovedstrømmen stort sett alle administrasjonsfunksjoner og funksjonalitet for kontrollplanet (control plane) i Envoy-prosessen. Kontrollplanet er her noe overbelastet, men hvis vi ser på det innenfor rammen av selve Envoy-prosessen og sammenligner det med videresendingen som arbeidsstrømmene utfører, fremstår dette som fornuftig. Som en generell regel utfører hovedstrømmen noe arbeid, og deretter er det nødvendig å oppdatere hver arbeidsstrøm i samsvar med resultatet av dette arbeidet. Samtidig trenger ikke arbeidsstrømmen å sette en lås ved hvert tilgang..
TLS (Thread local storage) Envoy-systemet fungerer som følger:
- Koden som kjører i hovedstrømmen kan tildele en TLS-slot for hele prosessen. Selv om dette er abstrahert, er det i praksis en indeks i en vektor som gir O(1) tilgang.
- Hovedstrømmen kan sette vilkårlige data i sin slot. Når dette er gjort, blir dataene publisert i hver arbeidsstrøm som en vanlig hendelse i hendelsessyklusen.
- Arbeidsstrømmer kan lese fra sin TLS-slot og hente ut eventuelle lokale tråddatasett som er tilgjengelig der.
Selv om dette er en veldig enkel og utrolig kraftig tilnærming, ligner den sterkt på konseptet med RCU (Read-Copy-Update) låsing. I hovedsak ser ikke arbeidsstrømmer noen endringer i dataene i TLS-slotene mens de utfører arbeidet. Endringer skjer kun i hvileperiodene mellom arbeids-hendelser.
Envoy bruker dette på to forskjellige måter:
- Ved å lagre forskjellige data i hver arbeidsstrøm, kan tilgang til disse dataene oppnås uten noen lås.
- Ved å lagre en felles peker til globale data i 'kun lesemodus' i hver arbeidsstrøm. Slik har hver arbeidsstrøm en referanseteller på dataene som ikke kan reduseres mens arbeidet pågår. Bare når alle arbeiderne har roet seg og lastet nye felles data, blir de gamle dataene ødelagt. Dette er identisk med RCU.
Klyngeoppdateringsstrøm (Cluster update threading)
I dette avsnittet skal jeg beskrive hvordan TLS (Thread local storage) brukes til å administrere klyngen. Administrasjon av klyngen inkluderer behandling av API xDS og / eller DNS, samt helsesjekk (health checking).
![[Oversettelse] Envoy trådmodell (Envoy threading model)](/wp-content/uploads/2019/04/238f5718729e1f12f8f0d6507f16abe8.jpeg)
Administrasjonen av klynge-strømmer inkluderer følgende komponenter og trinn:
- Klyngeleder er en komponent inne i Envoy som styrer alle kjente upstreams av klyngen, API-grensesnittet CDS (Cluster Discovery Service), API-grensesnittene SDS (Secret Discovery Service) og EDS (Endpoint Discovery Service), DNS og aktive eksterne helsesjekker (health checking). Den er ansvarlig for å skape en 'i siste instans konsistent' (eventually consistent) fremstilling av hver upstream av klyngen, som inkluderer oppdagede verter samt helsestatus.
- Helsesjekkverktøyet utfører aktiv helsesjekk og rapporterer om endringer i driftsstatus til klyngeleder.
- CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS kjøres for å bestemme klyngetilhørighet. Endringer i tilstand returneres til klyngelederen.
- Hver arbeidstråd utfører kontinuerlig en hendelsessyklus.
- Når klyngelederen bestemmer seg for at tilstanden til klyngen har endret seg, oppretter den et nytt lesbart snapshot av klyngetilstanden og sender det til hver arbeidstråd.
- I løpet av neste hvileperiode vil arbeidstråden oppdatere snapshotet i den tildelte TLS-plassen.
- Under I/O-hendelser som skal bestemme verten for lastbalansering, vil lastbalansereren be om TLS-plassen (Thread Local Storage) for å få vertsinformasjonen. Dette krever ingen lås. Vær også oppmerksom på at TLS også kan initiere hendelser ved oppdatering, slik at lastbalanseringssystemer og andre komponenter kan re-kalkulere cacher, datastrukturer osv. Dette går utover denne posten, men brukes på forskjellige steder i koden.
Ved å bruke den ovenfor beskrevne prosedyren kan Envoy håndtere hver forespørsel uten noen låser (unntatt de som er beskrevet tidligere). Bortsett fra kompleksiteten i selve TLS-koden, trenger det meste av koden ikke å forstå hvordan flertrådet fungerer, og kan skrives i en en-trådsmodus. Dette letter skriving av mesteparten av koden samt gir fremragende ytelse.
Andre underkomponenter som benytter TLS
TLS (Thread Local Storage) og RCU (Read Copy Update) brukes mye i Envoy.
Eksempler på bruk:
- Mekanisme for endring av funksjonalitet under utførelse: Den nåværende listen over aktiv funksjonalitet beregnes i hovedtråden. Deretter får hver arbeidstråd et snapshot kun for lesing ved hjelp av RCU-semantikk.
- Erstatning av rutetabeller: for route tables provided by RDS (Route Discovery Service), the route tables are created in the main thread. A read-only snapshot will later be provided to each worker thread using RCU (Read Copy Update) semantics. This makes modifications to the route tables atomically efficient.
- HTTP header caching: It turns out that calculating the HTTP header for each request (when performing ~25K+ RPS per core) is quite expensive. Envoy centrally calculates the header approximately every half second and provides it to each worker via TLS and RCU.
There are other cases, but the previous examples should provide a good understanding of when TLS is used.
Known performance pitfalls
Although Envoy generally works quite well, there are a few known areas that require attention when it is used with very high parallelism and throughput:
- As described in this article, currently all worker threads are blocked when writing to the memory buffer of the access log. At high parallelism and throughput, it will be necessary to batch access logs for each worker thread at the cost of unordered delivery when writing to the final file. An alternative is to create a separate access log for each worker thread.
- Although the statistics are very well optimized, at very high parallelism and throughput, there will likely be atomic contention on individual statistics. The solution to this problem is to use counters for a single worker thread with periodic resets of central counters. This will be discussed in a subsequent post.
- The existing architecture will not perform well if Envoy is deployed in a scenario with very few connections that require significant resources to handle. There is no guarantee that connections will be evenly distributed among worker threads. This can be resolved by implementing worker connection balancing, which will enable connection sharing between worker threads.
Conclusion
Envoy-trådmodellen er utviklet for å sikre enkel programmering og massiv parallellisme gjennom potensielt sløsende bruk av minne og forbindelser hvis de ikke er riktig konfigurert. Denne modellen gjør at den fungerer veldig bra med et svært høyt antall tråder og båndbredde.
Som jeg kort nevnte på Twitter, kan designet også fungere over en fullverdig nettverksstakk i bruker-modus, som DPDK (Data Plane Development Kit), noe som kan føre til at vanlige servere håndterer millioner av forespørsel per sekund med full L7-behandling. Det vil bli veldig interessant å se hva som bygges i løpet av de neste årene.
En siste rask kommentar: Jeg har blitt spurt mange ganger hvorfor vi valgte C++ for Envoy. Grunnen er fortsatt at det fortsatt er det eneste utbredte industrispråket som kan bygge arkitekturen som er beskrevet i dette innlegget. C++ passer definitivt ikke for alle eller til og med mange prosjekter, men for bestemte bruksområder er det fortsatt det eneste verktøyet som får jobben gjort.
Lenker til kode
Lenker til filene med grensesnitt og implementering av overskrifter diskutert i dette innlegget:
Kilde: habr.com
