[Traducere] Model de threading Envoy

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

Am găsit acest articol destul de interesant și, deoarece Envoy este folosit cel mai adesea ca parte a „istio” sau pur și simplu ca „controller de intrare” al kubernetelor, majoritatea oamenilor nu au aceeași interacțiune directă cu acesta ca, de exemplu, cu tipicul Instalări Nginx sau Haproxy. Totuși, dacă ceva se sparge, ar fi bine să înțelegem cum funcționează din interior. Am încercat să traduc cât mai mult text în rusă, inclusiv cuvinte speciale; pentru cei cărora le este dureros să se uite la asta, am lăsat originalele între paranteze. Bun venit la pisica.

Documentația tehnică de nivel scăzut pentru baza de cod Envoy este în prezent destul de rară. Pentru a remedia acest lucru, intenționez să fac o serie de postări pe blog despre diferitele subsisteme ale lui Envoy. Deoarece acesta este primul articol, vă rog să-mi spuneți ce părere aveți și ce v-ar putea interesa în articolele viitoare.

Una dintre cele mai frecvente întrebări tehnice pe care le primesc despre Envoy este să solicit o descriere la nivel scăzut a modelului de threading pe care îl folosește. În această postare, voi descrie modul în care Envoy mapează conexiunile la fire, precum și sistemul Thread Local Storage pe care îl folosește intern pentru a face codul mai paralel și mai performant.

Prezentare generală a firelor

[Traducere] Model de threading Envoy

Envoy utilizează trei tipuri diferite de fluxuri:

  • Principal: Acest thread controlează pornirea și terminarea procesului, toată procesarea API-ului XDS (xDiscovery Service), inclusiv DNS, verificarea stării de sănătate, managementul general al cluster-ului și al timpului de execuție, resetarea statisticilor, administrarea și managementul general al procesului - semnale Linux, repornire la cald, etc. se întâmplă în acest thread este asincron și „neblocant”. În general, firul principal coordonează toate procesele funcționale critice care nu necesită o cantitate mare de CPU pentru a rula. Acest lucru permite ca majoritatea codului de control să fie scris ca și cum ar fi un singur thread.
  • Muncitor: În mod implicit, Envoy creează un fir de lucru pentru fiecare fir hardware din sistem, acesta putând fi controlat folosind opțiunea --concurrency. Fiecare fir de lucru rulează o buclă de evenimente „neblocante”, care este responsabilă pentru ascultarea fiecărui ascultător; la momentul scrierii (29 iulie 2017) nu există fragmentare a ascultătorului, acceptând conexiuni noi, instanțiând o stivă de filtre pentru conexiunea și procesarea tuturor operațiunilor de intrare/ieșire (IO) pe durata de viață a conexiunii. Din nou, acest lucru permite ca majoritatea codului de gestionare a conexiunilor să fie scris ca și cum ar fi un singur thread.
  • Curățare fișiere: Fiecare fișier pe care Envoy îl scrie, în principal jurnalele de acces, are în prezent un fir de blocare independent. Acest lucru se datorează faptului că scrierea în fișierele stocate în cache de sistemul de fișiere chiar și atunci când se utilizează O_NONBLOCK se poate bloca uneori (oftat). Când firele de lucru trebuie să scrie într-un fișier, datele sunt de fapt mutate într-un buffer din memorie unde sunt în cele din urmă eliminate prin fir spălarea fișierului. Aceasta este o zonă a codului în care, din punct de vedere tehnic, toate firele de lucru pot bloca aceeași blocare în timp ce încearcă să umple un buffer de memorie.

Manipularea conexiunii

După cum sa discutat pe scurt mai sus, toate firele de lucru ascultă toți ascultătorii fără nicio fragmentare. Astfel, nucleul este folosit pentru a trimite grațios socket-urile acceptate către firele de lucru. Nucleele moderne sunt, în general, foarte bune la acest lucru, folosesc funcții precum creșterea priorității de intrare/ieșire (IO) pentru a încerca să umple un fir cu lucru înainte de a începe să folosească alte fire care ascultă și ele pe aceeași socket și, de asemenea, nu folosesc round robin. blocare (Spinlock) pentru a procesa fiecare cerere.
Odată ce o conexiune este acceptată pe un fir de lucru, nu părăsește niciodată acel fir. Toată procesarea ulterioară a conexiunii este gestionată în întregime în firul de lucru, inclusiv orice comportament de redirecționare.

Acest lucru are câteva consecințe importante:

  • Toate pool-urile de conexiuni din Envoy sunt alocate unui fir de lucru. Deci, deși grupurile de conexiuni HTTP/2 fac doar o conexiune la fiecare gazdă din amonte la un moment dat, dacă există patru fire de lucru, vor exista patru conexiuni HTTP/2 pentru fiecare gazdă din amonte într-o stare de echilibru.
  • Motivul pentru care Envoy funcționează în acest fel este că, păstrând totul pe un singur fir de lucru, aproape tot codul poate fi scris fără blocare și ca și cum ar fi un singur thread. Acest design facilitează scrierea multor coduri și se adaptează incredibil de bine la un număr aproape nelimitat de fire de lucru.
  • Cu toate acestea, una dintre principalele concluzii este că din punct de vedere al pool-ului de memorie și al eficienței conexiunii, este de fapt foarte important să configurați --concurrency. Având mai multe fire de execuție decât este necesar, va risipi memorie, va crea mai multe conexiuni inactive și va reduce rata de pooling de conexiuni. La Lyft, containerele noastre de transport secundar rulează cu o concurență foarte scăzută, astfel încât performanța să se potrivească aproximativ cu serviciile lângă care stau. Executăm Envoy ca un proxy edge numai la concurență maximă.

Ce înseamnă neblocare?

Termenul „neblocare” a fost folosit de mai multe ori până acum când se discută cum funcționează firele principale și de lucru. Tot codul este scris pe presupunerea că nimic nu este blocat vreodată. Cu toate acestea, acest lucru nu este în întregime adevărat (ce nu este în întregime adevărat?).

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

  • După cum sa discutat, la scrierea jurnalelor de acces, toate firele de lucru dobândesc aceeași blocare înainte ca bufferul de jurnal din memorie să fie umplut. Timpul de menținere a blocării ar trebui să fie foarte mic, dar este posibil ca blocarea să fie contestată la concurență ridicată și debit mare.
  • Envoy folosește un sistem foarte complex pentru a gestiona statisticile care sunt locale pentru fir. Acesta va fi subiectul unei postări separate. Cu toate acestea, voi menționa pe scurt că, ca parte a procesării locale a statisticilor firelor, uneori este necesar să obțineți o blocare pe un „magazin de statistici” central. Această blocare nu ar trebui să fie niciodată necesară.
  • Firul principal trebuie să se coordoneze periodic cu toate firele de lucru. Acest lucru se realizează prin „publicarea” de la firul principal în firele de lucru și, uneori, de la firele de lucru înapoi la firul principal. Trimiterea necesită o blocare, astfel încât mesajul publicat să poată fi pus în coadă pentru livrare ulterioară. Aceste încuietori nu ar trebui niciodată contestate serios, dar pot fi blocate din punct de vedere tehnic.
  • Când Envoy scrie un jurnal în fluxul de erori de sistem (eroare standard), acesta capătă o blocare pentru întregul proces. În general, înregistrarea locală a lui Envoy este considerată groaznică din punct de vedere al performanței, așa că nu s-a acordat prea multă atenție îmbunătățirii acesteia.
  • Există alte câteva blocări aleatorii, dar niciuna dintre ele nu este critică pentru performanță și nu ar trebui să fie contestată niciodată.

Stocare locală a firului

Datorită modului în care Envoy separă responsabilitățile firului principal de responsabilitățile firului de lucru, există o cerință ca procesarea complexă să poată fi efectuată pe firul principal și apoi furnizată fiecărui thread de lucru într-o manieră extrem de concurentă. Această secțiune descrie Envoy Thread Local Storage (TLS) la un nivel înalt. În secțiunea următoare voi descrie modul în care este utilizat pentru a gestiona un cluster.
[Traducere] Model de threading Envoy

După cum s-a descris deja, firul principal gestionează practic toate funcționalitățile planului de management și control din procesul Envoy. Planul de control este puțin supraîncărcat aici, dar când îl priviți în cadrul procesului Envoy în sine și îl comparați cu redirecționarea pe care o fac firele de lucru, are sens. Regula generală este că procesul principal de fir funcționează și apoi trebuie să actualizeze fiecare fir de lucru în funcție de rezultatul acelei lucrări. în acest caz, firul de lucru nu trebuie să obțină o blocare la fiecare acces.

Sistemul TLS (Thread local storage) al lui Envoy funcționează după cum urmează:

  • Codul care rulează pe firul principal poate aloca un slot TLS pentru întregul proces. Deși acest lucru este abstractizat, în practică este un index într-un vector, oferind acces O(1).
  • Firul principal poate instala date arbitrare în slotul său. Când se face acest lucru, datele sunt publicate în fiecare fir de lucru ca un eveniment normal al buclei.
  • Firele de lucru lucrătoare pot citi din slotul lor TLS și pot prelua orice date locale disponibile acolo.

Deși este o paradigmă foarte simplă și incredibil de puternică, este foarte asemănătoare cu conceptul de blocare RCU (Read-Copy-Update). În esență, firele de execuție de lucru nu văd niciodată modificări de date în sloturile TLS în timp ce se lucrează. Schimbarea are loc numai în perioada de odihnă dintre evenimentele de lucru.

Envoy îl folosește în două moduri diferite:

  • Prin stocarea diferitelor date pe fiecare fir de lucru, datele pot fi accesate fără nicio blocare.
  • Prin menținerea unui pointer partajat către datele globale în modul numai citire pe fiecare fir de lucru. Astfel, fiecare fir de lucru are un număr de referințe de date care nu poate fi decrementat în timp ce lucrarea rulează. Numai când toți lucrătorii se calmează și încarcă date noi partajate, datele vechi vor fi distruse. Acesta este identic cu RCU.

Thread de actualizare a clusterului

În această secțiune, voi descrie modul în care este utilizat TLS (stocare locală Thread) pentru a gestiona un cluster. Gestionarea clusterelor include procesarea xDS API și/sau DNS, precum și verificarea stării de sănătate.
[Traducere] Model de threading Envoy

Managementul fluxului de cluster include următoarele componente și pași:

  1. Cluster Manager este o componentă din Envoy care gestionează toate clusterele cunoscute în amonte, API-ul Cluster Discovery Service (CDS), API-urile Secret Discovery Service (SDS) și Endpoint Discovery Service (EDS), DNS și verificări externe active. Acesta este responsabil pentru crearea unei vizualizări „eventual coerente” a fiecărui cluster din amonte, care include gazde descoperite, precum și starea de sănătate.
  2. Verificatorul de sănătate efectuează o verificare activă a stării de sănătate și raportează managerului de cluster modificările stării de sănătate.
  3. CDS (Serviciul de descoperire a clusterelor) / SDS (Serviciul de descoperire secretă) / EDS (Serviciul de descoperire a punctelor finale) / DNS sunt efectuate pentru a determina apartenența la cluster. Modificarea stării este returnată managerului de cluster.
  4. Fiecare thread de lucru execută continuu o buclă de evenimente.
  5. Când managerul de cluster determină că starea unui cluster s-a schimbat, creează un nou instantaneu numai pentru citire a stării clusterului și îl trimite fiecărui fir de lucru.
  6. În următoarea perioadă de liniște, firul de lucru lucrător va actualiza instantaneul în slotul TLS alocat.
  7. În timpul unui eveniment I/O care ar trebui să determine gazda pentru echilibrarea încărcăturii, echilibratorul de încărcare va solicita un slot TLS (stocare locală Thread) pentru a obține informații despre gazdă. Acest lucru nu necesită încuietori. De asemenea, rețineți că TLS poate declanșa și evenimente de actualizare, astfel încât echilibratorii de încărcare și alte componente să poată recalcula cache-urile, structurile de date etc. Acest lucru depășește domeniul de aplicare al acestei postări, dar este folosit în diferite locuri în cod.

