Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Netværksbelastningsbalancerarkitektur i Yandex.Cloud
Hej, jeg er Sergey Elantsev, jeg udvikler mig netværk load balancer i Yandex.Cloud. Tidligere har jeg ledet udviklingen af ​​L7 balanceren til Yandex-portalen - kolleger joker med, at uanset hvad jeg gør, så viser det sig at være en balancer. Jeg vil fortælle Habr-læsere, hvordan man styrer belastningen i en cloud-platform, hvad vi ser som det ideelle værktøj til at nå dette mål, og hvordan vi bevæger os mod at bygge dette værktøj.

Lad os først introducere nogle udtryk:

  • VIP (Virtuel IP) - balancer IP-adresse
  • Server, backend, instans - en virtuel maskine, der kører en applikation
  • RIP (Real IP) - server IP-adresse
  • Healthcheck - kontrol af serverberedskab
  • Availability Zone, AZ - isoleret infrastruktur i et datacenter
  • Region - en forening af forskellige AZ'er

Load balancers løser tre hovedopgaver: de udfører selve balanceringen, forbedrer tjenestens fejltolerance og forenkler dens skalering. Fejltolerance er sikret gennem automatisk trafikstyring: balanceren overvåger applikationens tilstand og udelukker instanser fra balancering, der ikke består liveness-kontrollen. Skalering sikres ved at fordele belastningen jævnt på tværs af instanser, samt ved at opdatere listen over instanser i farten. Hvis afbalanceringen ikke er ensartet nok, vil nogle af instanserne få en belastning, der overstiger deres kapacitetsgrænse, og tjenesten bliver mindre pålidelig.

En load balancer er ofte klassificeret efter protokollaget fra OSI-modellen, som den kører på. Cloud Balancer fungerer på TCP-niveau, som svarer til det fjerde lag, L4.

Lad os gå videre til en oversigt over Cloud balancer-arkitekturen. Vi vil gradvist øge detaljegraden. Vi opdeler balancerkomponenterne i tre klasser. Konfigurationsplanklassen er ansvarlig for brugerinteraktion og gemmer systemets måltilstand. Kontrolplanet gemmer systemets aktuelle tilstand og administrerer systemer fra dataplanklassen, som er direkte ansvarlige for at levere trafik fra klienter til dine instanser.

Dataplan

Trafikken ender på dyre enheder kaldet grænseroutere. For at øge fejltolerancen fungerer flere sådanne enheder samtidigt i ét datacenter. Dernæst går trafikken til balancere, som annoncerer anycast IP-adresser til alle AZ'er via BGP for klienter. 

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Trafik transmitteres over ECMP - dette er en routingstrategi, ifølge hvilken der kan være flere lige gode ruter til målet (i vores tilfælde vil målet være destinations-IP-adressen), og pakker kan sendes langs enhver af dem. Vi støtter også arbejde i flere tilgængelighedszoner i henhold til følgende skema: vi annoncerer en adresse i hver zone, trafikken går til den nærmeste og går ikke ud over grænserne. Senere i indlægget vil vi se nærmere på, hvad der sker med trafikken.

Konfigurer fly

 
Nøglekomponenten i konfigurationsplanet er API'en, hvorigennem grundlæggende operationer med balancere udføres: oprettelse, sletning, ændring af sammensætningen af ​​instanser, opnåelse af sundhedstjekresultater osv. På den ene side er dette en REST API, og på den ene side andet, vi i skyen bruger meget ofte frameworket gRPC, så vi "oversætter" REST til gRPC og bruger så kun gRPC. Enhver anmodning fører til oprettelsen af ​​en række asynkrone idempotente opgaver, der udføres på en fælles pulje af Yandex.Cloud-arbejdere. Opgaver er skrevet på en sådan måde, at de til enhver tid kan suspenderes og derefter genstartes. Dette sikrer skalerbarhed, repeterbarhed og logning af operationer.

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Som følge heraf vil opgaven fra API'et lave en anmodning til balancer-servicecontrolleren, som er skrevet i Go. Det kan tilføje og fjerne balancere, ændre sammensætningen af ​​backends og indstillinger. 

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Tjenesten gemmer sin tilstand i Yandex Database, en distribueret administreret database, som du snart vil kunne bruge. I Yandex.Cloud, som vi allerede fortalt, er hundefoderkonceptet gældende: Hvis vi selv bruger vores tjenester, så vil vores kunder også gerne bruge dem. Yandex Database er et eksempel på implementeringen af ​​et sådant koncept. Vi gemmer alle vores data i YDB, og vi behøver ikke tænke på at vedligeholde og skalere databasen: disse problemer er løst for os, vi bruger databasen som en service.

