Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud
Bună, sunt Sergey Elantsev, mă dezvolt echilibrator de încărcare a rețelei în Yandex.Cloud. Anterior, am condus dezvoltarea echilibrului L7 pentru portalul Yandex - colegii glumesc că, indiferent ce fac, se dovedește a fi un echilibrant. Voi spune cititorilor Habr cum să gestioneze încărcarea într-o platformă cloud, ce vedem ca fiind instrumentul ideal pentru atingerea acestui obiectiv și cum ne îndreptăm către construirea acestui instrument.

Mai întâi, să introducem câțiva termeni:

  • VIP (IP virtual) - adresa IP a echilibrului
  • Server, backend, instanță - o mașină virtuală care rulează o aplicație
  • RIP (Real IP) - adresa IP a serverului
  • Healthcheck - verificarea pregătirii serverului
  • Zona de disponibilitate, AZ - infrastructură izolată într-un centru de date
  • Regiune - o uniune de diferite AZ

Echilibratoarele de sarcină rezolvă trei sarcini principale: efectuează echilibrarea în sine, îmbunătățesc toleranța la erori a serviciului și simplifică scalarea acestuia. Toleranța la erori este asigurată prin gestionarea automată a traficului: echilibrerul monitorizează starea aplicației și exclude de la echilibrare cazurile care nu trec de verificarea de viabilitate. Scalare este asigurată prin distribuirea uniformă a încărcăturii între instanțe, precum și prin actualizarea listei de instanțe din mers. Dacă echilibrarea nu este suficient de uniformă, unele dintre instanțe vor primi o sarcină care depășește limita de capacitate, iar serviciul va deveni mai puțin fiabil.

Un echilibrator de încărcare este adesea clasificat după nivelul de protocol din modelul OSI pe care rulează. Cloud Balancer funcționează la nivel TCP, care corespunde celui de-al patrulea strat, L4.

Să trecem la o prezentare generală a arhitecturii Cloud balancer. Vom crește treptat nivelul de detaliu. Împărțim componentele echilibrului în trei clase. Clasa planului de configurare este responsabilă pentru interacțiunea utilizatorului și stochează starea țintă a sistemului. Planul de control stochează starea curentă a sistemului și gestionează sistemele din clasa planului de date, care sunt direct responsabile pentru livrarea traficului de la clienți către instanțele dumneavoastră.

Planul de date

Traficul ajunge pe dispozitive scumpe numite routere de frontieră. Pentru a crește toleranța la erori, mai multe astfel de dispozitive funcționează simultan într-un singur centru de date. Apoi, traficul merge către echilibratori, care anunță adrese IP anycast către toate AZ-urile prin BGP pentru clienți. 

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Traficul este transmis prin ECMP - aceasta este o strategie de rutare conform căreia pot exista mai multe rute la fel de bune către țintă (în cazul nostru, ținta va fi adresa IP de destinație) și pachetele pot fi trimise de-a lungul oricăreia dintre ele. De asemenea, susținem lucrul în mai multe zone de disponibilitate după următoarea schemă: facem reclamă o adresă în fiecare zonă, traficul se îndreaptă spre cea mai apropiată și nu depășește limitele acesteia. Mai târziu în postare vom analiza mai detaliat ce se întâmplă cu traficul.

