Хабрахабр

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

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

Так что мы постепенно пройдём через все уровни, на которых мы обнаружили риски и закрыли их. Общая отказоустойчивость платформы складывается из устойчивости её компонентов.

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

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

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

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

Наш отказоустойчивый канал связи с дата-центром оказался уязвимым в одной точке, в колодце. Например, не так давно при работах в колодце рядом с одним из дата-центров экскаватором пробили трубу, внутри этой трубы оказался как основной, так и резервный оптический кабель. Мы сделали выводы, предприняли ряд действий, в том числе проложили дополнительную оптику по соседнему колодцу. Соответственно, мы потеряли часть инфраструктуры.

В дата-центрах есть точки присутствия провайдеров связи, которым мы транслируем свои префиксы по BGP. Для каждого сетевого направления выбирается лучшая метрика, что позволяет обеспечивать разным клиентам наилучшее качество соединения. Если связь через одного провайдера отключается, мы перестраиваем свою маршрутизацию через доступных провайдеров.

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

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

Что мы используем для отказоустойчивости на уровне приложений

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

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

Мы используем его для балансировки перед всеми сервисами: базы данных, брокеры сообщений, API-сервисы, web-сервисы, наши внутренние проекты — все стоит за HAProxy. HAProxy — высоконагруженный балансировщик, позволяющий настраивать очень гибкие правила балансировки трафика на разных уровнях модели OSI.

API application — web-приложение, написанное на python, с помощью которого пользователь управляет своей инфраструктурой, своим сервисом.

Например, создание диска происходит именно в worker, а запрос на создание — в API application. Worker application (далее просто worker) — в сервисах OpenStack это инфраструктурный демон, который позволяет транслировать API-команды на инфраструктуру.

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

Большинство сервисов, которые разрабатываются под OpenStack, пытаются следовать единой парадигме. Cервис обычно состоит из 2 частей: API и worker’ы (исполнители бэкенда). Как правило, API — это WSGI-приложение на python, которое запускается либо как самостоятельный процесс (daemon), либо с помощью уже готового веб сервера Nginx, Apache. API обрабатывает запрос пользователя и передает дальнейшие инструкции на выполнение приложению worker application. Передача происходит с помощью брокера сообщений, как правило это RabbitMQ, остальные поддерживаются плохо. Когда сообщения попадают в брокер, их обрабатывают worker’ы из в случае необходимости возвращают ответ.

Зато RabbitMQ изолирован в рамках одного сервиса и по идее может быть индивидуальным для каждого сервиса. Эта парадигма подразумевает изолированные общие точки отказа: RabbitMQ и базу данных. Этот подход хорош тем, что в случае аварии в каких-то уязвимых точках ломается не весь сервис, а только его часть. Так что мы в MCS максимально разделяем эти сервисы, для каждого отдельного проекта создаём отдельную базу, отдельный RabbitMQ.

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

В этом случае используется единый центр координации, кластерная система типа Redis, Memcache, etcd, которая позволяет одному worker'у сказать другому, что эта задача закреплена за ним («ты, пожалуйста, ее не бери»). В некоторых сервисах необходима координация внутри сервиса — когда происходят сложные последовательные операции между API и worker'ами. Как правило воркеры активно общается с базой данных, пишет и читает оттуда информацию. Мы используем etcd. В качестве базы данных мы используем mariadb, которая у нас находится в мультимастер-кластере.

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

Их архитектура заслуживает отдельной статьи.В этой статье хочу сфокусироваться на отказоустойчивости API. Слабым местом во всей схеме являются RabbitMQ и MariaDB.

Архитектура Openstack Application. Балансировка и отказоустойчивость облачной платформы

Делаем балансировщик HAProxy отказоустойчивым с помощью ExaBGP

Чтобы наши API были масштабируемы, быстры и отказоустойчивы, мы поставили перед ними балансировщик. Мы выбрали HAProxy. На мой взгляд, он обладает всеми необходимыми характеристиками под нашу задачу: балансировка на нескольких уровнях OSI, интерфейс управления, гибкость и масштабируемость, большое количество методов балансировки, поддержка таблиц сессий.

Просто установка балансировщика тоже создаёт точку отказа: балансировщик ломается — сервис падает. Первая проблема, которую необходимо было решить, — отказоустойчивость самого балансировщика. Чтобы так не получалось, мы использовали HAProxy совместно с ExaBGP.

Мы использовали этот механизм для того, чтобы проверять работоспособность HAProxy и в случае проблем выключать сервис HAProxy из BGP. ExaBGP позволяет реализовать механизм проверки состояния сервиса.

Схема ExaBGP+HAProxy

  1. Устанавливаем на три сервера необходимый софт, ExaBGP и HAProxy.
  2. На каждом из серверов создаём loopback-интерфейс.
  3. На всех трёх серверах прописываем на этот интерфейс один и тот же белый IP-адрес.
  4. Белый IP-адрес анонсируется в интернет через ExaBGP.

Отказоустойчивость достигается путем анонса одного и того же IP-адреса со всех трех серверов. С точки зрения сети один и тот же адрес, доступен с трех различных next хопов. Маршрутизатор видит три одинаковых маршрута, выбирает по собственной метрике наиболее приоритетный из них (это обычно один и тот же вариант), и трафик идёт только на один из серверов.

В случае проблем с работой HAProxy или выхода сервера из строя, ExaBGP перестает аннонсировать маршрут, и трафик плавно переключается на другой сервер.