Lad os vende tilbage til balancer-controlleren. Dens opgave er at gemme oplysninger om balanceren og sende en opgave for at kontrollere den virtuelle maskines parathed til healthcheck-controlleren.

Healthcheck controller

Den modtager anmodninger om at ændre kontrolregler, gemmer dem i YDB, fordeler opgaver blandt healtcheck-noder og aggregerer resultaterne, som derefter gemmes i databasen og sendes til loadbalancer-controlleren. Det sender til gengæld en anmodning om at ændre sammensætningen af ​​klyngen i dataplanet til loadbalancer-noden, som jeg vil diskutere nedenfor.

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Lad os tale mere om sundhedstjek. De kan opdeles i flere klasser. Audits har forskellige succeskriterier. TCP-tjek skal med succes etablere en forbindelse inden for et fast tidsrum. HTTP-tjek kræver både en vellykket forbindelse og et svar med en 200-statuskode.

Checks er også forskellige i aktionsklassen - de er aktive og passive. Passive checks overvåger simpelthen, hvad der sker med trafikken uden at foretage nogen særlig handling. Dette fungerer ikke særlig godt på L4, fordi det afhænger af logikken i protokollerne på højere niveau: på L4 er der ingen information om, hvor lang tid operationen tog, eller om forbindelsesfuldførelsen var god eller dårlig. Aktive kontroller kræver, at balanceren sender anmodninger til hver serverinstans.

De fleste load balancere udfører selv liveness-tjek. Hos Cloud besluttede vi at adskille disse dele af systemet for at øge skalerbarheden. Denne tilgang vil give os mulighed for at øge antallet af balancerer og samtidig bevare antallet af sundhedstjek-anmodninger til tjenesten. Tjek udføres af separate sundhedstjek-noder, på tværs af hvilke kontrolmål er sønderdelt og replikeret. Du kan ikke udføre kontrol fra én vært, da den kan mislykkes. Så får vi ikke status for de tilfælde, han tjekkede. Vi udfører tjek på enhver af forekomsterne fra mindst tre sundhedstjek-noder. Vi sønderdeler formålene med kontroller mellem noder ved hjælp af konsekvente hashing-algoritmer.

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Adskillelse af balancering og sundhedstjek kan føre til problemer. Hvis sundhedstjek-noden sender anmodninger til instansen og omgår balanceren (som i øjeblikket ikke betjener trafik), så opstår der en mærkelig situation: ressourcen ser ud til at være i live, men trafikken vil ikke nå den. Vi løser dette problem på denne måde: Vi er garanteret at starte sundhedstjek trafik gennem balancere. Med andre ord adskiller ordningen for flytning af pakker med trafik fra klienter og fra sundhedstjek sig minimalt: i begge tilfælde vil pakkerne nå balancerne, som vil levere dem til målressourcerne.

Forskellen er, at klienter sender anmodninger til VIP, mens sundhedstjek sender anmodninger til hver enkelt RIP. Et interessant problem opstår her: Vi giver vores brugere mulighed for at skabe ressourcer i grå IP-netværk. Lad os forestille os, at der er to forskellige cloud-ejere, der har gemt deres tjenester bag balancere. Hver af dem har ressourcer i 10.0.0.1/24-undernettet med de samme adresser. Du skal på en eller anden måde kunne skelne dem, og her skal du dykke ned i strukturen af ​​det virtuelle Yandex.Cloud-netværk. Det er bedre at finde ud af flere detaljer i video fra about:cloud-begivenheden, er det vigtigt for os nu, at netværket er flerlags og har tunneler, der kan skelnes efter subnet-id.

Healthcheck-noder kontakter balancere ved hjælp af såkaldte kvasi-IPv6-adresser. En kvasi-adresse er en IPv6-adresse med en IPv4-adresse og brugerundernet-id indlejret i den. Trafikken når balanceren, som uddrager IPv4-ressourceadressen fra den, erstatter IPv6 med IPv4 og sender pakken til brugerens netværk.

Den omvendte trafik går på samme måde: balanceren ser, at destinationen er et gråt netværk fra healthcheckers, og konverterer IPv4 til IPv6.