Planul de configurare

 
Componenta cheie a planului de configurare este API-ul, prin care se efectuează operațiuni de bază cu balansoare: crearea, ștergerea, modificarea compoziției instanțelor, obținerea rezultatelor verificărilor de sănătate etc. Pe de o parte, acesta este un API REST, iar pe de altă parte. altele, noi în Cloud folosim foarte des cadrul gRPC, așa că „traducem” REST în gRPC și apoi folosim doar gRPC. Orice solicitare duce la crearea unei serii de sarcini idempotente asincrone care sunt executate pe un grup comun de lucrători Yandex.Cloud. Sarcinile sunt scrise în așa fel încât să poată fi suspendate în orice moment și apoi repornite. Acest lucru asigură scalabilitate, repetabilitate și înregistrarea operațiunilor.

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Ca urmare, sarcina din API va face o solicitare către controlorul serviciului de echilibrare, care este scrisă în Go. Poate adăuga și elimina echilibranți, poate modifica compoziția backend-urilor și setărilor. 

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Serviciul își stochează starea în baza de date Yandex, o bază de date gestionată distribuită pe care o veți putea folosi în curând. În Yandex.Cloud, așa cum am făcut deja a declarat, se aplică conceptul de hrană pentru câini: dacă noi înșine folosim serviciile noastre, atunci și clienții noștri vor fi bucuroși să le folosească. Baza de date Yandex este un exemplu de implementare a unui astfel de concept. Stocăm toate datele noastre în YDB și nu trebuie să ne gândim la menținerea și scalarea bazei de date: aceste probleme sunt rezolvate pentru noi, folosim baza de date ca un serviciu.

Să revenim la controlerul de echilibrare. Sarcina sa este să salveze informații despre echilibrator și să trimită o sarcină pentru a verifica starea de pregătire a mașinii virtuale către controlerul de verificare a stării de sănătate.

Controler pentru verificarea sănătății

Primește solicitări de modificare a regulilor de verificare, le salvează în YDB, distribuie sarcini între nodurile healtcheck și agregează rezultatele, care sunt apoi salvate în baza de date și trimise controlerului loadbalancer. La rândul său, trimite o solicitare de modificare a compoziției clusterului în planul de date către nodul loadbalancer, despre care voi discuta mai jos.

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Să vorbim mai multe despre controalele de sănătate. Ele pot fi împărțite în mai multe clase. Auditurile au criterii de succes diferite. Verificările TCP trebuie să stabilească cu succes o conexiune într-un interval de timp fix. Verificările HTTP necesită atât o conexiune reușită, cât și un răspuns cu un cod de stare 200.

De asemenea, cecurile diferă în funcție de clasa de acțiune - sunt active și pasive. Verificările pasive pur și simplu monitorizează ceea ce se întâmplă cu traficul fără a lua nicio acțiune specială. Acest lucru nu funcționează foarte bine pe L4 deoarece depinde de logica protocoalelor de nivel superior: pe L4 nu există informații despre cât timp a durat operația sau dacă finalizarea conexiunii a fost bună sau proastă. Verificările active necesită ca echilibrantul să trimită cereri către fiecare instanță de server.

Majoritatea sistemelor de echilibrare a încărcăturii efectuează ei înșiși verificări de viabilitate. La Cloud, am decis să separăm aceste părți ale sistemului pentru a crește scalabilitatea. Această abordare ne va permite să creștem numărul de echilibratori, menținând în același timp numărul de solicitări de verificare a sănătății către serviciu. Verificările sunt efectuate de noduri separate de verificare a stării de sănătate, peste care țintele de verificare sunt fragmentate și replicate. Nu puteți efectua verificări de la o singură gazdă, deoarece poate eșua. Atunci nu vom afla starea instanțelor pe care le-a verificat. Efectuăm verificări pe oricare dintre instanțe de la cel puțin trei noduri de verificare a stării de sănătate. Împărțim scopurile verificărilor între noduri folosind algoritmi de hashing consecvenți.

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Separarea echilibrului și a controlului de sănătate poate duce la probleme. Dacă nodul de verificare a stării de sănătate face solicitări instanței, ocolind echilibrerul (care în prezent nu deservește trafic), atunci apare o situație ciudată: resursa pare să fie vie, dar traficul nu va ajunge la ea. Rezolvăm această problemă în felul acesta: avem garanția de a iniția traficul de verificare a stării de sănătate prin echilibrare. Cu alte cuvinte, schema de mutare a pachetelor cu trafic de la clienți și din controale de sănătate diferă minim: în ambele cazuri, pachetele vor ajunge la echilibratori, care le vor livra la resursele țintă.

