Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud
Hej, jag heter Sergey Elantsev, jag utvecklas nätverkslastbalanserare i Yandex.Cloud. Tidigare har jag lett utvecklingen av L7-balanseraren för Yandex-portalen – kollegor skämtar om att oavsett vad jag gör så visar det sig vara en balanserare. Jag kommer att berätta för Habr-läsare hur man hanterar belastningen i en molnplattform, vad vi ser som det perfekta verktyget för att uppnå detta mål och hur vi går mot att bygga detta verktyg.

Låt oss först introducera några termer:

  • VIP (Virtual IP) - balanserande IP-adress
  • Server, backend, instans - en virtuell maskin som kör en applikation
  • RIP (Real IP) - serverns IP-adress
  • Healthcheck - kontrollera serverns beredskap
  • Availability Zone, AZ - isolerad infrastruktur i ett datacenter
  • Region - en förening av olika AZ

Lastbalanserare löser tre huvuduppgifter: de utför själva balanseringen, förbättrar tjänstens feltolerans och förenklar dess skalning. Feltolerans säkerställs genom automatisk trafikhantering: balanseraren övervakar applikationens tillstånd och utesluter instanser från balansering som inte klarar liveness-kontrollen. Skalning säkerställs genom att fördela belastningen jämnt över instanser, samt uppdatera listan över instanser i farten. Om balanseringen inte är tillräckligt enhetlig kommer några av instanserna att få en belastning som överstiger deras kapacitetsgräns och tjänsten blir mindre tillförlitlig.

En lastbalanserare klassificeras ofta av protokollskiktet från OSI-modellen som den körs på. Cloud Balancer fungerar på TCP-nivå, vilket motsvarar det fjärde lagret, L4.

Låt oss gå vidare till en översikt över molnbalanseringsarkitekturen. Vi kommer successivt att öka detaljnivån. Vi delar in balanseringskomponenterna i tre klasser. Konfigplansklassen ansvarar för användarinteraktion och lagrar systemets måltillstånd. Kontrollplanet lagrar systemets aktuella tillstånd och hanterar system från dataplansklassen, som är direkt ansvariga för att leverera trafik från klienter till dina instanser.

Dataplan

Trafiken hamnar på dyra enheter som kallas gränsroutrar. För att öka feltoleransen fungerar flera sådana enheter samtidigt i ett datacenter. Därefter går trafiken till balanserare, som tillkännager anycast IP-adresser till alla AZ via BGP för klienter. 

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Trafik sänds över ECMP - detta är en routingstrategi enligt vilken det kan finnas flera lika bra rutter till målet (i vårt fall kommer målet att vara destinationens IP-adress) och paket kan skickas längs vilken som helst av dem. Vi stödjer även arbete i flera tillgänglighetszoner enligt följande schema: vi annonserar en adress i varje zon, trafiken går till den närmaste och går inte över dess gränser. Senare i inlägget ska vi titta närmare på vad som händer med trafiken.

Konfigurera plan

 
Nyckelkomponenten i konfigurationsplanet är API:t, genom vilket grundläggande operationer med balanserare utförs: skapa, ta bort, ändra sammansättningen av instanser, erhålla hälsokontrollresultat, etc. Å ena sidan är detta ett REST API, och å ena sidan I övrigt använder vi i molnet väldigt ofta ramverket gRPC, så vi "översätter" REST till gRPC och använder sedan bara gRPC. Varje begäran leder till skapandet av en serie asynkrona idempotenta uppgifter som utförs på en gemensam pool av Yandex.Cloud-arbetare. Uppgifter är skrivna på ett sådant sätt att de kan avbrytas när som helst och sedan startas om. Detta säkerställer skalbarhet, repeterbarhet och loggning av operationer.

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Som ett resultat kommer uppgiften från API:et att göra en förfrågan till balanseringstjänstkontrollanten, som är skriven i Go. Det kan lägga till och ta bort balanserare, ändra sammansättningen av backends och inställningar. 

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Tjänsten lagrar sitt tillstånd i Yandex Database, en distribuerad hanterad databas som du snart kommer att kunna använda. I Yandex.Cloud, som vi redan sa, hundmatskonceptet gäller: om vi själva använder våra tjänster så kommer våra kunder också gärna använda dem. Yandex Database är ett exempel på implementeringen av ett sådant koncept. Vi lagrar all vår data i YDB, och vi behöver inte tänka på att underhålla och skala databasen: dessa problem är lösta åt oss, vi använder databasen som en tjänst.