VPP - hjertet af dataplanet

Balanceren er implementeret ved hjælp af Vector Packet Processing (VPP) teknologi, en ramme fra Cisco til batchbehandling af netværkstrafik. I vores tilfælde fungerer rammeværket oven på bruger-space netværksenhedsstyringsbiblioteket - Data Plane Development Kit (DPDK). Dette sikrer høj pakkebehandlingsydelse: meget færre afbrydelser forekommer i kernen, og der er ingen kontekstskift mellem kernerum og brugerrum. 

VPP går endnu længere og presser endnu mere ydeevne ud af systemet ved at kombinere pakker i batches. Ydeevnegevinsten kommer fra den aggressive brug af caches på moderne processorer. Der bruges både datacaches (pakker behandles i "vektorer", dataene er tæt på hinanden) og instruktionscaches: i VPP følger pakkebehandling en graf, hvis noder indeholder funktioner, der udfører den samme opgave.

For eksempel foregår behandlingen af ​​IP-pakker i VPP i følgende rækkefølge: først parses pakkehovederne i parsing-noden, og derefter sendes de til noden, som videresender pakkerne videre i henhold til routing-tabeller.

Lidt hardcore. Forfatterne af VPP tolererer ikke kompromiser i brugen af ​​processorcaches, så typisk kode til behandling af en vektor af pakker indeholder manuel vektorisering: der er en behandlingsløkke, hvor en situation som "vi har fire pakker i køen" behandles, så det samme for to, så - for én. Prefetch-instruktioner bruges ofte til at indlæse data i caches for at fremskynde adgangen til dem i efterfølgende iterationer.

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 taler over IPv6 til VPP, som gør dem til IPv4. Dette gøres af en node i grafen, som vi kalder algoritmisk NAT. Til omvendt trafik (og konvertering fra IPv6 til IPv4) er der den samme algoritmiske NAT-node.

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Direkte trafik fra balancer-klienterne går gennem grafknuderne, som selv udfører balanceringen. 

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Den første node er klæbrige sessioner. Den gemmer hashen af 5-tuple til etablerede sessioner. 5-tuple inkluderer adressen og porten på den klient, hvorfra informationen transmitteres, adressen og portene på ressourcer, der er tilgængelige for at modtage trafik, samt netværksprotokollen. 

5-tuple hash hjælper os med at udføre mindre beregninger i den efterfølgende konsistente hashing node, samt bedre håndtere ressourcelisteændringer bag balanceren. Når en pakke, som der ikke er nogen session for, ankommer til balanceren, sendes den til den konsekvente hashing-knude. Det er her, balancering sker ved hjælp af konsekvent hashing: vi vælger en ressource fra listen over tilgængelige "live" ressourcer. Derefter sendes pakkerne til NAT-noden, som faktisk erstatter destinationsadressen og genberegner kontrolsummerne. Som du kan se, følger vi reglerne for VPP - kan lide at lide, og grupperer lignende beregninger for at øge effektiviteten af ​​processorcaches.

Konsekvent hashing

Hvorfor valgte vi det, og hvad er det endda? Lad os først overveje den forrige opgave - at vælge en ressource fra listen. 

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Ved inkonsistent hashing beregnes hashen for den indgående pakke, og en ressource vælges fra listen ved at dele denne hash med antallet af ressourcer. Så længe listen forbliver uændret, fungerer denne ordning godt: vi sender altid pakker med den samme 5-tuple til den samme instans. Hvis for eksempel en ressource holdt op med at reagere på sundhedstjek, så vil valget ændre sig for en betydelig del af hasherne. Klientens TCP-forbindelser vil blive brudt: en pakke, der tidligere nåede instans A, kan begynde at nå instans B, som ikke er bekendt med sessionen for denne pakke.

Konsekvent hashing løser det beskrevne problem. Den nemmeste måde at forklare dette koncept på er dette: Forestil dig, at du har en ring, som du distribuerer ressourcer til ved hjælp af hash (for eksempel efter IP:port). At vælge en ressource drejer hjulet i en vinkel, som bestemmes af pakkens hash.

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Dette minimerer trafikomfordeling, når sammensætningen af ​​ressourcer ændres. Sletning af en ressource vil kun påvirke den del af den konsekvente hashingring, hvori ressourcen var placeret. Tilføjelse af en ressource ændrer også fordelingen, men vi har en klæbrig sessionsknude, som giver os mulighed for ikke at skifte allerede etablerede sessioner til nye ressourcer.