Diferența este că clienții fac solicitări către VIP, în timp ce controalele de sănătate fac solicitări către fiecare RIP individual. O problemă interesantă apare aici: oferim utilizatorilor noștri posibilitatea de a crea resurse în rețele IP gri. Să ne imaginăm că există doi proprietari diferiți de cloud care și-au ascuns serviciile în spatele echilibratorilor. Fiecare dintre ele are resurse în subrețeaua 10.0.0.1/24, cu aceleași adrese. Trebuie să le puteți distinge cumva și aici trebuie să vă scufundați în structura rețelei virtuale Yandex.Cloud. Este mai bine să aflați mai multe detalii în videoclip de la about:cloud event, este important pentru noi acum că rețeaua este multistratificată și are tuneluri care pot fi distinse prin id-ul de subrețea.

Nodurile Healthcheck contactează echilibratorii folosind așa-numitele adrese cvasi-IPv6. O cvasi-adresă este o adresă IPv6 cu o adresă IPv4 și un ID de subrețea de utilizator încorporate în ea. Traficul ajunge la echilibrator, care extrage adresa resursei IPv4 din acesta, înlocuiește IPv6 cu IPv4 și trimite pachetul către rețeaua utilizatorului.

Traficul invers merge în același mod: echilibratorul vede că destinația este o rețea gri de la verificatorii de sănătate și convertește IPv4 în IPv6.

VPP - inima planului de date

Echilibratorul este implementat folosind tehnologia Vector Packet Processing (VPP), un cadru de la Cisco pentru procesarea în lot a traficului de rețea. În cazul nostru, cadrul funcționează pe deasupra bibliotecii de gestionare a dispozitivelor de rețea din spațiul utilizatorului - Kit de dezvoltare a planului de date (DPDK). Acest lucru asigură o performanță ridicată de procesare a pachetelor: în nucleu apar mult mai puține întreruperi și nu există schimbări de context între spațiul kernel și spațiul utilizator. 

VPP merge și mai departe și stoarce și mai multă performanță din sistem prin combinarea pachetelor în loturi. Câștigurile de performanță provin din utilizarea agresivă a cache-urilor pe procesoarele moderne. Sunt utilizate ambele cache de date (pachetele sunt procesate în „vectori”, datele sunt aproape unele de altele) și cache de instrucțiuni: în VPP, procesarea pachetelor urmează un grafic, ale cărui noduri conțin funcții care îndeplinesc aceeași sarcină.

De exemplu, procesarea pachetelor IP în VPP are loc în următoarea ordine: mai întâi, anteturile pachetelor sunt analizate în nodul de parsare, apoi sunt trimise la nod, care transmite pachetele în continuare conform tabelelor de rutare.

Puțin hardcore. Autorii VPP nu tolerează compromisuri în utilizarea cache-urilor procesorului, astfel încât codul tipic pentru procesarea unui vector de pachete conține vectorizare manuală: există o buclă de procesare în care este procesată o situație precum „avem patru pachete în coadă”, apoi la fel pentru doi, apoi - pentru unul. Instrucțiunile de preluare preliminară sunt adesea folosite pentru a încărca datele în cache pentru a accelera accesul la acestea în iterațiile ulterioare.

n_left_from = frame->n_vectors;
while (n_left_from > 0)
{
    vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);
    // ...
    while (n_left_from >= 4 && n_left_to_next >= 2)
    {
        // processing multiple packets at once
        u32 next0 = SAMPLE_NEXT_INTERFACE_OUTPUT;
        u32 next1 = SAMPLE_NEXT_INTERFACE_OUTPUT;
        // ...
        /* Prefetch next iteration. */
        {
            vlib_buffer_t *p2, *p3;

            p2 = vlib_get_buffer (vm, from[2]);
            p3 = vlib_get_buffer (vm, from[3]);

            vlib_prefetch_buffer_header (p2, LOAD);
            vlib_prefetch_buffer_header (p3, LOAD);

            CLIB_PREFETCH (p2->data, CLIB_CACHE_LINE_BYTES, STORE);
            CLIB_PREFETCH (p3->data, CLIB_CACHE_LINE_BYTES, STORE);
        }
        // actually process data
        /* verify speculative enqueues, maybe switch current next frame */
        vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
                to_next, n_left_to_next,
                bi0, bi1, next0, next1);
    }

    while (n_left_from > 0 && n_left_to_next > 0)
    {
        // processing packets by one
    }

    // processed batch
    vlib_put_next_frame (vm, node, next_index, n_left_to_next);
}

