Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі
Прывітанне, я Сяргей Еланцаў, распрацоўваю сеткавы балансавальнік нагрузкі у Яндэкс.Аблокі. Раней я кіраваў распрацоўкай L7-балансавальніка партала Яндэкса - калегі жартуюць, што чым бы я ні займаўся, атрымліваецца балансавальнік. Я раскажу чытачам Хабра, як трэба кіраваць нагрузкай у хмарнай платформе, якім мы бачым ідэальную прыладу дасягнення гэтай мэты і як рухаемся да пабудовы гэтай прылады.

Для пачатку ўвядзем некаторыя тэрміны:

  • VIP (Virtual IP) - IP-адрас балансавальніка
  • Сервер, бэкенд, інстанс - віртуальная машына з запушчаным дадаткам
  • RIP (Real IP) – IP-адрас сервера
  • Healthcheck - праверка гатоўнасці сервера
  • Зона даступнасці, Availability Zone, AZ - ізаляваная інфраструктура ў дата-цэнтры
  • Рэгіён - аб'яднанне розных AZ

Балансавальнікі нагрузкі вырашаюць тры асноўныя задачы: выконваюць саму балансаванне, паляпшаюць адмоваўстойлівасць сэрвісу і спрашчаюць яго маштабаванне. Адмаўстойлівасць забяспечваецца за кошт аўтаматычнага кіравання трафікам: балансавальнік сочыць за станам прыкладання і выключае з балансавання інстансы, якія не прайшлі праверку жвавасці. Маштабаванне забяспечваецца раўнамерным размеркаваннем нагрузкі па інстанс, а таксама абнаўленнем спісу інстансаў на лета. Калі балансіроўка будзе недастаткова раўнамернай, то некаторыя з інстансаў атрымаюць нагрузку, якая перавышае іх мяжа працаздольнасці, і сэрвіс стане менш надзейным.

Балансавальнік нагрузкі часта класіфікуюць па ўзроўні пратакола з мадэлі OSI, на якім ён працуе. Балансірошчык Аблокі працуе на ўзроўні TCP, што адпавядае чацвёртаму ўзроўню, L4.

Пяройдзем да агляду архітэктуры балансавальніка Аблокі. Будзем паступова падвышаць узровень дэталізацыі. Мы дзелім кампаненты балансавальніка на тры класы. Клас config plane адказвае за ўзаемадзеянне з карыстачом і захоўвае ў сабе мэтавае стан сістэмы. Control plane захоўвае ў сабе актуальны стан сістэмы і кіруе сістэмамі з класа data plane, якія адказваюць непасрэдна за дастаўку трафіку ад кліентаў да вашых інстансаў.

Data plane

Трафік трапляе на дарагія прылады пад назовам border routers. Для павышэння адмоваўстойлівасці ў адным дата-цэнтры адначасова працуе некалькі такіх прылад. Далей трафік трапляе на балансавальнікі, якія для кліентаў анансуюць anycast IP-адрас на ўсе AZ па BGP. 

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Трафік перадаецца па ECMP - гэта стратэгія маршрутызацыі, паводле якой можа існаваць некалькі аднолькава добрых маршрутаў да мэты (у нашым выпадку мэтай будзе destination IP-адрас) і пакеты можна адпраўляць па любым з іх. Таксама мы падтрымліваем працу ў некалькіх зонах даступнасці па наступнай схеме: анансуем адрас у кожнай з зон, трафік пападае ў найблізкую і ўжо за яе межы не выходзіць. Далей у пасце мы разгледзім падрабязней, што адбываецца з трафікам.

Config plane

 
Ключавым кампанентам config plane з'яўляецца API, праз які выконваюцца асноўныя аперацыі з балансавальнікамі: стварэнне, выдаленне, змена складу інстансаў, атрыманне вынікаў healthchecks і т. д. З аднаго боку, гэта REST API, а з іншай, мы ў Воблаку вельмі часта выкарыстоўваны фрэймворк gRPC, таму мы "перакладаны" REST у gRPC і далей выкарыстоўваны толькі gRPC. Любы запыт прыводзіць да стварэння серыі асінхронных ідэмпатэнтных задач, якія выконваюцца на агульным пуле воркераў Яндэкс.Аблокі. Задачы пішуцца такім чынам, што яны могуць быць у любы час прыпынены, а потым запушчаны нанава. Гэта забяспечвае маштабаванасць, паўтаральнасць і лагавальнасць аперацый.

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

