A Yandex.Cloud hálózati terheléselosztójának felépítése

A Yandex.Cloud hálózati terheléselosztójának felépítése
Helló, Sergey Elantsev vagyok, fejlődök hálózati terheléselosztó a Yandex.Cloudban. Korábban én vezettem az L7 kiegyensúlyozó fejlesztését a Yandex portál számára - a kollégák viccelődnek, hogy bármit is csinálok, kiderül, hogy kiegyensúlyozó. Elmondom a Habr olvasóinak, hogyan kell kezelni a terhelést egy felhőplatformon, mit látunk ideális eszköznek e cél eléréséhez, és hogyan haladunk az eszköz kiépítése felé.

Először is vezessünk be néhány kifejezést:

  • VIP (virtuális IP) - egyensúlyozó IP-címe
  • Szerver, háttérrendszer, példány – egy alkalmazást futtató virtuális gép
  • RIP (Real IP) - szerver IP-címe
  • Healthcheck – a szerver készenlétének ellenőrzése
  • Elérhetőségi zóna, AZ – elszigetelt infrastruktúra adatközpontban
  • Régió - különböző AZ-ok uniója

A terheléselosztók három fő feladatot oldanak meg: elvégzik magát a kiegyenlítést, javítják a szolgáltatás hibatűrését, egyszerűsítik a méretezését. A hibatűrést az automatikus forgalomkezelés biztosítja: a kiegyenlítő figyeli az alkalmazás állapotát, és kizárja a kiegyenlítésből azokat a példányokat, amelyek nem felelnek meg az élőképesség ellenőrzésen. A méretezést a terhelés egyenletes elosztása biztosítja a példányok között, valamint a példányok listájának menet közbeni frissítése. Ha a kiegyenlítés nem kellően egységes, akkor egyes példányok kapacitáskorlátját meghaladó terhelést kapnak, és a szolgáltatás kevésbé megbízható.

A terheléselosztót gyakran azon OSI-modell protokollrétege osztályozza, amelyen fut. A Cloud Balancer a TCP szinten működik, ami a negyedik rétegnek, az L4-nek felel meg.

Térjünk át a Cloud balancer architektúra áttekintésére. Fokozatosan növeljük a részletességet. A kiegyensúlyozó alkatrészeket három osztályba osztjuk. A konfigurációs sík osztály felelős a felhasználói interakcióért, és tárolja a rendszer célállapotát. A vezérlősík tárolja a rendszer aktuális állapotát, és az adatsík osztályból kezeli a rendszereket, amelyek közvetlenül felelősek a forgalom továbbításáért az ügyfelektől a példányokhoz.

Adatsík

A forgalom drága eszközökre, úgynevezett border routerekre kerül. A hibatűrés növelése érdekében több ilyen eszköz működik egyszerre egy adatközpontban. Ezután a forgalom a balanszerekhez megy, amelyek az anycast IP-címeket az összes AZ-nak BGP-n keresztül jelentik be az ügyfelek számára. 

A Yandex.Cloud hálózati terheléselosztójának felépítése

A forgalom ECMP-n keresztül történik - ez egy olyan útválasztási stratégia, amely szerint több egyformán jó útvonal is lehet a célhoz (esetünkben a cél a cél IP-címe lesz), és ezeken bármelyiken lehet csomagokat küldeni. Több rendelkezésre állási zónában is támogatjuk a munkát az alábbi séma szerint: minden zónában címet hirdetünk, a forgalom a legközelebbi felé halad és nem lépi túl annak határait. A bejegyzés későbbi részében részletesebben megvizsgáljuk, mi történik a forgalommal.

Konfigurációs sík

 
A konfigurációs sík kulcseleme az API, amelyen keresztül az egyensúlyozókkal alapvető műveleteket hajtanak végre: példányok létrehozása, törlése, összetételének megváltoztatása, állapotellenőrzési eredmények beszerzése stb. Ez egyrészt egy REST API, másrészt a másrészt mi a Felhőben nagyon gyakran használjuk a keretrendszer gRPC-t, ezért a REST-et „lefordítjuk” gRPC-re, majd csak a gRPC-t használjuk. Bármely kérés egy sor aszinkron idempotens feladat létrehozásához vezet, amelyeket a Yandex.Cloud dolgozóinak közös készletén hajtanak végre. A feladatok úgy vannak megírva, hogy bármikor felfüggeszthetők, majd újraindíthatók. Ez biztosítja a skálázhatóságot, a megismételhetőséget és a műveletek naplózását.