Låt oss återgå till balanseringskontrollen. Dess uppgift är att spara information om balanseraren och skicka en uppgift för att kontrollera den virtuella maskinens beredskap till hälsokontrollkontrollen.

Healthcheck controller

Den tar emot förfrågningar om att ändra kontrollregler, sparar dem i YDB, fördelar uppgifter mellan healtchecknoder och aggregerar resultaten, som sedan sparas i databasen och skickas till lastbalanseringskontrollern. Den skickar i sin tur en begäran om att ändra sammansättningen av klustret i dataplanet till loadbalancer-noden, vilket jag kommer att diskutera nedan.

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Låt oss prata mer om hälsokontroller. De kan delas in i flera klasser. Revisioner har olika framgångskriterier. TCP-kontroller måste lyckas upprätta en anslutning inom en bestämd tid. HTTP-kontroller kräver både en lyckad anslutning och ett svar med en 200-statuskod.

Dessutom skiljer sig kontrollerna åt i åtgärdsklassen - de är aktiva och passiva. Passiva kontroller övervakar helt enkelt vad som händer med trafiken utan att vidta några särskilda åtgärder. Detta fungerar inte särskilt bra på L4 eftersom det beror på logiken i protokollen på högre nivå: på L4 finns det ingen information om hur lång tid operationen tog eller om anslutningen var bra eller dålig. Aktiva kontroller kräver att balansören skickar förfrågningar till varje serverinstans.

De flesta belastningsutjämnare utför livhetskontroller själva. På Cloud bestämde vi oss för att separera dessa delar av systemet för att öka skalbarheten. Detta tillvägagångssätt kommer att tillåta oss att öka antalet balanserare samtidigt som antalet hälsokontrollförfrågningar till tjänsten bibehålls. Kontroller utförs av separata hälsokontrollnoder, över vilka kontrollmål delas och replikeras. Du kan inte utföra kontroller från en värd, eftersom den kan misslyckas. Då får vi inte status för de instanser han kontrollerat. Vi utför kontroller av någon av instanserna från minst tre hälsokontrollnoder. Vi delar syftet med kontroller mellan noder med hjälp av konsekventa hashalgoritmer.

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Att separera balansering och hälsokontroll kan leda till problem. Om hälsokontrollnoden gör förfrågningar till instansen och kringgår balanseraren (som för närvarande inte betjänar trafik), så uppstår en konstig situation: resursen verkar vara vid liv, men trafiken kommer inte att nå den. Vi löser det här problemet på det här sättet: vi är garanterade att initiera hälsokontrolltrafik genom balanserare. Med andra ord, schemat för att flytta paket med trafik från klienter och från hälsokontroller skiljer sig minimalt: i båda fallen kommer paketen att nå balanserarna, som kommer att leverera dem till målresurserna.

Skillnaden är att klienter gör förfrågningar till VIP, medan hälsokontroller gör förfrågningar till varje enskild RIP. Ett intressant problem uppstår här: vi ger våra användare möjlighet att skapa resurser i gråa IP-nätverk. Låt oss föreställa oss att det finns två olika molnägare som har gömt sina tjänster bakom balanserare. Var och en av dem har resurser i undernätet 10.0.0.1/24, med samma adresser. Du måste kunna särskilja dem på något sätt, och här måste du dyka in i strukturen för det virtuella Yandex.Cloud-nätverket. Det är bättre att ta reda på mer detaljer i video från about:cloud-evenemanget, det är viktigt för oss nu att nätverket är flerskiktigt och har tunnlar som kan särskiljas med subnät-id.

Healthcheck-noder kontaktar balanserare med hjälp av så kallade kvasi-IPv6-adresser. En kvasi-adress är en IPv6-adress med en IPv4-adress och användarens subnät-ID inbäddat i den. Trafiken når balanseringsenheten, som extraherar IPv4-resursadressen från den, ersätter IPv6 med IPv4 och skickar paketet till användarens nätverk.

Den omvända trafiken går på samma sätt: balansören ser att destinationen är ett grått nätverk från hälsokontroller och konverterar IPv4 till IPv6.

