Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions

Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions

Здравейте, Хабр! Аз съм Артем Карамышев, ръководител на екипа по системно администриране Mail.Ru Cloud Solutions (MCS). През последната година направихме много стартирания на нови продукти. Желаехме API услугите да бъдат лесно мащабируеми, отказоустойчиви и готови за бърз растеж на потребителското натоварване. Нашата платформа е реализирана на OpenStack и искам да разкажа за проблема с отказоустойчивостта на компонентите, които трябваше да разрешим, за да получим отказоустойчива система. Считам, че това ще бъде интересно за тези, които също развиват продукти на OpenStack.

Общата отказоустойчивост на платформата произтича от устойчивостта на нейните компоненти. Така че постепенно ще преминем през всички нива, на които сме открили рискове и сме ги адресирали.

Видеоверсия на тази история, с която първоизточникът е доклад на конференцията Uptime day 4, организирана от ITSumma, може да се види в YouTube канала на Uptime Community.

Отказоустойчивост на физическата архитектура

Публичната част на облака MCS в момента е базирана в два дата центъра от ниво Tier III, между които има собствено тъмно влакно, резервирано на физическо ниво с различни трасета, с пропускна способност 200 Гбит/c. Нивото Tier III осигурява необходимото ниво на отказоустойчивост на физическата инфраструктура.

Тъмното влакно е резервирано както на физическо, така и на логическо ниво. Процесът на резервиране на каналите беше итеративен, появяваха се проблеми и постоянно подобряваме връзката между дата центровете.

Например, неотдавна при работа в шахта до един от дата центровете, Булдозер нарани тръба, в която се намираха как основния, така и резервния оптичен кабел. Нашият отказоустойчив канал за комуникация с дата центъра се оказа уязвим на едно място, в шахтата. Съответно, загубихме част от инфраструктурата. Ние си направихме изводи, предприехме редица действия, включително и прекарахме допълнителна оптика покрай съседната шахта.

В дата-центровете има точки за присъствие на доставчици на връзки, на които предаваме своите префикси по BGP. За всяко мрежово направление се избира най-добрата метрика, което позволява на различни клиенти да получават най-добро качество на свързването. Ако връзката чрез един доставчик се прекъсне, пренасочваме маршрутизацията си през наличните доставчици.

В случай на отказ на доставчик, в автоматичен режим преминаваме към следващия. В случай на отказ на един от дата-центровете, имаме зеркално копие на нашите услуги във втория дата-център, който поема цялото натоварване.

Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions
Отказоустойчивост на физическата инфраструктура

Какво използваме за отказоустойчивост на ниво приложения

Нашият сервис е построен на редица opensource-компоненти.

ExaBGP — сервис, който реализира редица функции с използване на протокола за динамична маршрутизация на база BGP. Активно го използваме, за да анонсираме нашите бели IP адреси, чрез които потребителите получават достъп до API.

HAProxy — високо натоварен балансировщик, позволяващ да се настройват много гъвкави правила за балансиране на трафика на различни нива на модела OSI. Използваме го за балансиране преди всички услуги: бази данни, брокери на съобщения, API услуги, уеб услуги, нашите вътрешни проекти — всичко е зад HAProxy.

API приложение — уеб приложение, написано на python, с помощта на което потребителят управлява своята инфраструктура, своя сервис.

Работник приложение (по-нататък просто работник) — в услугите на OpenStack това е инфраструктурен демон, който позволява да се предават API команди на инфраструктурата. Например, създаването на диск става именно в работника, а заявката за създаване — в API приложението.

Стандартна архитектура на OpenStack приложението

Повечето услуги, които се разработват за OpenStack, се опитват да следват единна парадигма. Услугата обикновено се състои от 2 части: API и worker-и (изпълнители на бекенда). Обикновено API е WSGI приложение на Python, което се стартира или като самостоятелен процес (демон), или чрез вече готов уеб сървър Nginx, Apache. API обработва заявките на потребителя и предава допълнителни инструкции за изпълнение на приложението worker application. Предаването става чрез брокер на съобщения, обикновено RabbitMQ, останалите се поддържат слабо. Когато съобщенията стигнат до брокера, те се обработват от worker-ите, които при нужда връщат отговор.

Тази парадигма предвижда изолирани общи точки на провал: RabbitMQ и базата данни. Въпреки това RabbitMQ е изолиран в рамките на една услуга и по идея може да бъде индивидуален за всяка услуга. Затова в MCS максимално разделяме тези услуги, за всеки отделен проект създаваме отделна база и отделен RabbitMQ. Този подход е добър, тъй като в случай на авария в някои уязвими точки, се разрушава не цялата услуга, а само част от нея.