A Yandex.Cloud hálózati terheléselosztójának felépítése

Ennek eredményeként az API-ból származó feladat kérést küld a Balancer szolgáltatásvezérlőnek, amely a Go nyelvben van megírva. Hozzáadhat és eltávolíthat kiegyenlítőket, módosíthatja a háttérrendszerek összetételét és a beállításokat. 

A Yandex.Cloud hálózati terheléselosztójának felépítése

A szolgáltatás állapotát a Yandex Database-ben tárolja, egy elosztott felügyelt adatbázisban, amelyet hamarosan használhat. A Yandex.Cloudban, ahogy már mi is mondta, a kutyaeledel koncepció érvényesül: ha mi magunk is igénybe vesszük szolgáltatásainkat, akkor ügyfeleink is szívesen veszik azokat. A Yandex Database egy példa egy ilyen koncepció megvalósítására. Minden adatunkat az YDB-ben tároljuk, és nem kell az adatbázis karbantartására, méretezésére gondolnunk: ezek a problémák megoldódnak helyettünk, az adatbázist szolgáltatásként használjuk.

Térjünk vissza az egyensúlyozó vezérlőhöz. Feladata az egyensúlyozó információinak mentése és a virtuális gép készenlétének ellenőrzésére szolgáló feladat küldése az állapotellenőrző vezérlőnek.

Healthcheck vezérlő

Megkapja az ellenőrzési szabályok módosítására vonatkozó kéréseket, elmenti azokat az YDB-be, elosztja a feladatokat a healtcheck csomópontok között, és összesíti az eredményeket, amelyeket azután elment az adatbázisba, és elküldi a loadbalancer vezérlőnek. Ez viszont kérelmet küld a klaszter adatsík összetételének megváltoztatására a loadbalancer-node-nak, amit az alábbiakban tárgyalok.

A Yandex.Cloud hálózati terheléselosztójának felépítése

Beszéljünk többet az egészségügyi ellenőrzésekről. Több osztályra oszthatók. Az auditoknak különböző sikerességi kritériumai vannak. A TCP-ellenőrzéseknek meghatározott időn belül sikeresen létre kell hozniuk a kapcsolatot. A HTTP-ellenőrzések sikeres csatlakozást és egy 200-as állapotkódot tartalmazó választ is igényelnek.

Ezenkívül a csekk a cselekvési osztályban különbözik - aktív és passzív. A passzív ellenőrzések egyszerűen nyomon követik, hogy mi történik a forgalommal, anélkül, hogy különösebb lépéseket tennének. L4-en ez nem működik túl jól, mert ez a magasabb szintű protokollok logikájától függ: L4-en nincs információ arról, hogy mennyi ideig tartott a művelet, vagy hogy a kapcsolat befejezése jó vagy rossz volt. Az aktív ellenőrzések megkövetelik, hogy a kiegyenlítő kéréseket küldjön minden kiszolgálópéldánynak.

A legtöbb terheléselosztó saját maga végzi el az életerő-ellenőrzést. A Cloudnál úgy döntöttünk, hogy szétválasztjuk a rendszer ezen részeit a méretezhetőség növelése érdekében. Ez a megközelítés lehetővé teszi számunkra, hogy növeljük az egyensúlyozók számát, miközben fenntartjuk a szolgáltatáshoz intézett állapotfelmérések számát. Az ellenőrzéseket külön állapotellenőrzési csomópontok végzik, amelyeken keresztül az ellenőrzési célokat feldarabolják és replikálják. Nem hajthat végre ellenőrzéseket egyetlen gazdagépről, mert az meghiúsulhat. Akkor nem kapjuk meg az általa ellenőrzött példányok állapotát. A példányok bármelyikén legalább három állapotellenőrzési csomópontból ellenőrizzük. A csomópontok közötti ellenőrzések céljait konzisztens kivonatolási algoritmusok segítségével bontjuk ki.

A Yandex.Cloud hálózati terheléselosztójának felépítése