Deci, Healthchecks vorbește prin IPv6 cu VPP, care le transformă în IPv4. Acest lucru este realizat de un nod din grafic, pe care îl numim NAT algoritmic. Pentru traficul invers (și conversia de la IPv6 la IPv4) există același nod NAT algoritmic.

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Traficul direct de la clienții de echilibrare trece prin nodurile grafice, care efectuează singuri echilibrarea. 

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Primul nod sunt sesiunile sticky. Stochează hash-ul de 5-tuplu pentru sesiunile stabilite. 5-tuplu include adresa și portul clientului de la care sunt transmise informații, adresa și porturile resurselor disponibile pentru recepționarea traficului, precum și protocolul de rețea. 

Hash-ul cu 5 tuple ne ajută să efectuăm mai puține calcule în nodul de hashing coerent ulterior, precum și să gestionăm mai bine modificările listei de resurse din spatele echilibratorului. Când un pachet pentru care nu există sesiune ajunge la echilibrator, acesta este trimis la nodul de hashing consistent. Aici are loc echilibrarea folosind hashing consistent: selectăm o resursă din lista de resurse „live” disponibile. Apoi, pachetele sunt trimise la nodul NAT, care înlocuiește de fapt adresa de destinație și recalculează sumele de control. După cum puteți vedea, respectăm regulile VPP - like to like, grupând calcule similare pentru a crește eficiența cache-urilor procesorului.

Hashing constant

De ce l-am ales și ce este? Mai întâi, să luăm în considerare sarcina anterioară - selectarea unei resurse din listă. 

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Cu hashing inconsecvent, hash-ul pachetului de intrare este calculat și o resursă este selectată din listă cu restul împărțirii acestui hash la numărul de resurse. Atâta timp cât lista rămâne neschimbată, această schemă funcționează bine: trimitem întotdeauna pachete cu același 5-tuplu către aceeași instanță. Dacă, de exemplu, o resursă nu mai răspunde la controalele de sănătate, atunci pentru o parte semnificativă a hashurilor alegerea se va schimba. Conexiunile TCP ale clientului vor fi întrerupte: un pachet care a ajuns anterior la instanța A poate începe să ajungă la instanța B, care nu este familiarizat cu sesiunea pentru acest pachet.

Hashingul consecvent rezolvă problema descrisă. Cel mai simplu mod de a explica acest concept este următorul: imaginați-vă că aveți un inel către care distribuiți resurse prin hash (de exemplu, după IP:port). Selectarea unei resurse înseamnă rotirea roții cu un unghi, care este determinat de hash-ul pachetului.

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Acest lucru minimizează redistribuirea traficului atunci când compoziția resurselor se modifică. Ștergerea unei resurse va afecta doar partea din inelul hashing consistent în care a fost localizată resursa. Adăugarea unei resurse modifică și distribuția, dar avem un nod de sesiuni sticky, care ne permite să nu comutăm sesiunile deja stabilite la resurse noi.

Ne-am uitat la ce se întâmplă cu traficul direct între echilibrator și resurse. Acum să ne uităm la traficul de întoarcere. Urmează același model ca și traficul de verificare - prin NAT algoritmic, adică prin NAT 44 invers pentru traficul client și prin NAT 46 pentru traficul verificărilor de sănătate. Aderăm la propria noastră schemă: unificăm traficul de controale de sănătate și traficul real al utilizatorilor.