Броят на worker application не е ограничен, така че API може лесно да се мащабира хоризонтално зад балансьори с цел увеличаване на производителността и отказоустойчивостта.

В някои услуги е необходима координация вътре в услугата — когато се извършват сложни последователни операции между API и worker-ите. В този случай се използва единен център за координация, кластерна система от типа Redis, Memcache, etcd, която позволява на един worker да каже на другия, че тази задача е назначена за него ("ти, моля, не я взимай"). Ние използваме etcd. Обикновено worker-ите активно комуникират с базата данни, пишат и четат информация от нея. Като база данни използваме MariaDB, която е в мулти-майсторски кластер.

Този класически единичен сервис е организиран по общоприетия начин за OpenStack. Може да бъде разглеждан като затворена система, за която достатъчно очевидни са начините за мащабиране и отказоустойчивост. Например, за отказоустойчивост API е достатъчно да се постави пред него балансировач. Мащабирането на worker-ите се постига чрез увеличаване на тяхното количество.

Слабост в цялата схема са RabbitMQ и MariaDB. Архитектурата им заслужава отделна статия. В тази статия искам да се фокусирам върху отказоустойчивостта на API.

Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions
Архитектура на Openstack Application. Балансиране и отказоустойчивост на облачната платформа

Правим HAProxy отказоустойчив с помощта на ExaBGP

За да бъдат нашите API мащабируеми, бързи и отказоустойчиви, поставихме пред тях балансировчик. Избрахме HAProxy. Лично смятам, че той има всички необходими характеристики за нашата задача: балансиране на няколко нива на OSI, интерфейс за управление, гъвкавост и мащабируемост, множество методи за балансиране, поддръжка на таблици със сесии.

Проблемът, който трябваше да решим, е отказоустойчивостта на самия балансировчик. Само инсталирането на балансировчик също създава точка на отказ: ако балансировчикът се повреди, услугата спира. За да избегнем това, използвахме HAProxy заедно с ExaBGP.

ExaBGP позволява да реализираме механизъм за проверка на състоянието на услугата. Използвахме този механизъм, за да проверяваме работоспособността на HAProxy и в случай на проблеми да изключваме услугата HAProxy от BGP.

Схема ExaBGP+HAProxy

  1. Инсталираме необходимия софтуер, ExaBGP и HAProxy на три сървъра.
  2. На всеки от сървърите създаваме loopback интерфейс.
  3. На трите сървъра задаваме един и същ публичен IP адрес на този интерфейс.
  4. Публичният IP адрес се анонсира в интернет чрез ExaBGP.

Отказоустойчивостта се постига чрез анонсиране на един и същ IP адрес от трите сървъра. От мрежова гледна точка, един и същ адрес е достъпен от три различни next hop. Рутерът вижда три идентични маршрута и избира по собствена метрика най-приоритетния от тях (обикновено е един и същ вариант), а трафикът минава само през един от сървърите.

При проблеми с работата на HAProxy или при повреда на сървъра, ExaBGP спира да анонсира маршрута и трафикът плавно се прехвърля на другия сървър.

По този начин постигнахме отказоустойчивост на балансировчика.

Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions
Отказоустойчивост на балансировчиците HAProxy

Схемата се получи неидеална: научихме се да резервираме HAProxy, но не и да разпределяме натоварването вътре в услугите. Затова разширихме схемата: преминахме към балансиране между няколко публични IP адреса.

Балансировка на база DNS плюс BGP

Въпросът за балансировката на натоварването пред нашите HAProxy остава нерешен. Въпреки това, можем да го решим доста просто, както направихме и при нас.

За балансировка на три сървъра ще са необходими 3 публични IP адреса и старият добър DNS. Всеки от тези адреси е определен на loopback интерфейса на всеки HAProxy и се анонсира в интернет.

В OpenStack за управление на ресурсите се използва каталог на услугите, в който се задава endpoint API на съответната услуга. В този каталог записваме домейно име — public.infra.mail.ru, което се резолвира чрез DNS на три различни IP адреса. В резултат получаваме балансировка на натоварването между трите адреса чрез DNS.

Но тъй като при анонсирането на публични IP адреси не управляваме приоритетите на избора на сървър, все още не можем да наречем това балансировка. Обикновено ще бъде избран само един сървър по старшинство на IP адреса, а другите два ще бъдат неработещи, тъй като не са зададени никакви метрики в BGP.

Започнахме да предаваме маршрути чрез ExaBGP с различни метрики. Всеки балансировчик анонсира всичките три публични IP адреса, но един от тях, главният за конкретния балансировчик, се анонсира с минимална метрика. Така че, докато трите балансировчика са в ред, запитванията към първия IP адрес попадат на първия балансировчик, запитванията към втория на втория, а към третия на третия.