У выніку задача з API зробіць запыт у сэрвіс-кантролер балансавальнікаў, які напісаны на Go. Ён можа дадаваць і выдаляць балансавальнікі, змяняць склад бэкэндаў і налады. 

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Сэрвіс захоўвае свой стан у Yandex Database – размеркаванай кіраванай БД, якой неўзабаве зможаце карыстацца і вы. У Яндэкс.Аблокі, як мы ўжо расказвалі, дзейнічае канцэпцыя dog food: калі мы самі карыстаемся сваімі сэрвісамі, то і нашыя кліенты таксама будуць з задавальненнем імі карыстацца. Yandex Database - прыклад увасаблення такой канцэпцыі. Мы захоўваем у YDB усе свае дадзеныя, і нам не даводзіцца думаць аб абслугоўванні і маштабаванні базы: гэтыя праблемы вырашаныя за нас, мы карыстаемся базай як сэрвісам.

Вяртаемся да кантролера балансавальніка. Яго задача - захаваць інфармацыю аб балансавальнік, адправіць задачу праверкі гатоўнасці віртуальнай машыны ў healthcheck controller.

Healthcheck controller

Ён атрымлівае запыты на змену правіл праверак, захоўвае іх у YDB, размяркоўвае задачы па healtcheck nodes і агрэгуе вынікі, якія затым захоўваюцца ў базу і адпраўляюцца ў loadbalancer controller. Ён, у сваю чаргу, адпраўляе запыт на змену складу кластара ў data plane на loadbalancer-node, аб якім я раскажу ніжэй.

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Пагаворым падрабязней пра healthchecks. Іх можна падзяліць на некалькі класаў. У праверак бываюць розныя крытэры поспеху. TCP-праверкам трэба паспяхова ўсталяваць злучэнне за фіксаваны час. HTTP-праверкі патрабуюць і паспяховага злучэння, і атрыманні адказу са статут-кодам 200.

Таксама праверкі адрозніваюцца па класе дзеяння - яны бываюць актыўныя і пасіўныя. Пасіўныя праверкі проста сочаць за тым, што адбываецца з трафікам, не прадпрымаючы ніякіх спецыяльных дзеянняў. Гэта не вельмі добрае працуе на L4, бо залежыць ад логікі пратаколаў больш высокага ўзроўня: на L4 няма інфармацыі аб тым, колькі часу заняла аперацыя, і ці было завяршэнне злучэння добрым ці дрэнным. Актыўныя праверкі патрабуюць, каб балансавальнік пасылаў запыты да кожнай інстансы сервера.

Вялікая частка балансавальнікаў нагрузкі выконвае праверкі "жывосці" самастойна. Мы ў Воблаку вырашылі падзяліць гэтыя часткі сістэмы для павышэння маштабаванасці. Такі падыход дазволіць нам павялічваць колькасць балансавальнікаў, захоўваючы колькасць healthcheck-запытаў да сэрвісу. Праверкі выконваюцца асобнымі healthcheck nodes, па якіх шардаваны і рэплікаваны мэты праверак. Нельга рабіць праверкі з аднаго хаста, бо ён можа адмовіць. Тады мы не атрымаем стан правераных ім інстансаў. Мы выконваем праверкі любога з інстансаў мінімум з трох healthcheck nodes. Мэты праверак мы шалуем паміж нодамі з дапамогай алгарытмаў кансістэнтнага хэшавання.

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Падзел балансавання і healthcheck можа прыводзіць да праблем. Калі healthcheck node здзяйсняе запыты да інстанса, абыходзячы балансавальнік (які ў дадзены момант не абслугоўвае трафік), тое ўзнікае дзіўная сітуацыя: рэсурс быццам бы жывы, але трафік да яго не дойдзе. Гэтую праблему мы вырашаем так: гарантавана заводзім healthcheck-трафік праз балансароўшчыкі. Іншымі словамі, схема перасоўвання пакетаў з трафікам ад кліентаў і ад healthchecks адрозніваецца мінімальна: у абодвух выпадках пакеты патрапяць на балансавальнікі, якія даставяць іх да мэтавых рэсурсаў.

