Architectuur van een netwerkload balancer in Yandex.Cloud

Architectuur van een netwerkload balancer in Yandex.Cloud
Hallo, ik ben Sergey Elantsev, ik ontwikkel netwerkloadbalancer in Yandex.Cloud. Eerder leidde ik de ontwikkeling van de L7-balancer voor het Yandex-portaal - collega's maken grapjes dat wat ik ook doe, het een balancer blijkt te zijn. Ik zal Habr-lezers vertellen hoe ze de belasting in een cloudplatform kunnen beheren, wat wij zien als de ideale tool om dit doel te bereiken, en hoe we op weg zijn om deze tool te bouwen.

Laten we eerst enkele termen introduceren:

  • VIP (virtueel IP) - IP-adres van de balancer
  • Server, backend, instance - een virtuele machine waarop een applicatie draait
  • RIP (Real IP) - IP-adres van de server
  • Healthcheck - controleren of de server gereed is
  • Availability Zone, AZ - geïsoleerde infrastructuur in een datacenter
  • Regio - een unie van verschillende AZ's

Load balancers lossen drie hoofdtaken op: ze voeren de balancering zelf uit, verbeteren de fouttolerantie van de service en vereenvoudigen de schaalvergroting ervan. Fouttolerantie wordt gewaarborgd door automatisch verkeersbeheer: de balancer bewaakt de status van de applicatie en sluit instanties uit van balancering die de liveness check niet doorstaan. Schalen wordt verzekerd door de belasting gelijkmatig over instances te verdelen, en door de lijst met instances direct bij te werken. Als de balancering niet uniform genoeg is, zullen sommige instances een belasting ontvangen die hun capaciteitslimiet overschrijdt, en zal de service minder betrouwbaar worden.

Een load balancer wordt vaak geclassificeerd op basis van de protocollaag van het OSI-model waarop deze draait. De Cloud Balancer werkt op TCP-niveau, wat overeenkomt met de vierde laag, L4.

Laten we verder gaan met een overzicht van de Cloud Balancer-architectuur. Geleidelijk zullen we het detailniveau verhogen. We verdelen de balancercomponenten in drie klassen. De configuratievlakklasse is verantwoordelijk voor gebruikersinteractie en slaat de doelstatus van het systeem op. Het besturingsvlak slaat de huidige status van het systeem op en beheert systemen uit de datavlakklasse, die rechtstreeks verantwoordelijk zijn voor het leveren van verkeer van clients naar uw instanties.

Gegevensvlak

Het verkeer komt terecht op dure apparaten die borderrouters worden genoemd. Om de fouttolerantie te vergroten, werken meerdere van dergelijke apparaten tegelijkertijd in één datacenter. Vervolgens gaat het verkeer naar balancers, die anycast IP-adressen aan alle AZ's via BGP voor clients aankondigen. 

Architectuur van een netwerkload balancer in Yandex.Cloud

Verkeer wordt verzonden via ECMP - dit is een routeringsstrategie volgens welke er verschillende even goede routes naar het doel kunnen zijn (in ons geval is het doel het bestemmings-IP-adres) en pakketten kunnen langs elk van deze routes worden verzonden. We ondersteunen ook het werk in verschillende beschikbaarheidszones volgens het volgende schema: we adverteren een adres in elke zone, het verkeer gaat naar de dichtstbijzijnde en gaat niet verder dan zijn limieten. Later in dit bericht gaan we dieper in op wat er met het verkeer gebeurt.

Configuratievlak

 
Het belangrijkste onderdeel van het configuratievlak is de API, waarmee basisbewerkingen met balancers worden uitgevoerd: het maken, verwijderen, wijzigen van de samenstelling van instances, het verkrijgen van healthchecks-resultaten, enz. Aan de ene kant is dit een REST API, en aan de andere kant anders gebruiken we in de cloud heel vaak het raamwerk gRPC, dus 'vertalen' we REST naar gRPC en gebruiken dan alleen gRPC. Elk verzoek leidt tot het creëren van een reeks asynchrone idempotente taken die worden uitgevoerd op een gemeenschappelijke pool van Yandex.Cloud-werknemers. Taken zijn zo geschreven dat ze op elk moment kunnen worden opgeschort en vervolgens opnieuw kunnen worden gestart. Dit zorgt voor schaalbaarheid, herhaalbaarheid en logboekregistratie van bewerkingen.

Architectuur van een netwerkload balancer in Yandex.Cloud

Als gevolg hiervan zal de taak vanuit de API een verzoek indienen bij de balancer-servicecontroller, die in Go is geschreven. Het kan balancers toevoegen en verwijderen, de samenstelling van backends en instellingen wijzigen. 

Architectuur van een netwerkload balancer in Yandex.Cloud

De service slaat zijn status op in Yandex Database, een gedistribueerde beheerde database die u binnenkort kunt gebruiken. In Yandex.Cloud, zoals we al hebben gedaan verteldegeldt het hondenvoerconcept: als wij zelf gebruik maken van onze diensten, dan maken onze klanten daar ook graag gebruik van. Yandex Database is een voorbeeld van de implementatie van een dergelijk concept. Wij slaan al onze data op in YDB en hoeven niet na te denken over het onderhouden en schalen van de database: deze problemen zijn voor ons opgelost, wij gebruiken de database als een service.

Laten we terugkeren naar de balancercontroller. Zijn taak is om informatie over de balancer op te slaan en een taak naar de healthcheck-controller te sturen om de gereedheid van de virtuele machine te controleren.

Healthcheck-controller

Het ontvangt verzoeken om controleregels te wijzigen, slaat deze op in YDB, verdeelt taken over healthcheck-knooppunten en aggregeert de resultaten, die vervolgens in de database worden opgeslagen en naar de loadbalancer-controller worden verzonden. Het stuurt op zijn beurt een verzoek om de samenstelling van het cluster in het datavlak te wijzigen naar het loadbalancer-knooppunt, dat ik hieronder zal bespreken.

Architectuur van een netwerkload balancer in Yandex.Cloud

Laten we het nog eens hebben over gezondheidscontroles. Ze kunnen worden onderverdeeld in verschillende klassen. Audits kennen verschillende succescriteria. TCP-controles moeten ervoor zorgen dat er binnen een bepaalde tijd met succes een verbinding tot stand wordt gebracht. HTTP-controles vereisen zowel een succesvolle verbinding als een reactie met een 200-statuscode.

Bovendien verschillen controles per soort actie: ze zijn actief en passief. Passieve controles monitoren eenvoudigweg wat er met het verkeer gebeurt, zonder speciale actie te ondernemen. Dit werkt niet zo goed op L4 omdat het afhangt van de logica van de protocollen op een hoger niveau: op L4 is er geen informatie over hoe lang de bewerking duurde en of de voltooiing van de verbinding goed of slecht was. Voor actieve controles moet de balancer verzoeken naar elke serverinstantie sturen.

De meeste load balancers voeren zelf liveness checks uit. Bij Cloud hebben we besloten deze delen van het systeem te scheiden om de schaalbaarheid te vergroten. Met deze aanpak kunnen we het aantal balancers vergroten en tegelijkertijd het aantal healthcheck-aanvragen voor de service behouden. Controles worden uitgevoerd door afzonderlijke healthcheck-knooppunten, waarover controledoelen worden verdeeld en gerepliceerd. U kunt geen controles uitvoeren vanaf één host, omdat deze mogelijk mislukt. Dan krijgen we niet de status van de instanties die hij heeft gecontroleerd. We voeren controles uit op elk exemplaar van ten minste drie healthcheck-knooppunten. We onderscheiden de doeleinden van controles tussen knooppunten met behulp van consistente hash-algoritmen.

Architectuur van een netwerkload balancer in Yandex.Cloud

Het scheiden van balanceren en healthcheck kan tot problemen leiden. Als het healthcheck-knooppunt verzoeken doet aan de instantie, waarbij de balancer (die momenteel geen verkeer bedient) wordt omzeild, ontstaat er een vreemde situatie: de bron lijkt te leven, maar het verkeer zal deze niet bereiken. We lossen dit probleem op deze manier op: we initiëren gegarandeerd healthcheck-verkeer via balancers. Met andere woorden, het schema voor het verplaatsen van pakketten met verkeer van clients en van healthchecks verschilt minimaal: in beide gevallen zullen de pakketten de balancers bereiken, die ze bij de doelbronnen zullen afleveren.

Het verschil is dat clients verzoeken indienen bij VIP, terwijl healthchecks verzoeken indienen bij elke individuele RIP. Hier doet zich een interessant probleem voor: we geven onze gebruikers de mogelijkheid om bronnen te creëren in grijze IP-netwerken. Laten we ons voorstellen dat er twee verschillende cloudeigenaren zijn die hun diensten achter balancers hebben verborgen. Elk van hen heeft bronnen in het 10.0.0.1/24-subnet, met dezelfde adressen. Je moet ze op de een of andere manier kunnen onderscheiden, en hier moet je in de structuur van het virtuele Yandex.Cloud-netwerk duiken. Het is beter om meer details te vinden in video van about:cloud-evenement, is het nu belangrijk voor ons dat het netwerk uit meerdere lagen bestaat en tunnels heeft die kunnen worden onderscheiden door subnet-ID.

Healthcheck-nodes maken contact met balancers via zogenaamde quasi-IPv6-adressen. Een quasi-adres is een IPv6-adres met daarin een IPv4-adres en een gebruikers-subnet-ID. Het verkeer bereikt de balancer, die er het IPv4-bronadres uit haalt, IPv6 vervangt door IPv4 en het pakket naar het netwerk van de gebruiker stuurt.

Het omgekeerde verkeer gaat op dezelfde manier: de balancer ziet dat de bestemming een grijs netwerk van healthcheckers is, en converteert IPv4 naar IPv6.

VPP - het hart van het datavlak

De balancer wordt geïmplementeerd met behulp van Vector Packet Processing (VPP)-technologie, een raamwerk van Cisco voor batchverwerking van netwerkverkeer. In ons geval werkt het raamwerk bovenop de bibliotheek voor netwerkapparaatbeheer voor gebruikersruimte: Data Plane Development Kit (DPDK). Dit zorgt voor hoge pakketverwerkingsprestaties: er vinden veel minder interrupts plaats in de kernel, en er zijn geen contextwisselingen tussen kernelruimte en gebruikersruimte. 

VPP gaat zelfs nog verder en haalt nog meer prestaties uit het systeem door pakketten in batches te combineren. De prestatiewinst komt voort uit het agressieve gebruik van caches op moderne processors. Er wordt gebruik gemaakt van zowel datacaches (pakketten worden verwerkt in "vectoren", de gegevens staan ​​dicht bij elkaar) als instructiecaches: in VPP volgt de pakketverwerking een grafiek, waarvan de knooppunten functies bevatten die dezelfde taak uitvoeren.

De verwerking van IP-pakketten in VPP vindt bijvoorbeeld in de volgende volgorde plaats: eerst worden de pakketheaders geparseerd in het parseerknooppunt en vervolgens worden ze naar het knooppunt verzonden, dat de pakketten verder doorstuurt volgens routeringstabellen.

Een beetje hardcore. De auteurs van VPP tolereren geen compromissen bij het gebruik van processorcaches, dus typische code voor het verwerken van een vectorpakketten bevat handmatige vectorisatie: er is een verwerkingslus waarin een situatie als "we hebben vier pakketten in de wachtrij" wordt verwerkt, dan hetzelfde voor twee, dan - voor één. Prefetch-instructies worden vaak gebruikt om gegevens in caches te laden om de toegang daartoe in volgende iteraties te versnellen.

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