Vi så på, hvad der sker med direkte trafik mellem balanceren og ressourcerne. Lad os nu se på returtrafikken. Det følger det samme mønster som kontroltrafik - gennem algoritmisk NAT, det vil sige gennem omvendt NAT 44 for klienttrafik og gennem NAT 46 for trafik til sundhedstjek. Vi overholder vores egen ordning: vi forener sundhedstjektrafik og reel brugertrafik.

Loadbalancer-node og samlede komponenter

Sammensætningen af ​​balancerer og ressourcer i VPP rapporteres af den lokale service - loadbalancer-node. Den abonnerer på strømmen af ​​hændelser fra loadbalancer-controller og er i stand til at plotte forskellen mellem den aktuelle VPP-tilstand og måltilstanden modtaget fra controlleren. Vi får et lukket system: Hændelser fra API'et kommer til balancer-controlleren, som tildeler opgaver til sundhedstjek-controlleren for at kontrollere "liveness" af ressourcer. Det tildeler igen opgaver til healthcheck-noden og aggregerer resultaterne, hvorefter den sender dem tilbage til balancer-controlleren. Loadbalancer-node abonnerer på hændelser fra controlleren og ændrer status for VPP. I et sådant system ved hver tjeneste kun, hvad der er nødvendigt om nabotjenester. Antallet af forbindelser er begrænset, og vi har mulighed for at drive og skalere forskellige segmenter uafhængigt.

Netværksbelastningsbalancerarkitektur i Yandex.Cloud

Hvilke problemer blev undgået?

Alle vores tjenester i kontrolplanet er skrevet i Go og har gode skalerings- og pålidelighedsegenskaber. Go har mange open source-biblioteker til at bygge distribuerede systemer. Vi bruger aktivt GRPC, alle komponenter indeholder en open source implementering af service discovery - vores tjenester overvåger hinandens ydeevne, kan ændre deres sammensætning dynamisk, og vi koblede dette sammen med GRPC balancering. Til målinger bruger vi også en open source-løsning. I dataplanet fik vi en anstændig ydeevne og en stor ressourcereserve: det viste sig at være meget vanskeligt at samle et stativ, hvorpå vi kunne stole på ydeevnen af ​​en VPP i stedet for et jernnetværkskort.

Problemer og løsninger

Hvad fungerede ikke så godt? Go har automatisk hukommelsesstyring, men der sker stadig hukommelseslækager. Den nemmeste måde at håndtere dem på er at køre goroutiner og huske at afslutte dem. Takeaway: Se dine Go-programmers hukommelsesforbrug. Ofte er en god indikator antallet af goroutiner. Der er et plus i denne historie: i Go er det nemt at få runtime-data - hukommelsesforbrug, antallet af kørende goroutiner og mange andre parametre.

Desuden er Go muligvis ikke det bedste valg til funktionelle tests. De er ret verbose, og standardtilgangen med at "køre alt i CI i en batch" er ikke særlig velegnet til dem. Faktum er, at funktionelle test er mere ressourcekrævende og forårsager reelle timeouts. På grund af dette kan test mislykkes, fordi CPU'en er optaget med enhedstests. Konklusion: Hvis det er muligt, udfør "tunge" tests adskilt fra enhedstests. 

Microservice hændelsesarkitektur er mere kompleks end en monolit: at indsamle logfiler på snesevis af forskellige maskiner er ikke særlig praktisk. Konklusion: hvis du laver mikrotjenester, så tænk straks på sporing.

Vores planer

Vi vil lancere en intern balancer, en IPv6 balancer, tilføje understøttelse af Kubernetes-scripts, fortsætte med at sønderdele vores tjenester (i øjeblikket er kun healthcheck-node og healthcheck-ctrl sharded), tilføje nye sundhedstjek og også implementere smart aggregering af kontroller. Vi overvejer muligheden for at gøre vores tjenester endnu mere uafhængige – så de ikke kommunikerer direkte med hinanden, men ved hjælp af en beskedkø. En SQS-kompatibel tjeneste er for nylig dukket op i skyen Yandex meddelelseskø.

For nylig fandt den offentlige udgivelse af Yandex Load Balancer sted. Udforske dokumentation til tjenesten, administrer balancere på en måde, der er praktisk for dig, og øg dine projekters fejltolerance!

Kilde: www.habr.com

Tilføj en kommentar