Адрозненне ў тым, што кліенты робяць запыты на VIP, а healthchecks звяртаюцца да кожнага асобнага RIP. Тут узнікае цікавая праблема: нашым карыстачам мы даем магчымасць ствараць рэсурсы ў шэрых IP-сетках. Уявім, што ёсць два розныя ўладальнікі аблокаў, якія схавалі свае сэрвісы за балансавальнікі. У кожнага з іх ёсць рэсурсы ў падсетцы 10.0.0.1/24, прычым з аднолькавымі адрасамі. Трэба ўмець нейкім чынам іх адрозніваць, і тут трэба пагрузіцца ў прыладу віртуальнай сеткі Яндекс.Аблокі. Падрабязнасці лепш даведацца ў відэа з мерапрыемства about:cloud, нам зараз важна, што сетка шматслаёвая і мае ў сабе тунэлі, якія можна адрозніваць па id падсеткі.

Healthcheck nodes звяртаюцца да балансавальнікаў з дапамогай так званых квазі-IPv6-адрасоў. Квазіадрас - гэта IPv6-адрас, усярэдзіне якога зашыты IPv4-адрас і id падсеткі карыстальніка. Трафік трапляе на балансавальнік, той здабывае з яго IPv4-адрас рэсурсу, замяняе IPv6 на IPv4 і адпраўляе пакет у сетку карыстача.

Зваротны трафік ідзе гэтак жа: балансавальнік бачыць, што прызначэнне – шэрая сетка з healthcheckers, і пераўтворыць IPv4 у IPv6.

VPP - сэрца data plane

Балансавальнік рэалізаваны на тэхналогіі Vector Packet Processing (VPP) – фрэймворке ад Cisco для пакетнай апрацоўкі сеткавага трафіку. У нашым выпадку фрэймворк працуе па-над бібліятэкай user-space-кіравання сеткавымі прыладамі – Data Plane Development Kit (DPDK). Гэта забяспечвае высокую прадукцыйнасць апрацоўкі пакетаў: у ядры адбываецца нашмат менш перапыненняў, няма пераключэнняў кантэксту паміж kernel space і user space. 

VPP ідзе яшчэ далей і выціскае з сістэмы яшчэ больш прадукцыйнасці за рахунак аб'яднання пакетаў у батчы. Павышэнне прадукцыйнасці адбываецца дзякуючы агрэсіўнаму выкарыстанню кэшаў сучасных працэсараў. Выкарыстоўваюцца як кэшы дадзеных (пакеты апрацоўваюцца "вектарамі", дадзеныя ляжаць блізка адзін да аднаго), так і кэшы інструкцый: у VPP апрацоўка пакетаў варта па графе, у вузлах якога знаходзяцца функцыі, якія выконваюць адну задачу.

Напрыклад, апрацоўка IP-пакетаў у VPP праходзіць у такім парадку: спачатку ў вузле разбору адбываецца парсінг загалоўкаў пакетаў, а потым яны адпраўляюцца ў вузел, які перасылае пакеты далей паводле табліц маршрутызацыі.

Трохі хардкора. Аўтары VPP не церпяць кампрамісаў у выкарыстанні кэшаў працэсара, таму тыповы код апрацоўкі вектара пакетаў утрымоўвае ў сабе ручную вектарызацыю: ёсць цыкл апрацоўкі, у якім апрацоўваецца сітуацыя выгляду «у нас чатыры пакеты ў чарзе», затым - тое ж самае для двух, затым - для аднаго. Часта выкарыстоўваюцца prefetch-інструкцыі, якія загружаюць дадзеныя ў кэшы для паскарэння доступу да іх на наступных ітэрацыях.

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 звяртаюцца па IPv6 да VPP, які ператварае іх у IPv4. Гэтым займаецца вузел графа, які мы завем алгарытмічным NAT. Для зваротнага трафіку (і пераўтварэнні з IPv6 у IPv4) ёсць такі ж вузел алгарытмічнага NAT.

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Прамы трафік ад кліентаў балансавальніка ідзе праз вузлы графа, якія выконваюць саму балансаванне. 

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Першы вузел - sticky sessions. У ім захоўваецца хэш ад 5-tuple для ўсталяваных сесій. 5-tuple уключае ў сябе адрас і порт кліента, з якога перадаецца інфармацыя, адрас і портаў рэсурсаў, даступных для прыёму трафіку, а таксама сеткавы пратакол. 