Healthchecks praten dus via IPv6 met de VPP, die ze in IPv4 verandert. Dit gebeurt door een knooppunt in de grafiek, dat we algoritmische NAT noemen. Voor omgekeerd verkeer (en conversie van IPv6 naar IPv4) is er hetzelfde algoritmische NAT-knooppunt.

Architectuur van een netwerkload balancer in Yandex.Cloud

Direct verkeer van de balancer-clients gaat via de grafiekknooppunten, die de balancering zelf uitvoeren. 

Architectuur van een netwerkload balancer in Yandex.Cloud

Het eerste knooppunt zijn sticky-sessies. Het slaat de hasj op 5-tupel voor gevestigde sessies. 5-tuple omvat het adres en de poort van de client van waaruit informatie wordt verzonden, het adres en de poorten van bronnen die beschikbaar zijn voor het ontvangen van verkeer, evenals het netwerkprotocol. 

De hash van vijf tupels helpt ons minder berekeningen uit te voeren in het daaropvolgende consistente hashknooppunt, en om wijzigingen in de resourcelijst achter de balancer beter af te handelen. Wanneer een pakket waarvoor geen sessie bestaat, bij de balancer arriveert, wordt het naar het consistente hashknooppunt verzonden. Dit is waar balancering plaatsvindt met behulp van consistente hashing: we selecteren een bron uit de lijst met beschikbare ‘live’ bronnen. Vervolgens worden de pakketten naar het NAT-knooppunt gestuurd, dat feitelijk het bestemmingsadres vervangt en de controlesommen opnieuw berekent. Zoals u kunt zien, volgen we de regels van VPP - graag, waarbij we vergelijkbare berekeningen groeperen om de efficiëntie van processorcaches te vergroten.

Consistente hashing

Waarom hebben we ervoor gekozen en wat is het eigenlijk? Laten we eerst eens kijken naar de vorige taak: een bron uit de lijst selecteren. 

Architectuur van een netwerkload balancer in Yandex.Cloud

Bij inconsistente hashing wordt de hash van het binnenkomende pakket berekend en wordt een bron uit de lijst geselecteerd door de rest van deze hash te delen door het aantal bronnen. Zolang de lijst ongewijzigd blijft, werkt dit schema goed: we sturen pakketten met hetzelfde 5-tupel altijd naar dezelfde instantie. Als een bron bijvoorbeeld niet meer reageert op healthchecks, zal voor een aanzienlijk deel van de hashes de keuze veranderen. De TCP-verbindingen van de client worden verbroken: een pakket dat eerder instantie A heeft bereikt, kan instantie B beginnen te bereiken, die niet bekend is met de sessie voor dit pakket.

Consistente hashing lost het beschreven probleem op. De eenvoudigste manier om dit concept uit te leggen is als volgt: stel je voor dat je een ring hebt waarnaar je bronnen distribueert via hash (bijvoorbeeld via IP:poort). Als u een bron selecteert, draait u het wiel over een hoek, die wordt bepaald door de hash van het pakket.

Architectuur van een netwerkload balancer in Yandex.Cloud

Dit minimaliseert de herverdeling van het verkeer wanneer de samenstelling van de bronnen verandert. Het verwijderen van een bron heeft alleen invloed op het deel van de consistente hashring waarin de bron zich bevond. Het toevoegen van een bron verandert ook de distributie, maar we hebben een vast sessieknooppunt, waardoor we reeds bestaande sessies niet kunnen overschakelen naar nieuwe bronnen.

We hebben gekeken naar wat er gebeurt met het directe verkeer tussen de balancer en de bronnen. Laten we nu eens kijken naar het retourverkeer. Het volgt hetzelfde patroon als controleverkeer: via algoritmische NAT, dat wil zeggen via reverse NAT 44 voor clientverkeer en via NAT 46 voor healthchecks-verkeer. We houden ons aan ons eigen schema: we verenigen het verkeer van de gezondheidscontroles en het echte gebruikersverkeer.

Loadbalancer-node en geassembleerde componenten