Таким образом мы добились отказоустойчивости балансировщика.

Отказоустойчивость балансировщиков HAProxy

Поэтому мы эту схему немного расширили: перешли к балансировке между несколькими белыми IP-адресами. Схема получилась неидеальной: мы научились резервировать HAProxy, но не научились распределять нагрузку внутри сервисов.

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

Остался не решен вопрос балансировки нагрузки перед нашими HAProxy. Тем не менее, решить его можно достаточно просто, как мы поступили и у себя.

Каждый из этих адресов определяется на loopback-интерфейс каждого HAProxy и анонсируется в интернет. Для балансировки трех серверов понадобится 3 белых IP-адреса и старый добрый DNS.

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

Как правило, будет выбираться только один сервер по старшинству IP-адреса, а два других будут простаивать, поскольку не указаны никакие метрики в BGP. Но так как при анонсировании белых IP-адресов мы не управляем приоритетами выбора сервера, пока это не балансировка.

Каждый балансировщик анонсирует все три белых IP-адреса, но один из них, главный для данного балансировщика, анонсируется с минимальной метрикой. Мы начали отдавать маршруты через ExaBGP с разной метрикой. Так что пока все три балансировщика в строю, обращения к первому IP-адресу попадают на первый балансировщик, обращения ко второму на второй, к третьему на третий.

При отказе любого балансировщика его основой адрес всё ещё анонсируется с двух других, трафик между ними перераспределяется. Что происходит в тот момент когда один из балансировщиков падает? Путем балансировки по DNS и разной метрики мы получаем равномерное распределение нагрузки на все три балансировщика. Таким образом, мы отдаём пользователю через DNS сразу несколько IP-адресов. И при этом не теряем отказоустойчивость.

Балансировка HAProxy на базе DNS + BGP

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

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

Это софтовая реализация взаимодействия между ExaBGP и HAProxy, когда ExaBGP использует кастомные скрипты для проверки статуса приложений. Поэтому, расширяя предыдущую схему, мы реализовали heartbeat между ExaBGP и HAProxy.

В нашем случае мы настроили health backend в HAProxy, а со стороны ExaBGP проверяем простым GET запросом. Для этого в конфиге ExaBGP необходимо настроить health checker, который сможет проверять статус HAProxy. Если анонс перестает происходить, то HAProxy, скорее всего, не работает, и анонсировать его не надо.

HAProxy Health Check

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

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

Мы хотели реализовать второй вариант. Существуют разные методы балансировки: простые, такие как round-robin, и расширенные, когда запоминается сессия клиента, и он каждый раз попадает на тот же сервер, что и раньше.

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

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

В нашем случае, это динамический адрес на loopback-интерфейсе. Для правильной работы должна быть решена проблема source IP-адреса балансировщика, с которого установлена сессия.

То есть, TCP-таймауты должны быть достаточно большими или переключение должно быть достаточно быстрое, чтобы TCP-сессия не успела оборваться. Правильная работа peers достигается лишь в определенных условиях. Тем не менее это позволяет бесшовно переключаться.

Это Load Balancer как сервис для OpenStack, который называется Octavia. У нас в IaaS есть сервис, построенный по такой же технологии. В этом сервисе они отлично себя зарекомендовали. Он основан на базе двух процессов HAProxy, в нём изначально заложена поддержка peers.

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

HAProxy Peers (синхронизация сессий)

Не факт, что это сработает в таком же виде в 100% случаев. Если вы будете реализовывать такую же схему, её работу надо внимательно тестировать. Но, по крайней мере, вы не будете терять stick-таблицы, когда нужно помнить source IP клиента.

Ограничение количества одновременных запросов с одного и того же клиента

Любые сервисы находящиеся в открытом доступе, в том числе и наши API, могут быть подвержены лавинам запросов. Причины у них могут быть совершенно разные, от ошибок пользователей, до целенаправленных атак. Нас периодически DDoSят по IP-адресам. Клиенты часто ошибаются в своих скриптах, делают нам мини-DDoSы.

Очевидным решением становится ограничивать количество запросов к API и не тратить процессорное время на обработку вредоносных запросов. Так или иначе необходимо предусмотреть дополнительную защиту.

Настраиваются лимиты достаточно просто и позволяют ограничить пользователя по количеству запросов к API. Для реализации подобных ограничений мы применяем rate limits, организованные на базе HAProxy, с помощью тех же stick-таблиц. Само собой, мы вычислили средний профиль нагрузки на API у каждого сервиса и установили лимит ≈ в 10 раз больше этого значения. Алгоритм запоминает source IP, с которого производят запросы, и ограничивает количество одновременных запросов с одного пользователя. Мы продолжаем до сих пор внимательно наблюдать за ситуацией, держим руку на пульсе.

У нас есть клиенты, которые постоянно пользуются нашими 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. После обновления всех бэкендов, запускаются функциональные тесты. Если их не хватает, то разработчик смотрит любую новую функциональность, которую он делал.

На этом деплой завершен.

Цикл обновления сервиса

Мы поддерживаем на бою одновременно старую и новую версии. Эта схема не была бы рабочей, если бы у нас не было одного правила. В результате происходит постепенное обновление кодовой базы. Заранее, на этапе разработки софта, закладывается, что даже если будут изменения в базе данных сервиса, они не будут ломать предыдущий код.

Заключение

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

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

Всем стабильного uptime!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть