Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud
Hei, jeg er Sergey Elantsev, jeg utvikler meg nettverksbelastningsbalanser i Yandex.Cloud. Tidligere ledet jeg utviklingen av L7-balansereren for Yandex-portalen - kolleger fleiper med at uansett hva jeg gjør, viser det seg å være en balanserer. Jeg vil fortelle Habr-lesere hvordan de skal håndtere belastningen i en skyplattform, hva vi ser på som det ideelle verktøyet for å nå dette målet, og hvordan vi går mot å bygge dette verktøyet.

La oss først introdusere noen begreper:

  • VIP (Virtuell IP) - balanserende IP-adresse
  • Server, backend, instans - en virtuell maskin med en applikasjon som kjører
  • RIP (Real IP) - server IP-adresse
  • Helsesjekk - sjekker serverberedskap
  • Availability Zone, AZ - isolert infrastruktur i et datasenter
  • Region - en forening av forskjellige AZ-er

Lastbalansere løser tre hovedoppgaver: de utfører balanseringen selv, forbedrer feiltoleransen til tjenesten og forenkler skaleringen. Feiltoleranse er sikret gjennom automatisk trafikkstyring: balanseringsenheten overvåker applikasjonens tilstand og utelukker fra balansering forekomster som ikke består liveness-kontrollen. Skalering sikres ved å fordele belastningen jevnt på tvers av instanser, samt oppdatere listen over instanser i farten. Dersom balanseringen ikke er jevn nok, vil noen av instansene få en belastning som overstiger kapasitetsgrensen, og tjenesten blir mindre pålitelig.

En lastbalanser blir ofte klassifisert etter protokolllaget fra OSI-modellen den kjører på. Cloud Balancer opererer på TCP-nivå, som tilsvarer det fjerde laget, L4.

La oss gå videre til en oversikt over Cloud balancer-arkitekturen. Vi vil gradvis øke detaljnivået. Vi deler balanserkomponentene inn i tre klasser. Konfigurasjonsplanklassen er ansvarlig for brukerinteraksjon og lagrer måltilstanden til systemet. Kontrollplanet lagrer den nåværende statusen til systemet og administrerer systemer fra dataplanklassen, som er direkte ansvarlige for å levere trafikk fra klienter til instansene dine.

Dataplan

Trafikken ender opp på dyre enheter som kalles grenserutere. For å øke feiltoleransen opererer flere slike enheter samtidig i ett datasenter. Deretter går trafikken til balansere, som kunngjør eventuelle cast IP-adresser til alle AZ-er via BGP for klienter. 

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Trafikk overføres over ECMP - dette er en rutestrategi i henhold til at det kan være flere like gode ruter til målet (i vårt tilfelle vil målet være destinasjons-IP-adressen) og pakker kan sendes langs hvilken som helst av dem. Vi støtter også arbeid i flere tilgjengelighetssoner i henhold til følgende ordning: vi annonserer en adresse i hver sone, trafikken går til den nærmeste og går ikke utover grensene. Senere i innlegget skal vi se nærmere på hva som skjer med trafikken.

Konfigurer fly

 
Nøkkelkomponenten i konfigurasjonsplanet er APIen, gjennom hvilken grunnleggende operasjoner med balansere utføres: opprettelse, sletting, endring av sammensetningen av forekomster, innhenting av helsesjekkresultater osv. På den ene siden er dette en REST API, og på den ene siden. annet, vi i skyen bruker veldig ofte rammeverket gRPC, så vi "oversetter" REST til gRPC og bruker deretter bare gRPC. Enhver forespørsel fører til opprettelsen av en serie asynkrone idempotente oppgaver som utføres på en felles pool av Yandex.Cloud-arbeidere. Oppgaver er skrevet på en slik måte at de når som helst kan suspenderes og deretter startes på nytt. Dette sikrer skalerbarhet, repeterbarhet og logging av operasjoner.

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Som et resultat vil oppgaven fra API-en sende en forespørsel til balansertjenestekontrolleren, som er skrevet i Go. Den kan legge til og fjerne balansere, endre sammensetningen av backends og innstillinger. 

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Tjenesten lagrer tilstanden sin i Yandex Database, en distribuert administrert database som du snart vil kunne bruke. I Yandex.Cloud, som vi allerede fortalte, hundematkonseptet gjelder: hvis vi selv bruker tjenestene våre, vil våre kunder også gjerne bruke dem. Yandex Database er et eksempel på implementeringen av et slikt konsept. Vi lagrer alle våre data i YDB, og vi trenger ikke tenke på å vedlikeholde og skalere databasen: disse problemene er løst for oss, vi bruker databasen som en tjeneste.

La oss gå tilbake til balansekontrolleren. Dens oppgave er å lagre informasjon om balanseren og sende en oppgave for å sjekke beredskapen til den virtuelle maskinen til helsesjekkkontrolleren.

Helsekontrollkontroller

Den mottar forespørsler om å endre sjekkregler, lagrer dem i YDB, fordeler oppgaver mellom helsesjekknoder og samler resultatene, som deretter lagres i databasen og sendes til loadbalancer-kontrolleren. Den sender på sin side en forespørsel om å endre sammensetningen av klyngen i dataplanet til loadbalancer-noden, som jeg vil diskutere nedenfor.

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

La oss snakke mer om helsesjekker. De kan deles inn i flere klasser. Tilsyn har ulike suksesskriterier. TCP-sjekker må lykkes med å etablere en tilkobling innen en fast tidsperiode. HTTP-sjekker krever både en vellykket tilkobling og et svar med en 200-statuskode.

Sjekker er også forskjellige i aksjonsklassen - de er aktive og passive. Passive kontroller overvåker ganske enkelt hva som skjer med trafikken uten å ta noen spesielle tiltak. Dette fungerer ikke veldig bra på L4 fordi det avhenger av logikken til protokollene på høyere nivå: på L4 er det ingen informasjon om hvor lang tid operasjonen tok eller om tilkoblingsfullføringen var god eller dårlig. Aktive kontroller krever at balansereren sender forespørsler til hver serverforekomst.

De fleste lastbalansere utfører liveness-sjekker selv. Hos Cloud bestemte vi oss for å skille disse delene av systemet for å øke skalerbarheten. Denne tilnærmingen vil tillate oss å øke antallet balansere og samtidig opprettholde antallet helsesjekkforespørsler til tjenesten. Kontroller utføres av separate helsesjekknoder, over hvilke sjekkmål er sønderdelt og replikert. Du kan ikke utføre kontroller fra én vert, da den kan mislykkes. Da får vi ikke statusen til forekomstene han sjekket. Vi utfører kontroller på alle forekomstene fra minst tre helsesjekknoder. Vi deler hensikten med sjekker mellom noder ved å bruke konsistente hashing-algoritmer.

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Å skille balansering og helsesjekk kan føre til problemer. Hvis helsesjekknoden sender forespørsler til forekomsten og omgår balanseren (som for øyeblikket ikke betjener trafikk), så oppstår en merkelig situasjon: ressursen ser ut til å være i live, men trafikken vil ikke nå den. Vi løser dette problemet på denne måten: vi er garantert å starte helsesjekktrafikk gjennom balansere. Med andre ord, ordningen for å flytte pakker med trafikk fra klienter og fra helsesjekker er minimalt forskjellig: i begge tilfeller vil pakkene nå balansererne, som vil levere dem til målressursene.

Forskjellen er at klienter sender forespørsler til VIP, mens helsesjekker sender forespørsler til hver enkelt RIP. Et interessant problem oppstår her: vi gir våre brukere muligheten til å lage ressurser i grå IP-nettverk. La oss forestille oss at det er to forskjellige skyeiere som har gjemt tjenestene sine bak balansere. Hver av dem har ressurser i undernettet 10.0.0.1/24, med de samme adressene. Du må på en eller annen måte kunne skille dem, og her må du dykke inn i strukturen til det virtuelle Yandex.Cloud-nettverket. Det er bedre å finne ut flere detaljer i video fra about:cloud-arrangementet, er det viktig for oss nå at nettverket er flerlags og har tunneler som kan skilles ut med subnett-ID.

Healthcheck-noder kontakter balansere ved å bruke såkalte kvasi-IPv6-adresser. En kvasi-adresse er en IPv6-adresse med en IPv4-adresse og brukerundernett-ID innebygd i den. Trafikken når balanseringsenheten, som trekker ut IPv4-ressursadressen fra den, erstatter IPv6 med IPv4 og sender pakken til brukerens nettverk.

Den omvendte trafikken går på samme måte: balanseren ser at destinasjonen er et grått nettverk fra helsesjekkere, og konverterer IPv4 til IPv6.

VPP - hjertet av dataplanet

Balanseringen er implementert ved hjelp av Vector Packet Processing (VPP) teknologi, et rammeverk fra Cisco for batchbehandling av nettverkstrafikk. I vårt tilfelle fungerer rammeverket på toppen av nettverksadministrasjonsbiblioteket for brukerrom - Data Plane Development Kit (DPDK). Dette sikrer høy pakkebehandlingsytelse: mye færre avbrudd forekommer i kjernen, og det er ingen kontekstbytter mellom kjerneplass og brukerplass. 

VPP går enda lenger og presser enda mer ytelse ut av systemet ved å kombinere pakker i batcher. Ytelsesgevinsten kommer fra den aggressive bruken av cacher på moderne prosessorer. Både datacacher brukes (pakker behandles i "vektorer", dataene er nær hverandre) og instruksjonscacher: i VPP følger pakkebehandling en graf, hvis noder inneholder funksjoner som utfører samme oppgave.

For eksempel skjer behandlingen av IP-pakker i VPP i følgende rekkefølge: først analyseres pakkehodene i parsing-noden, og deretter sendes de til noden, som videresender pakkene i henhold til rutingtabeller.

Litt hardcore. Forfatterne av VPP tolererer ikke kompromisser i bruken av prosessorcacher, så typisk kode for å behandle en vektor av pakker inneholder manuell vektorisering: det er en prosesseringssløyfe der en situasjon som "vi har fire pakker i køen" behandles, så det samme for to, så - for en. Instruksjoner for forhåndshenting brukes ofte til å laste data inn i cacher for å få raskere tilgang til dem i påfølgende iterasjoner.

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);
}

Så, Healthchecks snakker over IPv6 til VPP, som gjør dem til IPv4. Dette gjøres av en node i grafen, som vi kaller algoritmisk NAT. For omvendt trafikk (og konvertering fra IPv6 til IPv4) er det den samme algoritmiske NAT-noden.

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Direkte trafikk fra balanseringsklientene går gjennom grafnodene, som selv utfører balanseringen. 

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Den første noden er klissete økter. Den lagrer hasjen til 5-tuppel for etablerte økter. 5-tuppel inkluderer adressen og porten til klienten som informasjonen overføres fra, adressen og portene til ressursene som er tilgjengelige for å motta trafikk, samt nettverksprotokollen. 

5-tuppel-hashen hjelper oss med å utføre mindre beregninger i den påfølgende konsistente hashing-noden, samt bedre å håndtere ressurslisteendringer bak balanseringsenheten. Når en pakke som det ikke er noen økt for ankommer balanseringsenheten, sendes den til den konsekvente hashingnoden. Det er her balansering skjer ved å bruke konsekvent hashing: vi velger en ressurs fra listen over tilgjengelige "live" ressurser. Deretter sendes pakkene til NAT-noden, som faktisk erstatter destinasjonsadressen og beregner kontrollsummene på nytt. Som du kan se, følger vi reglene til VPP - liker å like, og grupperer lignende beregninger for å øke effektiviteten til prosessorcacher.

Konsekvent hashing

Hvorfor valgte vi det og hva er det til og med? La oss først vurdere den forrige oppgaven - å velge en ressurs fra listen. 

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Med inkonsekvent hashing beregnes hashen til den innkommende pakken, og en ressurs velges fra listen ved å dele denne hashen med antall ressurser. Så lenge listen forblir uendret, fungerer denne ordningen bra: vi sender alltid pakker med samme 5-tuppel til samme instans. Hvis for eksempel en ressurs sluttet å svare på helsesjekker, vil valget endres for en betydelig del av hashen. Klientens TCP-forbindelser vil bli brutt: en pakke som tidligere nådde instans A kan begynne å nå instans B, som ikke er kjent med økten for denne pakken.

Konsekvent hashing løser det beskrevne problemet. Den enkleste måten å forklare dette konseptet på er denne: Tenk deg at du har en ring som du distribuerer ressurser til med hash (for eksempel etter IP:port). Å velge en ressurs dreier hjulet i en vinkel, som bestemmes av hashen til pakken.

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Dette minimerer trafikkomfordeling når ressurssammensetningen endres. Sletting av en ressurs vil bare påvirke den delen av den konsistente hashingringen der ressursen var lokalisert. Å legge til en ressurs endrer også distribusjonen, men vi har en klebrig sesjonsnode, som lar oss ikke bytte allerede etablerte økter til nye ressurser.

Vi så på hva som skjer med direkte trafikk mellom balansereren og ressursene. La oss nå se på returtrafikken. Den følger samme mønster som sjekktrafikk - gjennom algoritmisk NAT, det vil si gjennom revers NAT 44 for klienttrafikk og gjennom NAT 46 for helsesjekktrafikk. Vi følger vår egen ordning: vi forener helsesjekktrafikk og ekte brukertrafikk.

Loadbalancer-node og sammensatte komponenter

Sammensetningen av balansere og ressurser i VPP rapporteres av den lokale tjenesten - loadbalancer-node. Den abonnerer på strømmen av hendelser fra loadbalancer-controller og er i stand til å plotte forskjellen mellom gjeldende VPP-tilstand og måltilstanden mottatt fra kontrolleren. Vi får et lukket system: hendelser fra API-en kommer til balanseringskontrolleren, som tildeler oppgaver til helsesjekkkontrolleren for å sjekke "livligheten" til ressursene. Det tildeler i sin tur oppgaver til helsesjekk-noden og samler resultatene, hvoretter den sender dem tilbake til balanseringskontrolleren. Loadbalancer-node abonnerer på hendelser fra kontrolleren og endrer tilstanden til VPP. I et slikt system vet hver tjeneste bare hva som er nødvendig om nabotjenester. Antall forbindelser er begrenset og vi har muligheten til å operere og skalere ulike segmenter uavhengig.

Arkitektur av en nettverksbelastningsbalanser i Yandex.Cloud

Hvilke problemer ble unngått?

Alle våre tjenester i kontrollplanet er skrevet i Go og har gode skalerings- og pålitelighetsegenskaper. Go har mange åpen kildekode-biblioteker for å bygge distribuerte systemer. Vi bruker GRPC aktivt, alle komponentene inneholder en åpen kildekodeimplementering av tjenesteoppdagelse - tjenestene våre overvåker hverandres ytelse, kan endre sammensetningen deres dynamisk, og vi koblet dette med GRPC-balansering. For beregninger bruker vi også en åpen kildekode-løsning. I dataplanet fikk vi anstendig ytelse og en stor ressursreserve: det viste seg å være svært vanskelig å sette sammen et stativ som vi kunne stole på ytelsen til en VPP, i stedet for et jernnettverkskort.

Problemer og løsninger

Hva fungerte ikke så bra? Go har automatisk minnebehandling, men minnelekkasjer skjer fortsatt. Den enkleste måten å håndtere dem på er å kjøre goroutiner og huske å avslutte dem. Takeaway: Se minneforbruket til Go-programmene dine. Ofte er en god indikator antall goroutiner. Det er et pluss i denne historien: i Go er det enkelt å få kjøretidsdata - minneforbruk, antall løpende goroutiner og mange andre parametere.

Dessuten er kanskje ikke Go det beste valget for funksjonstester. De er ganske detaljerte, og standardtilnærmingen med å "kjøre alt i CI i en batch" er ikke særlig egnet for dem. Faktum er at funksjonstester er mer ressurskrevende og forårsaker reelle tidsavbrudd. På grunn av dette kan tester mislykkes fordi CPU-en er opptatt med enhetstester. Konklusjon: Hvis mulig, utfør "tunge" tester separat fra enhetstester. 

Mikroservice-hendelsesarkitektur er mer kompleks enn en monolitt: å samle logger på dusinvis av forskjellige maskiner er ikke særlig praktisk. Konklusjon: hvis du lager mikrotjenester, tenk umiddelbart på sporing.

Planene våre

Vi vil lansere en intern balansering, en IPv6-balansering, legge til støtte for Kubernetes-skript, fortsette å skjære tjenestene våre (foreløpig er det bare healthcheck-node og healthcheck-ctrl som er splittet), legge til nye helsesjekker, og også implementere smart aggregering av sjekker. Vi vurderer muligheten for å gjøre tjenestene våre enda mer uavhengige – slik at de ikke kommuniserer direkte med hverandre, men ved hjelp av en meldingskø. En SQS-kompatibel tjeneste har nylig dukket opp i skyen Yandex meldingskø.

Nylig fant den offentlige utgivelsen av Yandex Load Balancer sted. Utforske dokumentasjon til tjenesten, administrer balansere på en måte som er praktisk for deg og øk feiltoleransen til prosjektene dine!

Kilde: www.habr.com

Legg til en kommentar