Хэш ад 5-tuple дапамагае нам выконваць менш вылічэнняў у наступным вузле кансістэнтнага хэшавання, а таксама лепш апрацоўваць змену спісу рэсурсаў за балансавальнікам. Калі на балансроўшчык прыходзіць пакет, для якога няма сесіі, ён адпраўляецца ў вузел consistent hashing. Тут і адбываецца балансіроўка з дапамогай кансістэнтнага хэшавання: мы выбіраем рэсурс са спісу даступных "жывых" рэсурсаў. Далей пакеты адпраўляюцца ў вузел NAT, які праводзіць фактычную замену адраса прызначэння і пераразлік кантрольных сум. Як бачыце, мы прытрымліваемся правілаў VPP – падобнае да падобнага, групуем падобныя вылічэнні для павелічэння эфектыўнасці кэшаў працэсара.

Кансістэнтнае хэшаванне

Чаму мы выбралі менавіта яго і што гэта ўвогуле такое? Для пачатку разгледзім ранейшую задачу - выбару рэсурсу з спісу. 

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Пры некансістэнтным хэшаванні вылічаюць хэш ад уваходнага пакета, а рэсурс выбіраюць з спісу па астатку ад дзялення гэтага хэша на колькасць рэсурсаў. Пакуль спіс застаецца нязменным, такая схема працуе добра: мы заўсёды адпраўляем пакеты з аднолькавым 5-tuple на адзін і той жа інстанс. Калі ж, напрыклад, нейкі рэсурс перастаў адказваць на healthchecks, то для значнай часткі хэшаў выбар зменіцца. У кліента разарвуцца TCP-злучэнні: пакет, які раней трапляў на інстанс А, можа пачаць трапляць на інстанс Б, які з сесіяй для гэтага пакета не знаёмы.

Кансістэтнае хэшаванне вырашае апісаную праблему. Прасцей за ўсё растлумачыць гэтую канцэпцыю так: уявіце, што ў вас ёсць кольца, на якое вы размяркоўваеце рэсурсы па хэшу (напрыклад, па IP:port). Выбар рэсурсу - гэта паварот кола на кут, які вызначаецца па хэшу ад пакета.

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Тым самым мінімізуецца пераразмеркаванне трафіку пры змене складу рэсурсаў. Выдаленне рэсурсу паўплывае толькі на тую частку кольца кансістэнтнага хэшавання, на якой знаходзіўся дадзены рэсурс. Даданне рэсурсу таксама мяняе размеркаванне, але ў нас ёсць вузел sticky sessions, які дазваляе не пераключаць ужо ўсталяваныя сесіі на новыя рэсурсы.

Мы разгледзелі, што адбываецца з прамым трафікам паміж балансавальнікам і рэсурсамі. Цяпер давайце разбярэмся са зваротным трафікам. Ён варта па такой жа схеме, як і трафік праверак – праз алгарытмічны NAT, гэта значыць праз зваротны NAT 44 для кліенцкага трафіку і праз NAT 46 для трафіку healthchecks. Мы прытрымліваемся сваёй жа схемы: уніфікуем трафік healthchecks і рэальны трафік карыстальнікаў.

Loadbalancer-node і кампаненты ў зборы