Az egyensúlyozás és az állapotfelmérés szétválasztása problémákhoz vezethet. Ha az állapotellenőrző csomópont kéréseket intéz a példányhoz, megkerülve a kiegyenlítőt (amely jelenleg nem szolgálja ki a forgalmat), akkor furcsa helyzet áll elő: úgy tűnik, hogy az erőforrás él, de a forgalom nem éri el. Ezt a problémát így oldjuk meg: garantáltan az egyensúlyozókon keresztül indítjuk az egészségellenőrzési forgalmat. Más szóval, a kliensek és az állapotellenőrzések forgalommal történő csomagok mozgatásának sémája minimálisan különbözik: mindkét esetben a csomagok eljutnak a kiegyenlítőkhöz, amelyek eljuttatják azokat a cél erőforrásokhoz.

A különbség az, hogy az ügyfelek kéréseket intéznek a VIP-hez, míg az állapotfelmérés minden egyes RIP-hez. Itt egy érdekes probléma merül fel: lehetőséget adunk felhasználóinknak, hogy szürke IP-hálózatokban hozzanak létre erőforrásokat. Képzeljük el, hogy két különböző felhőtulajdonos van, akik szolgáltatásaikat az egyensúlyozók mögé rejtették. Mindegyikük rendelkezik erőforrásokkal a 10.0.0.1/24 alhálózatban, azonos címekkel. Valahogy meg kell tudni különböztetni őket, és itt meg kell merülnie a Yandex.Cloud virtuális hálózat szerkezetében. Jobb, ha további részleteket itt talál videó az about:cloud eseményről, most fontos számunkra, hogy a hálózat többrétegű legyen, és olyan alagutak legyenek, amelyek alhálózati azonosító alapján megkülönböztethetők.

A Healthcheck csomópontok úgynevezett kvázi-IPv6-címek használatával lépnek kapcsolatba a kiegyenlítőkkel. A kvázicím egy IPv6-cím, amelybe be van ágyazva egy IPv4-cím és egy felhasználói alhálózati azonosító. A forgalom eléri a kiegyenlítőt, amely kivonja belőle az IPv4 erőforrás címét, az IPv6-ot IPv4-re cseréli és a csomagot elküldi a felhasználó hálózatába.

A fordított forgalom ugyanígy megy: a kiegyenlítő látja, hogy a cél egy szürke hálózat az egészségellenőrzőktől, és az IPv4-et IPv6-ba konvertálja.

VPP – az adatsík szíve

A kiegyenlítő a Vector Packet Processing (VPP) technológiával valósul meg, amely a Cisco keretrendszere a hálózati forgalom kötegelt feldolgozására. Esetünkben a keretrendszer a felhasználói tér hálózati eszközkezelő könyvtárán – Data Plane Development Kit (DPDK) – működik. Ez biztosítja a nagy csomagfeldolgozási teljesítményt: sokkal kevesebb megszakítás történik a kernelben, és nincs kontextusváltás a kernelterület és a felhasználói terület között. 

A VPP még tovább megy, és még több teljesítményt facsar ki a rendszerből a csomagok kötegekbe történő kombinálásával. A teljesítménynövekedés a gyorsítótárak agresszív használatából származik a modern processzorokon. Mindkét adatgyorsítótárat (a csomagok „vektorokban” dolgozzák fel, az adatok közel vannak egymáshoz) és az utasítás gyorsítótárat is használjuk: a VPP-ben a csomagfeldolgozás egy gráfot követ, melynek csomópontjai ugyanazt a feladatot ellátó funkciókat tartalmaznak.

Például az IP-csomagok feldolgozása VPP-ben a következő sorrendben történik: először a csomagfejléceket az elemző csomópontban elemzik, majd elküldik a csomópontnak, amely az útválasztási táblák szerint továbbítja a csomagokat.

Egy kis hardcore. A VPP szerzői nem tűrnek kompromisszumot a processzor gyorsítótárak használatában, ezért a csomagvektorok feldolgozásának tipikus kódja manuális vektorizálást tartalmaz: van egy feldolgozási hurok, amelyben a „négy csomagunk van a sorban” helyzet feldolgozása, akkor ugyanaz kettőnek, majd - egynek. Az előzetes letöltési utasításokat gyakran használják az adatok gyorsítótárakba való betöltésére, hogy felgyorsítsák a hozzáférést a későbbi iterációk során.

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

Tehát a Healthchecks IPv6-on keresztül beszél a VPP-vel, amely IPv4-ré változtatja őket. Ezt a gráf egy csomópontja végzi, amelyet algoritmikus NAT-nak nevezünk. A visszirányú forgalomhoz (és az IPv6-ról IPv4-re való átalakításhoz) ugyanaz az algoritmikus NAT csomópont.

A Yandex.Cloud hálózati terheléselosztójának felépítése

A kiegyenlítő kliensektől érkező közvetlen forgalom a gráf csomópontjain megy keresztül, amelyek maguk végzik el a kiegyensúlyozást. 

A Yandex.Cloud hálózati terheléselosztójának felépítése

Az első csomópont a ragadós munkamenetek. Tárolja a hash-t 5 soros kialakított ülésekre. Az 5-tuple tartalmazza a kliens címét és portját, ahonnan az információ továbbításra kerül, a forgalom fogadására rendelkezésre álló erőforrások címét és portjait, valamint a hálózati protokollt. 

Az 5 sorból álló hash segítségével kevesebb számítást végezhetünk a következő konzisztens kivonatoló csomópontban, valamint jobban kezelhetjük az erőforráslista változásait a kiegyenlítő mögött. Amikor egy olyan csomag érkezik a kiegyenlítőhöz, amelyhez nincs munkamenet, az elküldésre kerül a konzisztens kivonatoló csomópontnak. Ez az a hely, ahol a kiegyenlítés következetes kivonatolás segítségével történik: kiválasztunk egy erőforrást az elérhető „élő” erőforrások listájából. Ezután a csomagok elküldésre kerülnek a NAT csomóponthoz, amely ténylegesen lecseréli a célcímet, és újraszámolja az ellenőrző összegeket. Mint látható, a VPP - like to like szabályait követjük, hasonló számításokat csoportosítunk a processzor gyorsítótárak hatékonyságának növelése érdekében.

Következetes kivonatolás

Miért választottuk és mi az? Először is nézzük meg az előző feladatot – válasszunk ki egy erőforrást a listából. 

A Yandex.Cloud hálózati terheléselosztójának felépítése

Inkonzisztens kivonatolás esetén a rendszer kiszámítja a bejövő csomag kivonatát, és kiválaszt egy erőforrást a listából a hash és az erőforrások számának maradékával. Amíg a lista változatlan marad, addig ez a séma jól működik: mindig ugyanarra a példányra küldjük az azonos 5 sorral rendelkező csomagokat. Ha például valamelyik erőforrás nem reagál az állapotellenőrzésekre, akkor a hash-ek jelentős részén a választás megváltozik. Az ügyfél TCP-kapcsolatai megszakadnak: egy csomag, amely korábban elérte az A példányt, elkezdheti elérni a B példányt, amely nem ismeri a csomag szekcióját.

A következetes hash megoldja a leírt problémát. Ezt a fogalmat a legegyszerűbben a következőképpen magyarázhatjuk meg: képzeljük el, hogy van egy gyűrűje, amelyre az erőforrásokat hash alapján osztja el (például IP:port szerint). Az erőforrás-kiválasztás a kerék szöggel történő elforgatása, amelyet a csomag hash-je határoz meg.

A Yandex.Cloud hálózati terheléselosztójának felépítése

Ez minimalizálja a forgalom újraelosztását, amikor az erőforrások összetétele megváltozik. Az erőforrás törlése csak a konzisztens hash-gyűrű azon részét érinti, amelyben az erőforrás található. Az erőforrás hozzáadása a disztribúciót is megváltoztatja, de van egy ragadós munkamenet-csomópontunk, amely lehetővé teszi, hogy a már létrehozott munkameneteket ne váltsuk át új erőforrásokra.

Megnéztük, mi történik a forgalom irányításával az egyensúlyozó és az erőforrások között. Most nézzük a visszatérő forgalmat. Ugyanazt a mintát követi, mint a forgalom ellenőrzése – algoritmikus NAT-on keresztül, azaz fordított NAT 44-en keresztül az ügyfélforgalom és a NAT 46-on keresztül az állapotellenőrzési forgalom esetében. Ragaszkodunk a saját rendszerünkhöz: egyesítjük az egészségellenőrzési forgalmat és a valós felhasználói forgalmat.

Loadbalancer-csomópont és összeszerelt alkatrészek

