Hej, jag heter Sergey Elantsev, jag utvecklas
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.
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.
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.
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
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.
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.
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
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.
Direkt trafik från balanseringsklienterna går genom grafnoderna, som själva utför balanseringen.
Den första noden är klibbiga sessioner. Den lagrar hash av
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.
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.
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.
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
Nyligen ägde den offentliga utgivningen av Yandex Load Balancer rum. Utforska
Källa: will.com