Аб складзе балансавальнікаў і рэсурсаў у VPP паведамляе лакальны сэрвіс - loadbalancer-node. Ён падпісваецца на струмень падзей ад loadbalancer-controller, умее будаваць розніцу бягучага стану VPP і мэтавага стану, атрыманага ад кантролера. Мы атрымліваем замкнёную сістэму: падзеі з API прыходзяць на кантролер балансавальніка, які ставіць healthcheck-кантролеру задачы на ​​праверку "жывосці" рэсурсаў. Той, у сваю чаргу, ставіць задачы ў healthcheck-node і агрэгуе вынікі, пасля чаго аддае іх зваротна кантролеру балансавальнікаў. Loadbalancer-node падпісваецца на падзеі ад кантролера і мяняе стан VPP. У такой сістэме кожны сэрвіс ведае толькі неабходнае аб суседніх сэрвісах. Колькасць сувязяў абмежавана, і ў нас ёсць магчымасць незалежна эксплуатаваць і маштабаваць розныя сегменты.

Архітэктура сеткавага балансавальніка нагрузкі ў Яндэкс.Аблокі

Якіх пытанняў удалося пазбегнуць

Усе нашы сэрвісы ў control plane напісаны на Go і адрозніваюцца добрымі характарыстыкамі па маштабаванні і надзейнасці. У Go ёсць шмат апенсорсных бібліятэк для пабудовы размеркаваных сістэм. Мы актыўна выкарыстоўваем GRPC, усе кампаненты ўтрымоўваюць у сабе апенсорсную рэалізацыю service discovery – нашы сэрвісы сочаць за працаздольнасцю адзін аднаго, могуць змяняць свой склад дынамічна, і мы правязалі гэта з GRPC-балансаваннем. Для метрык мы таксама выкарыстоўваем апенсорснае рашэнне. У data plane мы атрымалі годную прадукцыйнасць і вялікі запас па рэсурсах: аказалася вельмі няпроста сабраць стэнд, на якім можна было б уперціся ў прадукцыйнасць VPP, а не жалезнай сеткавай карты.

Праблемы і рашэнні

Што спрацавала не вельмі добрае? У Go кіраванне памяццю аўтаматычнае, але ўцечкі памяці ўсё ж бываюць. Самы просты спосаб зладзіцца з імі - запускаць гаруціны і не забываць іх завяршаць. Выснова: сачыце за спажываннем памяці Go-праграм. Часта добрым індыкатарам з'яўляецца колькасць гаруцін. У гэтай гісторыі ёсць і плюс: у Go лёгка атрымаць дадзеныя па runtime - па спажыванні памяці, па колькасці запушчаных гаруцін і па шматлікіх іншых параметрах.

Акрамя таго, Go - магчыма, не лепшы выбар для функцыянальных тэстаў. Яны даволі шматслоўныя, і стандартны падыход "запусціць усё ў CI пачкам" для іх не вельмі падыходзіць. Справа ў тым, што функцыянальныя тэсты больш патрабавальныя да рэсурсаў, з імі ўзнікаюць сапраўдныя таймаўты. З-за гэтага тэсты могуць завяршацца няўдала, бо CPU заняты юніт-тэстамі. Выснова: па магчымасці выконвайце "цяжкія" тэсты асобна ад юніт-тэстаў. 

Мікрасэрвісная падзейная архітэктура складаней маналіта: грэпаць логі на дзясятках розных машын не вельмі зручна. Выснова: калі робіце мікрасэрвісы, адразу думайце пра трэйсінг.

Нашы планы

Мы запусцім унутраны балансавальнік, IPv6-балансавальнік, дадамо падтрымку сцэнараў Kubernetes, будзем і далей шаляваць нашы сэрвісы (цяпер шаляваныя толькі healthcheck-node і healthcheck-ctrl), дадамо новыя healthchecks, а таксама рэалізуем разумную агрэгацыю правер. Мы разглядаем магчымасць зрабіць нашы сэрвісы яшчэ больш незалежнымі - каб яны размаўлялі не напрамую паміж сабой, а з дапамогай чаргі паведамленняў. У Воблаку нядаўна з'явіўся SQS-сумяшчальны сэрвіс Yandex Message Queue.

Нядаўна адбыўся публічны рэліз Yandex Load Balancer. Вывучайце дакументацыю да сэрвісу, кіруйце балансавальнікамі зручным вам спосабам і падвышайце адмоваўстойлівасць сваіх праектаў!

Крыніца: habr.com

Дадаць каментар