A VPP kiegyensúlyozóinak és erőforrásainak összetételét a helyi szolgáltatás - loadbalancer-node - jelenti. Feliratkozik a loadbalancer-controller eseményfolyamára, és képes ábrázolni az aktuális VPP állapot és a vezérlőtől kapott célállapot közötti különbséget. Zárt rendszert kapunk: az API-ból az események a balancer vezérlőhöz érkeznek, amely feladatokat rendel a healthcheck vezérlőhöz, hogy ellenőrizze az erőforrások „élőségét”. Ez viszont feladatokat rendel az állapotellenőrző csomóponthoz, és összesíti az eredményeket, majd visszaküldi azokat a kiegyenlítő vezérlőnek. A Loadbalancer-node feliratkozik a vezérlő eseményeire, és megváltoztatja a VPP állapotát. Egy ilyen rendszerben minden szolgáltatás csak azt tudja, ami szükséges a szomszédos szolgáltatásokról. A kapcsolatok száma korlátozott, és lehetőségünk van a különböző szegmensek önálló működtetésére és méretezésére.

A Yandex.Cloud hálózati terheléselosztójának felépítése

Milyen problémákat sikerült elkerülni?

A vezérlősíkon minden szolgáltatásunk Go nyelven íródott, és jó skálázási és megbízhatósági jellemzőkkel rendelkezik. A Go számos nyílt forráskódú könyvtárral rendelkezik elosztott rendszerek építéséhez. Aktívan használjuk a GRPC-t, minden komponens tartalmaz egy nyílt forráskódú szolgáltatásfelderítést - szolgáltatásaink figyelik egymás teljesítményét, dinamikusan változtathatják összetételüket, és ezt összekapcsoltuk a GRPC-kiegyenlítéssel. A mérőszámokhoz nyílt forráskódú megoldást is használunk. Adatsíkon tisztességes teljesítményt és nagy erőforrás-tartalékot kaptunk: nagyon nehéznek bizonyult olyan állvány összeállítása, amelyre nem egy vas hálózati kártya, hanem egy VPP teljesítményére támaszkodhattunk.

Problémák és megoldások

Mi nem működött olyan jól? A Go automatikus memóriakezeléssel rendelkezik, de memóriaszivárgás továbbra is előfordul. A legegyszerűbb módja annak, hogy megbirkózzon velük, ha gorutinokat futtat, és ne felejtse el leállítani őket. Elvihető: Figyelje Go programjai memóriafelhasználását. Gyakran jó mutató a gorutinok száma. Ennek a történetnek van egy pluszja is: a Go-ban könnyű megszerezni a futásidejű adatokat – a memóriafogyasztást, a futó gorutinok számát és sok más paramétert.

Ezenkívül a Go nem a legjobb választás a funkcionális tesztekhez. Meglehetősen bőbeszédűek, és a szokásos megközelítés, miszerint „minden CI-ben egy kötegben fut”, nem igazán megfelelő számukra. A tény az, hogy a funkcionális tesztek erőforrásigényesebbek, és valós időtúllépést okoznak. Emiatt előfordulhat, hogy a tesztek sikertelenek, mert a CPU egységtesztekkel van elfoglalva. Következtetés: Ha lehetséges, végezze el a „nehéz” teszteket az egységtesztektől elkülönítve. 

A mikroszolgáltatási eseményarchitektúra összetettebb, mint egy monolit: a naplók gyűjtése több tucat különböző gépen nem túl kényelmes. Következtetés: ha mikroszolgáltatásokat készít, azonnal gondoljon a nyomkövetésre.

A terveink

Elindítunk egy belső kiegyenlítőt, egy IPv6-egyensúlyozót, hozzáadjuk a Kubernetes szkriptek támogatását, folytatjuk szolgáltatásaink szilánkosítását (jelenleg csak a healthcheck-node és a healthcheck-ctrl vannak feldarabolva), új állapotellenőrzéseket adunk hozzá, és az ellenőrzések intelligens összesítését is bevezetjük. Megfontoljuk annak lehetőségét, hogy szolgáltatásainkat még függetlenebbé tegyük - hogy ne közvetlenül, hanem üzenetsor segítségével kommunikáljanak egymással. Nemrég jelent meg a Cloudban egy SQS-kompatibilis szolgáltatás Yandex üzenetsor.

Nemrég került sor a Yandex Load Balancer nyilvános kiadására. Fedezd fel dokumentáció a szervizbe, kezelje a kiegyensúlyozókat az Ön számára kényelmes módon és növelje projektjei hibatűrését!

Forrás: will.com

Hozzászólás