Какво се случва в момента, когато един от балансировчиците падне? При отказан балансировчик основният адрес все още се анонсира от другите двама, трафикът между тях се преразпределя. По този начин предаваме на потребителя чрез DNS веднага няколко IP адреса. Чрез балансировка по DNS и различна метрика получаваме равномерно разпределение на натоварването между всичките три балансировчика и в същото време не губим отказоустойчивост.

Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions
Балансировка HAProxy на база DNS + BGP

Взаимодействие между ExaBGP и HAProxy

И така, реализирахме отказоустойчивост в случай на отказ на сървър, основана на прекратяването на анонса на маршрутите. Но HAProxy може да се изключи и по други причини освен отказ на сървър: административни грешки, неизправности в услугата. Искаме да премахнем повредения балансировчик от натоварването и в тези случаи е необходим друг механизъм.

Затова, разширявайки предишната схема, реализирахме heartbeat между ExaBGP и HAProxy. Това е софтуерна реализация на взаимодействието между ExaBGP и HAProxy, при която ExaBGP използва персонализирани скриптове за проверка на статуса на приложенията.

За целта в конфигурацията на ExaBGP трябва да бъде настроен health checker, който да проверява статуса на HAProxy. В нашия случай настроихме health backend в HAProxy, а от ExaBGP проверяваме с прост GET заявка. Ако анонсът спре да се случва, то HAProxy вероятно не работи и няма нужда да го анонсираме.

Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions
HAProxy Health Check

HAProxy Peers: синхронизация на сесиите

Следващото нещо, което трябваше да направим, е да синхронизираме сесиите. При работа с разпределени балансьори е трудно да се организира запазването на информацията за клиентските сесии. Но HAProxy е един от малкото балансьори, които могат да го направят благодарение на функционалността Peers — възможността за прехвърляне на таблиците с сесии между различни процеси на HAProxy.

Съществуват различни методи на балансиране: прости, като round-robin, и разширени, при които сесията на клиента се запомня, и той всеки път попада на същия сървър, на който е бил преди. Ние искахме да реализираме втория вариант.

В HAProxy за запазване на сесиите на клиента се използва stick-tables. Те запазват оригиналния IP адрес на клиента, избрания целеви адрес (бекенд) и някаква служебна информация. Обикновено stick-таблиците се използват за запазване на двойката source-IP + destination-IP, което е особено полезно за приложения, които не могат да предават контекста на сесията на потребителя, когато преминават към друг балансьор, например — в режим на балансиране RoundRobin.

Ако stick-таблицата бъде научена да се прехвърля между различни процеси на HAProxy (между които се извършва балансирането), нашите балансьори ще могат да работят с един и същ набор от stick-таблици. Това би дало възможност за безшевно превключване на мрежата на клиента при срив на един от балансьорите, а работата със сесиите на клиентите ще продължи на същите бекенди, които бяха избрани преди.

За правилната работа трябва да бъде решен проблемът с IP адреса на балансьора, от който е установена сессията. В нашия случай, това е динамичен адрес на loopback интерфейса.

Правилната работа на peers се постига само при определени условия. Тоест, TCP таймаутите трябва да бъдат достатъчно дълги или превключването трябва да бъде достатъчно бързо, за да не свърши TCP сесията. Въпреки това, това позволява безпроблемно превключване.

В нашата IaaS има услуга, изградена с такава съща технология. Това е Load Balancer като услуга за OpenStack, наречена Octavia. Тя е основана на две проекции HAProxy, инициално предвиждаща подкрепа за peers. В тази услуга те са доказали своята отлична работа.

На картинката е схематично показано преминаването на peers-таблици между три инстанции на HAProxy, предложена е конфигурация, как това може да се настрои:

Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions
HAProxy Peers (синхронизация на сесиите)

Ако ще реализирате такава схема, работата й трябва да бъде внимателно тествана. Не е сигурно, че това ще проработи по същия начин в 100% от случаите. Но поне, вие няма да загубите stick-таблиците, когато е необходимо да запомните source IP на клиента.

Ограничение на броя на едновременното запитване от един и същи клиент

Всички услуги, които са в открит достъп, включително нашите API, могат да бъдат подложени на лавини от запитвания. Причините може да са напълно различни, от грешки на потребителите до целенасочени атаки. Периодично ни DDoS-ят по IP адреси. Клиентите често грешат в своите скриптове, правят ни мини-DDoS атаки.

Както и да е, е необходимо да предвидите допълнителна защита. Очевидното решение е да ограничите броя запитвания към API и да не губите процесорно време в обработка на злонамерени запитвания.