De samenstelling van balancers en bronnen in VPP wordt gerapporteerd door de lokale service - loadbalancer-node. Het abonneert zich op de stroom gebeurtenissen van de loadbalancer-controller en kan het verschil in kaart brengen tussen de huidige VPP-status en de doelstatus die wordt ontvangen van de controller. We krijgen een gesloten systeem: gebeurtenissen uit de API komen naar de balancercontroller, die taken toewijst aan de healthcheck-controller om de ‘levendigheid’ van bronnen te controleren. Dat wijst op zijn beurt taken toe aan het healthcheck-knooppunt en aggregeert de resultaten, waarna het ze terugstuurt naar de balancer-controller. Loadbalancer-node abonneert zich op gebeurtenissen van de controller en wijzigt de status van de VPP. In zo'n systeem weet elke dienst alleen wat nodig is over aangrenzende diensten. Het aantal aansluitingen is beperkt en wij hebben de mogelijkheid om verschillende segmenten zelfstandig te opereren en op te schalen.

Architectuur van een netwerkload balancer in Yandex.Cloud

Welke problemen zijn vermeden?

Al onze services op het besturingsvlak zijn geschreven in Go en hebben goede schaal- en betrouwbaarheidskenmerken. Go heeft veel open source-bibliotheken voor het bouwen van gedistribueerde systemen. We maken actief gebruik van GRPC, alle componenten bevatten een open source implementatie van service discovery - onze services monitoren elkaars prestaties, kunnen hun samenstelling dynamisch wijzigen, en we hebben dit gekoppeld aan GRPC-balancing. Voor statistieken gebruiken we ook een open source-oplossing. Op datavlak kregen we behoorlijke prestaties en een grote reserve aan hulpbronnen: het bleek erg moeilijk om een ​​standaard in elkaar te zetten waarop we konden vertrouwen op de prestaties van een VPP, in plaats van op een ijzeren netwerkkaart.

Problemen en oplossingen

Wat werkte niet zo goed? Go heeft automatisch geheugenbeheer, maar geheugenlekken komen nog steeds voor. De eenvoudigste manier om ermee om te gaan is door goroutines uit te voeren en deze te beëindigen. Afhaalmaaltijden: let op het geheugenverbruik van uw Go-programma's. Vaak is een goede indicator het aantal goroutines. Er zit een pluspunt aan dit verhaal: in Go is het gemakkelijk om runtimegegevens te verkrijgen: geheugengebruik, het aantal actieve goroutines en vele andere parameters.

Ook is Go mogelijk niet de beste keuze voor functionele tests. Ze zijn behoorlijk uitgebreid, en de standaardaanpak om “alles in een batch in CI uit te voeren” is niet erg geschikt voor hen. Feit is dat functionele tests meer middelen vergen en echte time-outs veroorzaken. Hierdoor kunnen tests mislukken omdat de CPU bezig is met unit-tests. Conclusie: Voer indien mogelijk “zware” tests los van unit-tests uit. 

De architectuur van microservice-gebeurtenissen is complexer dan een monoliet: het verzamelen van logboeken op tientallen verschillende machines is niet erg handig. Conclusie: als je microservices maakt, denk dan meteen aan tracing.

Onze plannen

We zullen een interne balancer lanceren, een IPv6-balancer, ondersteuning voor Kubernetes-scripts toevoegen, onze services blijven delen (momenteel zijn alleen healthcheck-node en healthcheck-ctrl geshard), nieuwe healthchecks toevoegen en ook een slimme aggregatie van controles implementeren. We overwegen de mogelijkheid om onze diensten nog onafhankelijker te maken, zodat ze niet rechtstreeks met elkaar communiceren, maar via een berichtenwachtrij. Sinds kort is er een SQS-compatibele dienst in de Cloud verschenen Yandex-berichtenwachtrij.

Onlangs vond de publieke release van Yandex Load Balancer plaats. Ontdekken de documentatie aan de service, beheer balancers op een manier die voor u handig is en verhoog de fouttolerantie van uw projecten!

Bron: www.habr.com

Voeg een reactie