Як реалізується стійка до відмови веб-архітектура в платформі 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-сервіси, web-сервіси, наші внутрішні проекти – все за HAProxy.

API application — web-додаток, написаний на python, за допомогою якого користувач керує своєю інфраструктурою, своїм сервісом.

Worker application (Далі просто worker) - в сервісах OpenStack це інфраструктурний демон, який дозволяє транслювати API-команди на інфраструктуру. Наприклад, створення диска відбувається саме у worker, а запит на створення - в API application.

Стандартна архітектура OpenStack Application

Більшість сервісів, які розробляються під OpenStack, намагаються слідувати єдиній парадигмі. Сервіс зазвичай складається з 2 частин: API та worker'и (виконавці бекенду). Як правило, API - це WSGI-додаток на python, який запускається або як самостійний процес (daemon), або за допомогою вже готового веб-сервера Nginx, Apache. API обробляє запит користувача і передає подальші інструкції виконання додатку worker application. Передача відбувається за допомогою брокера повідомлень, як правило, це RabbitMQ, інші підтримуються погано. Коли повідомлення потрапляють у брокер, їх обробляють worker'и з у разі потреби повертають відповідь.

Ця парадигма має на увазі ізольовані загальні точки відмови: RabbitMQ та базу даних. Натомість RabbitMQ ізольований в рамках одного сервісу і, за ідеєю, може бути індивідуальним для кожного сервісу. Отже, ми в MCS максимально розділяємо ці сервіси, для кожного окремого проекту створюємо окрему базу, окремий RabbitMQ. Цей підхід хороший тим, що у разі аварії в якихось уразливих точках ламається не весь сервіс, а лише його частина.

Кількість worker application нічим не обмежена, тому API може легко масштабуватись горизонтально за балансувальниками з метою збільшення продуктивності та відмовостійкості.

У деяких сервісах необхідна координація всередині сервісу, коли відбуваються складні послідовні операції між API та worker'ами. У цьому випадку використовується єдиний центр координації, кластерна система типу Redis, Memcache, etcd, яка дозволяє одному worker'у сказати іншому, що ця задача закріплена за ним («ти, будь ласка, її не бери»). Ми використовуємо etcd. Як правило, воркери активно спілкується з базою даних, пише і читає звідти інформацію. Як база даних ми використовуємо 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 хопів. Маршрутизатор бачить три однакових маршруту, вибирає за власною метрикою найпріоритетніший з них (це зазвичай один і той же варіант), і трафік йде лише на один із серверів.

У разі проблем з роботою 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 таблиці сесій.

Існують різні методи балансування: прості, такі як кругової, і розширені, коли запам'ятовується сесія клієнта, і він щоразу потрапляє на той самий сервер, що й раніше. Ми хотіли реалізувати другий варіант.

У HAProxy для збереження сесій клієнта цього механізму використовується stick-tables. Вони зберігають вихідну IP-адресу клієнта, обрану таргет-адресу (бекенд) та деяку службову інформацію. Зазвичай stick-таблиці використовуються для збереження пари source-IP + destination-IP, що особливо корисно для програм, які не можуть передавати контекст сесії користувача при перемиканні на інший балансувальник, наприклад, в режимі балансування RoundRobin.

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

Для правильної роботи має бути вирішена проблема source 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 підтримують технології graceful shutdown: при вимиканні вони чекають повного завершення поточного завдання, будь то http-запит або якесь службове завдання. Те саме відбувається з worker'ом. Він знає всі завдання, що робить, і завершується, коли все успішно доробив.

Завдяки цим двом моментам, безпечний алгоритм нашого деплою виглядає так.

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

На цьому деплою завершено.

Як реалізується стійка до відмови веб-архітектура в платформі Mail.ru Cloud Solutions
Цикл оновлення сервісу

Ця схема не була б робочою, якби ми не мали одного правила. Ми підтримуємо на бою одночасно стару та нову версії. Заздалегідь, на етапі розробки софту закладається, що навіть якщо будуть зміни в базі даних сервісу, вони не будуть ламати попередній код. В результаті відбувається поступове оновлення кодової бази.

Висновок

Ділячись власними думками щодо відмовостійкої WEB-архітектури, хочу ще раз відзначити її ключові моменти:

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

Всім стабільним uptime!

Джерело: habr.com

Додати коментар або відгук