За реализиране на подобни ограничения прилагаме rate limits, организирани на база HAProxy, с помощта на същите stick-таблици. Лимитите се настройват доста лесно и позволяват ограничаване на потребителя по брой запитвания към API. Алгоритъмът запомня source IP, от който се правят запитванията, и ограничава броя на едновременно запитвания от един потребител. Разбира се, ние изчислихме средния профил на натоварването на API за всяка услуга и установихме лимит около 10 пъти по-висок от това значение. Все още внимателно наблюдаваме ситуацията и държим ръка на пулса.

Как изглежда това на практика? Имаме клиенти, които постоянно използват нашите API за автоматично мащабиране. Те създават около двеста-триста виртуални машини сутрин и ги изтриват вечер. За OpenStack създаването на виртуална машина, заедно с PaaS услуги, изисква поне 1000 API заявки, тъй като взаимодействието между услугите също става чрез API.

Такива прехвърляния на задачи предизвикват доста голямо натоварване. Оценихме това натоварване, събрахме дневни пикове, увеличихме ги десетократно и това стана нашият rate лимит. Държим ръка на пулса. Често виждаме ботове, скенери, които се опитват да видят дали имаме някакви CGA скриптове, които могат да бъдат стартирани, и активно ги блокираме.

Как да обновяваме кодовата база незабелязано за потребителите

Реализираме отказоустойчивост и на нивото на процесите по внедряване на кода. При разгръщанията се случват сривове, но влиянието им върху наличността на услугите може да бъде минимизирано.

Постоянно обновяваме нашите услуги и трябва да осигурим процеса по обновление на кодовата база без да влияе на потребителите. Успяхме да решим тази задача, като използвахме възможностите за управление на HAProxy и реализирахме Graceful Shutdown в нашите услуги.

За да решим тази задача, беше нужно да осигурим управление на балансировщика и „правилно“ изключване на услугите:

  • В случая с HAProxy управлението става през stats файл, който по същество е сокет и е определен в конфигурацията на HAProxy. Команди могат да му бъдат изпращани през stdio. Но основният ни инструмент за контрол на конфигурациите е Ansible, така че в него има вграден модул за управление на HAProxy, който активно използваме.
  • По-голямата част от нашите API и Engine услуги поддържат технологии за плавно изключване: при изключване те изчакват пълното завършване на текущата задача, независимо дали става въпрос за http заявка или някаква служебна задача. Същото важи и за работниците. Те знаят всички задачи, които извършват, и завършват, когато всичко е успешно завършено.

Благодарение на тези два момента, безопасният алгоритъм на нашето разгръщане изглежда по следния начин.

  1. Разработчикът събира нов пакет код (при нас това е RPM), тестват в dev среда, тестват в stage и оставя в stage репозитория.
  2. Разработчик формулира задачата за деплой с максимално подробно описание на «артефактите»: версия на новия пакет, описание на новия функционал и други подробности за деплоя, ако е необходимо.
  3. Системният администратор започва обновлението. Започва изпълнението на плейбук Ansible, който от своя страна извършва следното:
    • Взима пакета от stage-репозитория и актуализира версията на пакета в продуктовия репозиторий.
    • Съставя списък на бекендовете на обновявания сервис.
    • Изключва първия обновяван сервис в HAProxy и изчаква приключването на неговите процеси. Благодарение на плавното спиране, сме уверени, че всички текущи клиентски заявки ще завършат успешно.
    • След пълното спиране на API, worker-ите и изключването на HAProxy, се извършва обновление на кода.
    • Ansible стартира сервисите.
    • За всеки сервис активира определени «реакции», които извършват unit-тестове по редица предварително определени ключови тестове. Извършва се базова проверка на новия код.
    • Ако на предишния етап не са открити грешки, бекендът се активира.
    • Преминаваме към следващия бекенд.
  4. След обновяването на всички бекенди, се стартират функционалните тестове. Ако им липсва нещо, разработчикът преглежда всяка нова функционалност, която е създал.

На този етап деплойът е завършен.

Как се реализира отказоустойчивата уеб архитектура в платформата Mail.ru Cloud Solutions
Цикъл на обновяване на сервиса

Тази схема нямаше да бъде работеща, ако нямаше едно правило. Поддържаме в продукция едновременно старата и новата версия. Веднага, на етапа на разработка на софтуера, се предвижда, че дори при промени в базата данни на сервиса, те няма да разрушат предишния код. В резултат на това се извършва постепенен ъпдейт на кодовата база.

Заключение

Споделяйки собствените си мисли относно отказоустойчивата WEB-архитектура, искам още веднъж да подчертая нейните ключови аспекти:

  • физическа отказоустойчивост;
  • мрежова отказоустойчивост (балансьори, BGP);
  • отказоустойчивост на използвания и разработван софтуер.

На всички - стабилен uptime!

Източник: habr.com

Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри 🔥 Купете надежден хостинг за сайтове с защита от DDoS, VPS VDS сървъри | ProHoster