VPP - hjärtat i dataplanet

Balanseraren är implementerad med hjälp av Vector Packet Processing (VPP) teknologi, ett ramverk från Cisco för batchbehandling av nätverkstrafik. I vårt fall fungerar ramverket ovanpå användarutrymmets nätverksenhetshanteringsbibliotek - Data Plane Development Kit (DPDK). Detta säkerställer hög paketbearbetningsprestanda: mycket färre avbrott inträffar i kärnan, och det finns inga kontextväxlar mellan kärnutrymme och användarutrymme. 

VPP går ännu längre och pressar ut ännu mer prestanda ur systemet genom att kombinera paket i batcher. Prestandavinsterna kommer från den aggressiva användningen av cacher på moderna processorer. Både datacacher används (paket bearbetas i "vektorer", data ligger nära varandra) och instruktionscacher: i VPP följer paketbehandling en graf, vars noder innehåller funktioner som utför samma uppgift.

Till exempel sker bearbetningen av IP-paket i VPP i följande ordning: först tolkas pakethuvuden i tolknoden, och sedan skickas de till noden, som vidarebefordrar paketen enligt routingtabeller.

Lite hardcore. Författarna till VPP tolererar inte kompromisser i användningen av processorcacher, så typisk kod för att bearbeta en vektor av paket innehåller manuell vektorisering: det finns en bearbetningsslinga där en situation som "vi har fyra paket i kön" bearbetas, sedan samma för två, sedan - för en. Förhämtningsinstruktioner används ofta för att ladda data till cachar för att påskynda åtkomsten till dem i efterföljande 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 pratar över IPv6 till VPP, vilket gör dem till IPv4. Detta görs av en nod i grafen, som vi kallar algoritmisk NAT. För omvänd trafik (och konvertering från IPv6 till IPv4) finns samma algoritmiska NAT-nod.

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Direkt trafik från balanseringsklienterna går genom grafnoderna, som själva utför balanseringen. 

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Den första noden är klibbiga sessioner. Den lagrar hash av 5-tupel för etablerade sessioner. 5-tupel inkluderar adressen och porten för klienten från vilken information överförs, adressen och portarna för resurser som är tillgängliga för att ta emot trafik, såväl som nätverksprotokollet. 

5-tuppel-hash hjälper oss att utföra mindre beräkningar i den efterföljande konsekventa hash-noden, samt bättre hantera resursliständringar bakom balanseraren. När ett paket för vilket det inte finns någon session anländer till balanseringsenheten, skickas det till den konsekventa hashnoden. Det är här balansering sker med konsekvent hash: vi väljer en resurs från listan över tillgängliga "live"-resurser. Därefter skickas paketen till NAT-noden, som faktiskt ersätter destinationsadressen och räknar om kontrollsummorna. Som du kan se följer vi reglerna för VPP - gillar att gilla, och grupperar liknande beräkningar för att öka effektiviteten hos processorcacher.

Konsekvent hashning

Varför valde vi det och vad är det ens? Låt oss först överväga föregående uppgift - att välja en resurs från listan. 

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Med inkonsekvent hash beräknas hashen för det inkommande paketet, och en resurs väljs från listan genom att resten dividera denna hash med antalet resurser. Så länge listan förblir oförändrad fungerar detta schema bra: vi skickar alltid paket med samma 5-tuppel till samma instans. Om till exempel någon resurs slutade svara på hälsokontroller, kommer valet att ändras för en betydande del av hasharna. Klientens TCP-anslutningar kommer att brytas: ett paket som tidigare nått instans A kan börja nå instans B, som inte är bekant med sessionen för detta paket.

Konsekvent hashning löser det beskrivna problemet. Det enklaste sättet att förklara detta koncept är detta: föreställ dig att du har en ring som du distribuerar resurser till med hash (till exempel via IP:port). Att välja en resurs är att vrida hjulet en vinkel, som bestäms av paketets hash.

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Detta minimerar trafikomfördelningen när sammansättningen av resurser förändras. Att ta bort en resurs påverkar bara den del av den konsekventa hashringen där resursen fanns. Att lägga till en resurs ändrar också distributionen, men vi har en klibbig sessionsnod, som gör att vi inte kan byta redan etablerade sessioner till nya resurser.

