[Перевод] Modelul de fire Envoy (Envoy threading model)

Traducerea articolului: Modelul de threading Envoy — https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Acest articol mi s-a părut destul de interesant, iar deoarece Envoy este folosit cel mai adesea ca parte a «istio» sau pur și simplu ca «ingress controller» pentru Kubernetes, majoritatea oamenilor nu au o interacțiune directă cu acesta, așa cum au cu instalările tipice de Nginx sau Haproxy. Cu toate acestea, atunci când ceva se strică, ar fi bine să înțelegem cum funcționează din interior. Am încercat să traduc cât mai mult text în română, inclusiv termeni speciali; pentru cei cărora le este greu să vadă așa ceva, am lăsat originalele în paranteze. Bun venit la detalii.

Documentația tehnică la un nivel de bază despre baza de cod Envoy este în prezent destul de sărăcăcioasă. Pentru a remedia acest lucru, plănuiesc să fac o serie de articole pe blog despre diferitele subsisteme Envoy. Deoarece acesta este primul articol, vă rog să-mi spuneți ce părere aveți și ce v-ar putea interesa în următoarele articole.

Una dintre cele mai comune întrebări tehnice pe care le primesc despre Envoy este solicitarea pentru o descriere detaliată a modelului de threading utilizat. În acest articol, voi descrie cum Envoy asociază conexiunile cu thread-urile, precum și descrierea sistemului de stocare locală a thread-urilor (Thread Local Storage) care este folosit în interior pentru a face codul mai paralel și mai performant.

Prezentarea thread-urilor (Threading overview)

[Перевод] Modelul de fire Envoy (Envoy threading model)

Envoy folosește trei tipuri diferite de thread-uri:

  • Principal (Main): Acest thread gestionează inițierea și încheierea procesului, toată procesarea API-ului XDS (xDiscovery Service), inclusiv DNS, verificarea sănătății (health checking), gestionarea generală a cluster-ului și a procesului de funcționare a serviciului (runtime), resetarea statisticilor, administrarea și gestionarea generală a proceselor — semnalele Linux, repornirea la cald (hot restart) etc. Tot ceea ce se întâmplă în acest thread este asincron și «non-blocant». În general, thread-ul principal coordonează toate procesele critice ale funcționalității, pentru îndeplinirea cărora nu este nevoie de o cantitate mare de CPU. Acest lucru permite ca majoritatea codului de gestionare să fie scris ca și cum ar fi uniplex.
  • De lucru (Worker): Implicit, Envoy creează un thread de lucru (worker thread) pentru fiecare thread hardware din sistem, ceea ce poate fi controlat prin opțiunea --concurrencyFiecare thread de lucru pornește un ciclu de evenimente „non-blocant” (event loop) care este responsabil pentru ascultarea fiecărui ascultător (listener); la momentul redactării acestui articol (29 iulie 2017), nu există segmentare (sharding) pentru ascultător (listener), acceptarea noilor conexiuni, crearea unei instanțe a stivei de filtre pentru conexiune și prelucrarea tuturor operațiunilor de intrare-ieșire (IO) pe întreaga durată a conexiunii. Din nou, aceasta permite ca majoritatea codului pentru prelucrarea conexiunilor să fie scris ca și cum ar fi un singur fir de execuție.
  • Flusherele de fișiere (File flusher): Fiecare fișier pe care îl scrie Envoy, în principal jurnale de acces (access logs), are în prezent un fir de execuție blocant independent. Acest lucru se datorează faptului că scrierea în fișierele cache-uite de sistemul de fișiere, chiar și atunci când se utilizează O_NONBLOCK poate bloca (sigh) uneori. Atunci când firele de lucru trebuie să scrie în fișier, datele sunt de fapt mutate într-un tampon în memorie, unde sunt apoi descărcate prin firul file flush. Aceasta este una dintre zonele de cod în care tehnic toate firele de lucru (worker threads) pot bloca (block) aceeași blocare (lock) în încercarea de a umple tamponul de memorie.

Prelucrarea conexiunilor (Connection handling)