Loadbalancer-nod și componente asamblate

Compoziția echilibratorilor și a resurselor în VPP este raportată de serviciul local - loadbalancer-node. Acesta se abonează la fluxul de evenimente de la loadbalancer-controller și poate reprezenta diferența dintre starea curentă VPP și starea țintă primită de la controler. Obținem un sistem închis: evenimentele din API vin la controlerul de echilibrare, care atribuie sarcini controlorului de verificare a sănătății pentru a verifica „vivabilitatea” resurselor. Acesta, la rândul său, atribuie sarcini nodului de verificare a sănătății și agregează rezultatele, după care le trimite înapoi controlerului de echilibrare. Loadbalancer-node se abonează la evenimente de la controler și schimbă starea VPP-ului. Într-un astfel de sistem, fiecare serviciu știe doar ce este necesar despre serviciile vecine. Numărul de conexiuni este limitat și avem capacitatea de a opera și scala diferite segmente în mod independent.

Arhitectura echilibrului de încărcare a rețelei în Yandex.Cloud

Ce probleme au fost evitate?

Toate serviciile noastre din planul de control sunt scrise în Go și au caracteristici bune de scalare și fiabilitate. Go are multe biblioteci open source pentru construirea de sisteme distribuite. Folosim în mod activ GRPC, toate componentele conțin o implementare open source a descoperirii serviciilor - serviciile noastre monitorizează performanța celuilalt, își pot schimba compoziția în mod dinamic și am legat acest lucru cu echilibrarea GRPC. Pentru metrici, folosim și o soluție open source. În planul de date, am obținut performanță decentă și o rezervă mare de resurse: s-a dovedit a fi foarte dificil să asamblam un suport pe care să ne putem baza pe performanța unui VPP, mai degrabă decât pe o placă de rețea de fier.

Probleme și soluții

Ce nu a mers atât de bine? Go are gestionarea automată a memoriei, dar scurgeri de memorie se mai produc. Cel mai simplu mod de a le face față este să rulați goroutine și să nu uitați să le opriți. La pachet: Urmărește consumul de memorie al programelor tale Go. Adesea, un indicator bun este numărul de goroutine. Există un plus în această poveste: în Go este ușor să obțineți date de rulare - consumul de memorie, numărul de rutine de rulare și mulți alți parametri.

De asemenea, Go poate să nu fie cea mai bună alegere pentru testele funcționale. Sunt destul de verbose, iar abordarea standard de „a rula totul în CI într-un lot” nu este foarte potrivită pentru ei. Cert este că testele funcționale necesită mai multe resurse și provoacă timeout-uri reale. Din această cauză, testele pot eșua deoarece CPU-ul este ocupat cu testele unitare. Concluzie: Dacă este posibil, efectuați teste „grele” separat de testele unitare. 

Arhitectura evenimentelor microservicii este mai complexă decât un monolit: colectarea jurnalelor de pe zeci de mașini diferite nu este foarte convenabilă. Concluzie: dacă faci microservicii, gândește-te imediat la urmărire.

Planurile noastre

Vom lansa un echilibrator intern, un echilibrator IPv6, vom adăuga suport pentru scripturile Kubernetes, vom continua să ne spargem serviciile (în prezent, doar healthcheck-node și healthcheck-ctrl sunt fragmentate), vom adăuga noi verificări de sănătate și vom implementa, de asemenea, agregarea inteligentă a controalelor. Luăm în considerare posibilitatea de a face serviciile noastre și mai independente - astfel încât acestea să comunice nu direct între ele, ci folosind o coadă de mesaje. Un serviciu compatibil cu SQS a apărut recent în Cloud Coada de mesaje Yandex.

Recent, a avut loc lansarea publică a Yandex Load Balancer. Explora documentație la serviciu, gestionați echilibratorii într-un mod convenabil pentru dvs. și creșteți toleranța la erori a proiectelor dvs.!

Sursa: www.habr.com

Adauga un comentariu