Vi tittade på vad som händer med direkttrafiken mellan balansören och resurserna. Låt oss nu titta på returtrafiken. Den följer samma mönster som kontrolltrafik - genom algoritmisk NAT, det vill säga genom omvänd NAT 44 för klienttrafik och genom NAT 46 för hälsokontrolltrafik. Vi följer vårt eget schema: vi förenar hälsokontrolltrafik och verklig användartrafik.

Loadbalancer-nod och monterade komponenter

Sammansättningen av balanserare och resurser i VPP rapporteras av den lokala tjänsten - loadbalancer-nod. Den abonnerar på strömmen av händelser från loadbalancer-controller och kan plotta skillnaden mellan det aktuella VPP-tillståndet och måltillståndet som tas emot från regulatorn. Vi får ett slutet system: händelser från API:t kommer till balanseringskontrollern, som tilldelar uppgifter till hälsokontrollkontrollanten för att kontrollera resursernas "livlighet". Det i sin tur tilldelar uppgifter till healthcheck-noden och aggregerar resultaten, varefter den skickar tillbaka dem till balanseringskontrollanten. Loadbalancer-nod prenumererar på händelser från styrenheten och ändrar tillståndet för VPP. I ett sådant system vet varje tjänst endast vad som är nödvändigt om närliggande tjänster. Antalet anslutningar är begränsat och vi har möjlighet att driva och skala olika segment oberoende av varandra.

Arkitektur för en nätverkslastbalanserare i Yandex.Cloud

Vilka problem undveks?

Alla våra tjänster i kontrollplanet är skrivna i Go och har goda skalnings- och tillförlitlighetsegenskaper. Go har många bibliotek med öppen källkod för att bygga distribuerade system. Vi använder aktivt GRPC, alla komponenter innehåller en öppen källkodsimplementering av tjänsteupptäckt - våra tjänster övervakar varandras prestanda, kan ändra deras sammansättning dynamiskt, och vi kopplade detta till GRPC-balansering. För mätvärden använder vi även en öppen källkodslösning. I dataplanet fick vi hyfsad prestanda och en stor resursreserv: det visade sig vara mycket svårt att montera ett stativ som vi kunde lita på prestanda hos en VPP, snarare än ett järnnätverkskort.

Problem och lösningar

Vad fungerade inte så bra? Go har automatisk minneshantering, men minnesläckor inträffar fortfarande. Det enklaste sättet att hantera dem är att köra goroutiner och komma ihåg att avsluta dem. Takeaway: Se dina Go-programs minnesförbrukning. Ofta är en bra indikator antalet goroutiner. Det finns ett plus i den här historien: i Go är det lätt att få körtidsdata - minnesförbrukning, antalet löpande goroutiner och många andra parametrar.

Go kanske inte är det bästa valet för funktionstester. De är ganska mångsidiga, och standardmetoden att "köra allt i CI i en batch" är inte särskilt lämplig för dem. Faktum är att funktionstester är mer resurskrävande och orsakar verkliga timeouts. På grund av detta kan tester misslyckas eftersom CPU:n är upptagen med enhetstester. Slutsats: Om möjligt, utför "tunga" tester separat från enhetstester. 

Microservice-händelsearkitektur är mer komplex än en monolit: att samla loggar på dussintals olika maskiner är inte särskilt bekvämt. Slutsats: om du gör mikrotjänster, tänk omedelbart på spårning.

Våra planer

Vi kommer att lansera en intern balanserare, en IPv6-balanserare, lägga till stöd för Kubernetes-skript, fortsätta att sönderdela våra tjänster (för närvarande är bara healthcheck-nod och healthcheck-ctrl delade), lägga till nya hälsokontroller och även implementera smart aggregering av kontroller. Vi överväger möjligheten att göra våra tjänster ännu mer oberoende – så att de inte kommunicerar direkt med varandra, utan med hjälp av en meddelandekö. En SQS-kompatibel tjänst har nyligen dykt upp i molnet Yandex meddelandekö.

Nyligen ägde den offentliga utgivningen av Yandex Load Balancer rum. Utforska dokumentation till tjänsten, hantera balanserare på ett sätt som är bekvämt för dig och öka feltoleransen för dina projekt!

Källa: will.com

Lägg en kommentar