După cum s-a discutat pe scurt mai sus, toate firele de lucru ascultă toți ascultătorii (listeners) fără nicio segmentare. Astfel, nucleul este utilizat pentru a direcționa eficient socket-urile primite către firele de lucru. Nuclee moderne sunt în general foarte bune în acest sens, folosesc caracteristici precum creșterea priorității de intrare-ieșire (IO) pentru a încerca să umple un fir de lucru cu sarcini înainte de a începe să utilizeze alte fire de lucru care ascultă același socket, precum și să evite utilizarea blocării ciclice (Spinlock) pentru a gestiona fiecare cerere.
Odată ce conexiunea este acceptată pe un fir de lucru (worker thread), aceasta nu părăsește niciodată acel fir (thread). Tot restul prelucrării conexiunii este complet gestionat în firul de lucru (worker thread), inclusiv orice comportament de redirecționare (forwarding behavior).

Acest lucru are câteva consecințe importante:

  • Toate pool-urile de conexiuni în Envoy sunt asociate cu un fir de execuție. Prin urmare, deși pool-urile de conexiuni HTTP/2 realizează doar o singură conexiune cu fiecare gazdă superioară deodată, dacă există patru fire de execuție, va exista câte o conexiune HTTP/2 pentru fiecare gazdă superioară în stare stabilă.
  • Motivul pentru care Envoy funcționează în acest mod este că, menținând totul într-un singur fir de execuție, aproape tot codul poate fi scris fără blocări și ca și cum ar fi un singur fir. Acest design simplifică scrierea unui volum mare de cod și se scalează incredibil de bine pentru un număr aproape nelimitat de fire de execuție.
  • Cu toate acestea, una dintre concluziile principale este că, din perspectiva eficienței pool-ului de memorie și a conexiunilor, este de fapt foarte important să se configureze parametrul --concurrency. A avea mai multe fire de execuție decât este necesar va duce la pierderi de memorie, crearea de mai multe conexiuni inactive și la o reducere a vitezei de accesare a pool-ului de conexiuni. La Lyft, containerele noastre envoy sidecar funcționează cu un grad de paralelism foarte scăzut, astfel încât performanța este aproximativ similară cu serviciile de care se află aproape. Rulăm Envoy ca un proxy la margine doar când se atinge paralelismul maxim.

Ce înseamnă modul non-blocant (What non-blocking means)

Termenul "non-blocant" a fost utilizat de mai multe ori în discuțiile despre modul în care funcționează firul principal și firele de execuție. Întregul cod este scris cu condiția că nimic nu se blochează vreodată. Totuși, acest lucru nu este chiar corect (ce nu este chiar corect?).

Envoy folosește mai multe blocări de proces de lungă durată:

  • După cum s-a menționat, atunci când se înregistrează jurnale de acces, toate firele de execuție primesc aceeași blocare înainte de a umple buffer-ul jurnalului în memorie. Timpul de retenție al blocării ar trebui să fie foarte scăzut, dar este posibil ca această blocare să fie contestată la un grad ridicat de paralelism și o lățime de bandă mare.
  • Envoy utilizează un sistem foarte complex pentru prelucrarea statisticilor, care este local pentru flux. Aceasta va fi tema unei postări separate. Cu toate acestea, voi menționa pe scurt că, ca parte a prelucrării locale a statisticilor fluxului, uneori este necesară obținerea unei blocări pentru «depozitul central al statisticilor». Această blocare nu ar trebui să fie niciodată necesară.
  • Fluxul principal necesită perioada coordonare cu toate fluxurile de muncă. Acest lucru se face prin «publicarea» din fluxul principal în fluxurile de muncă și, uneori, din fluxurile de muncă înapoi în fluxul principal. Pentru a trimite, este necesară o blocare, astfel încât mesajul publicat să poată fi pus în coadă pentru livrarea ulterioară. Aceste blocări nu ar trebui să sufere niciodată competiție serioasă, dar ele pot fi totuși tehnic blocate.
  • Când Envoy scrie jurnalul în fluxul de erori standard, el obține o blocare a întregului proces. În general, înregistrarea locală a Envoy este considerată groaznică din punct de vedere al performanței, așa că nu se acordă multă atenție îmbunătățirii acesteia.
  • Există câteva alte blocări ocazionale, dar niciuna dintre ele nu este critică pentru performanță și nu ar trebui niciodată contestată.

Depozit local de fir (Thread local storage)

Din cauza modului în care Envoy separă responsabilitățile fluxului principal de responsabilitățile fluxului de muncă, există cerința ca prelucrarea complexă să poată fi realizată în fluxul principal și apoi furnizată fiecărui flux de muncă cu un grad ridicat de paralelism. În această secțiune este descrisă sistemul Envoy Thread Local Storage (TLS) la un nivel înalt. În secțiunea următoare, voi descrie cum este utilizat pentru gestionarea clusterului.
[Перевод] Modelul de fire Envoy (Envoy threading model)