Utilizând procedura de mai sus, Envoy poate procesa fiecare cerere fără nicio blocare (cu excepția celor descrise anterior). În afară de complexitatea codului TLS în sine, cea mai mare parte a codului nu trebuie să înțeleagă cum funcționează multithreading-ul și poate fi scris cu un singur thread. Acest lucru face ca majoritatea codului să fie mai ușor de scris, pe lângă performanța superioară.

Alte subsisteme care utilizează TLS

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

Exemple de utilizare:

  • Mecanism de modificare a funcționalității în timpul execuției: Lista curentă a funcționalităților activate este calculată în firul principal. Fiecare fir de lucru lucrător primește apoi un instantaneu numai pentru citire folosind semantica RCU.
  • Înlocuirea tabelelor de rute: Pentru tabelele de rute furnizate de RDS (Route Discovery Service), tabelele de rute sunt create pe firul principal. Instantaneul numai pentru citire va fi furnizat ulterior fiecărui fir de lucru folosind semantica RCU (Read Copy Update). Acest lucru face ca schimbarea tabelelor de rute să fie eficientă din punct de vedere atomic.
  • Memorarea în cache a antetului HTTP: După cum se dovedește, calcularea antetului HTTP pentru fiecare cerere (în timp ce rulează ~25K+ RPS per nucleu) este destul de costisitoare. Envoy calculează central antetul aproximativ la fiecare jumătate de secundă și îl oferă 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.

Capcane de performanță cunoscute

În timp ce Envoy funcționează destul de bine în general, există câteva zone notabile care necesită atenție atunci când este utilizat cu concurență și debit foarte mare:

  • După cum este descris în acest articol, în prezent toate firele de execuție de lucru obțin o blocare atunci când scriu în memoria tampon de memorie de jurnal de acces. La concurență ridicată și la debit mare, va trebui să grupați jurnalele de acces pentru fiecare fir de lucru în detrimentul livrării în afara comenzii atunci când scrieți în fișierul final. Alternativ, puteți crea un jurnal de acces separat pentru fiecare fir de lucru.
  • Deși statisticile sunt extrem de optimizate, la concurență și debit foarte mare va exista probabil o dispută atomică asupra statisticilor individuale. Soluția la această problemă este contoarele pe fir de lucru cu resetarea periodică a contoarelor centrale. Acest lucru va fi discutat într-o postare ulterioară.
  • Arhitectura actuală nu va funcționa bine dacă Envoy este implementat într-un scenariu în care există foarte puține conexiuni care necesită resurse de procesare semnificative. Nu există nicio garanție că conexiunile vor fi distribuite uniform între firele de lucru. Acest lucru poate fi rezolvat prin implementarea echilibrării conexiunilor de lucru, care va permite schimbul de conexiuni între firele de lucru.

Concluzie

Modelul de threading al lui Envoy este conceput pentru a oferi ușurință de programare și paralelism masiv în detrimentul memoriei și conexiunilor potențial irosite, dacă nu sunt configurate corect. Acest model îi permite să funcționeze foarte bine la un număr foarte mare de fire și debit.
După cum am menționat pe scurt pe Twitter, designul poate rula și pe o stivă de rețea completă în modul utilizator, cum ar fi DPDK (Kit de dezvoltare a planului de date), ceea ce poate duce la serverele convenționale care gestionează milioane de solicitări pe secundă cu procesare L7 completă. Va fi foarte interesant de văzut ce se construiește î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 că este încă singurul limbaj de grad industrial utilizat pe scară largă în care poate fi construită arhitectura descrisă în această postare. C++ cu siguranță nu este potrivit pentru toate sau chiar pentru multe proiecte, dar pentru anumite cazuri de utilizare este încă singurul instrument pentru a face treaba.

Link-uri către cod

Link-uri către fișiere cu interfețe și implementări de antet discutate în această postare:

Sursa: www.habr.com

Adauga un comentariu