Așa cum a fost descris anterior, fluxul principal gestionează aproape toate funcțiile de administrare și funcționalitatea planului de control în procesul Envoy. Planul de control este puțin suprasolicitat aici, dar dacă îl considerăm în cadrul procesului Envoy și comparăm cu redirecționarea efectuată de fluxurile de muncă, aceasta pare rațională. Ca regulă generală, procesul fluxului principal efectuează o anumită muncă și apoi este necesar să actualizeze fiecare flux de muncă conform rezultatului acestei lucrări. fluxul de lucru nu trebuie să blocheze la fiecare acces.

Sistemul TLS (Thread local storage) Envoy funcționează astfel:

  • Codul care rulează în fluxul principal poate aloca un slot TLS pentru întreaga proces. Deși acest lucru este abstractizat, în practică, este un index într-un vector care asigură accesul O(1).
  • Fluxul principal poate stoca date arbitrare în slotul său. Odată ce acest lucru este făcut, datele sunt publicate în fiecare flux de lucru ca un eveniment obișnuit în ciclul de evenimente.
  • Fluxurile de lucru pot citi din slotul lor TLS și extrage orice date locale de flux disponibile acolo.

Deși este o paradigmă foarte simplă și incredibil de puternică, care este foarte asemănătoare cu conceptul de blocare RCU (Read-Copy-Update). Practic, fluxurile de lucru nu văd nicio modificare a datelor din sloturile TLS în timpul execuției muncii. Modificarea are loc doar în perioada de inactivitate între evenimentele de lucru.

Envoy folosește acest lucru în două moduri diferite:

  • Păstrând date diferite pentru fiecare flux de lucru, accesul la aceste date se face fără nicio blocare.
  • Păstrând un pointer comun la date globale în modul „doar pentru citire” pentru fiecare flux de lucru. Astfel, fiecare flux de lucru are un contor de referințe la date care nu poate fi redus în timpul execuției muncii. Numai atunci când toți lucrătorii sunt inactivi și încarcă noi date comune, vechile date vor fi distruse. Acest lucru este identic cu RCU.

Fluxul de actualizare a clusterului (Cluster update threading)

În această secțiune, voi descrie cum este utilizat TLS (Thread local storage) pentru gestionarea clusterului. Gestionarea clusterului include procesarea API xDS și / sau DNS, precum și verificarea stării de funcționare (health checking).
[Перевод] Modelul de fire Envoy (Envoy threading model)

Gestionarea fluxurilor de cluster include următoarele componente și etape:

  1. Managerul de cluster este un component în interiorul Envoy care gestionează toate upstream-urile cunoscute ale clusterului, API-ul CDS (Cluster Discovery Service), API-urile SDS (Secret Discovery Service) și EDS (Endpoint Discovery Service), DNS și verificările externe de sănătate active (health checking). Acesta este responsabil pentru crearea unei reprezentări „eventually consistent” (în cele din urmă coerentă) pentru fiecare upstream al clusterului, care include gazdele descoperite, precum și starea de sănătate.
  2. Instrumentul de verificare a sănătății (health checker) efectuează o verificare activă a stării și raportează orice modificări ale stării de funcționare managerului de cluster.
  3. CDS (Cluster Discovery Service) / SDS (Secret Discovery Service) / EDS (Endpoint Discovery Service) / DNS sunt utilizate pentru a determina apartenența la cluster. Schimbarea stării este returnată managerului de cluster.
  4. Fiecare fir de execuție lucrează constant într-un ciclu de procesare a evenimentelor.
  5. Când managerul de cluster determină că starea clusterului s-a schimbat, acesta creează o nouă imagine a stării clusterului, disponibilă doar pentru citire, și o trimite fiecărui fir de execuție.
  6. În următoarea perioadă de repaus, firul de execuție va actualiza imaginea în slotul dedicat TLS.
  7. În timpul unui eveniment de intrare/ieșire, care trebuie să determine gazda pentru echilibrarea încărcării, echilibratorul de sarcină va solicita slotul TLS (Thread local storage) pentru informații despre gazdă. Pentru aceasta, nu sunt necesare blocaje. De asemenea, rețineți că TLS poate iniția evenimente la actualizare, astfel încât subsistemele de echilibrare a sarcinii și alte componente să poată recalcula cache-urile, structurile de date etc. Aceasta depășește scopul acestui post, dar este utilizată în diverse locuri în cod.

Folosind procedura descrisă mai sus, Envoy poate gestiona fiecare cerere fără nicio blocare (cu excepția celor menționate anterior). Pe lângă complexitatea codului TLS, cea mai mare parte a codului nu trebuie să înțeleagă cum funcționează multithreadingul și poate fi scrisă într-un mod unithread. Acest lucru facilitează scrierea celei mai mari părți a codului, oferind în același timp o performanță excelentă.

Alte subsisteme care utilizează TLS

TLS (Thread local storage) și RCU (Read Copy Update) sunt utilizate pe larg în Envoy.

Exemple de utilizare:

  • Mecanismul de modificare a funcționalității în timpul execuției: Lista curentă a funcționalităților activate este calculată în firul principal. Apoi, fiecărui fir de execuție i se oferă o imagine doar pentru citire utilizând semantica RCU.
  • Înlocuirea tabelelor de rute: pentru tabelele de rute furnizate de RDS (Route Discovery Service), tabelele de rute sunt create în fluxul principal. O copie de citire va fi mai departe furnizată fiecărui fir de execuție utilizând semantica RCU (Read Copy Update). Aceasta face ca modificarea tabelelor de rute să fie atomică și eficientă.
  • Cache-ul antetelor HTTP: După cum se dovedește, calculul antetului HTTP pentru fiecare cerere (la ~25K+ RPS pe nucleu) este destul de costisitor. Envoy calculează antetul centralizat de aproximativ două ori pe secundă și îl furnizează fiecărui lucrător prin TLS și RCU.

Există și alte cazuri, dar exemplele anterioare ar trebui să ofere o bună înțelegere a utilizării TLS.

Probleme de performanță cunoscute (Known performance pitfalls)

Deși în general Envoy funcționează destul de bine, există câteva domenii cunoscute care necesită atenție atunci când este utilizat cu un grad foarte ridicat de paralelism și capacitate de procesare:

  • După cum este deja menționat în acest articol, în prezent toate firele de execuție obțin un blocaj atunci când scriu în bufferul de memorie al jurnalului de acces. La un paralelism și o capacitate de procesare mari, va fi necesară realizarea unui pachet de jurnale de acces pentru fiecare fir de execuție, cu costul unei livrări neordonate atunci când scriu în fișierul final. Ca alternativă, pot fi create jurnale de acces separate pentru fiecare fir de execuție.
  • Deși statisticile sunt foarte optimizate, la un paralelism și o capacitate de procesare foarte ridicată, este probabil să existe concurență atomică asupra statisticilor individuale. Soluția acestei probleme constă în contoare pentru un singur fir de execuție cu resetarea periodică a contoarelor centrale. Aceasta va fi discutată într-o postare ulterioară.
  • Arhitectura existentă nu va funcționa bine dacă Envoy este desfășurat într-un scenariu cu foarte puține conexiuni, care necesită resurse semnificative pentru procesare. Nu există nicio garanție că conexiunile vor fi repartizate uniform între firele de execuție. Aceasta poate fi rezolvată prin implementarea unui echilibrator de conexiuni de muncă, unde se va realiza posibilitatea de schimb de conexiuni între firele de execuție.

Concluzie (Conclusion)

Modelul fluxurilor Envoy este conceput pentru a asigura o programare simplă și un paralelism de masă prin utilizarea potențial excesivă a memoriei și a conexiunilor, dacă acestea nu sunt configurate corect. Această modelare îi permite să funcționeze foarte bine cu un număr extrem de ridicat de fluxuri și lățime de bandă.
Așa cum am menționat pe scurt pe Twitter, designul poate funcționa, de asemenea, deasupra unui stivă de rețea complet funcțională în modul utilizator, cum ar fi DPDK (Data Plane Development Kit), ceea ce poate duce la faptul că serverele obișnuite vor procesa milioane de cereri pe secundă, cu o manipulare completă L7. Va fi foarte interesant să vedem ce se va construi în următorii câțiva ani.
Un ultim comentariu rapid: am fost întrebat de multe ori de ce am ales C++ pentru Envoy. Motivul rămâne acela că este încă singurul limbaj de nivel industrial pe scară largă care poate construi arhitectura descrisă în acest post. C++ nu este cu siguranță potrivit pentru toate sau chiar pentru multe proiecte, dar pentru anumite cazuri de utilizare este încă singurul instrument care poate îndeplini sarcina.

Legături către cod (Links to code)

Legături către fișiere cu interfețele și implementările de antet discutate în acest post:

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster