Как устроены сервисы управляемых баз данных в Яндекс.Облаке

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

Меня зовут Владимир Бородин, я руководитель платформы данных Яндекс.Облака. Сегодня я хочу рассказать вам, как всё устроено и работает внутри сервисов Yandex Managed Databases, почему всё сделано именно так и в чём преимущества — с точки зрения пользователей — тех или иных наших решений. И конечно, вы обязательно узнаете, что мы планируем доработать в ближайшее время, чтобы сервис стал лучше и удобнее для всех, кому он нужен.

Что ж, поехали!

image

Управляемые базы данных (Yandex Managed Databases) — один из самых востребованных сервисов Яндекс.Облака. Точнее, это целая группа сервисов, которая по популярности сейчас уступает только виртуальным машинам Yandex Compute Cloud.

Yandex Managed Databases даёт возможность достаточно быстро получить работающую базу данных и берет на себя вот такие задачи:

  • Масштабирование — от элементарной возможности добавить вычислительные ресурсы или пространство на диске до увеличения количества реплик и шардов.
  • Установку обновлений, минорных и мажорных.
  • Резервное копирование и восстановление.
  • Обеспечение отказоустойчивости.
  • Мониторинг.
  • Предоставление удобных средства настройки и управления.

Как устроены сервисы управляемых баз данных: вид сверху


Сервис состоит из двух основных частей: Control Plane и Data Plane. Control Plane — это, упрощенно говоря, API для управления базами данных, который позволяет создать, изменить или удалить БД. Data Plane — это уровень непосредственного хранения данных.

image

У пользователей сервиса есть, по сути, две точки входа:

  • В Control Plane. На самом деле входов много — Web-консоль, CLI-утилита и API gateway, предоставляющий публичный API (gRPC и REST). Но все они в конечном счёте ходят в то, что мы называем Internal API, а потому будем считать это одной точкой входа в Control Plane. Фактически это точка, с которой начинается зона ответственности сервиса Managed Databases (MDB).
  • В Data Plane. Это уже непосредственное подключение к запущенной базе данных через протоколы доступа к СУБД. Если речь идет, например, о PostgreSQL, то это будет интерфейс libpq.


image
Ниже мы подробнее опишем всё, что происходит в Data Plane, и разберём каждый из компонентов Control Plane.

Data Plane


Прежде чем рассматривать компоненты Control Plane, рассмотрим происходящее в Data Plane.

Внутри виртуальной машины


MDB запускает базы данных в таких же виртуальных машинах, которые предоставляются в Yandex Compute Cloud.

image

Прежде всего там развёрнут движок базы данных, допустим, PostgreSQL. Параллельно могут быть запущены различные вспомогательные программы. Для PostgreSQL это будет Odyssey — пулер соединений к базе данных.

Также внутри виртуальной машины запускается некий стандартный набор сервисов, свой для каждой СУБД:

  • Сервис для создания резервных копий. Для PostgreSQL это инструмент с открытым программным кодом WAL-G. Он создаёт резервные копии и складывает их в Yandex Object Storage.
  • Salt Minion — компонент системы SaltStack для выполнения операций и управления конфигурациями. Подробнее о нём — ниже, в описании Deploy-инфраструктуры.
  • MDB metrics, который отвечает за передачу метрик БД в Yandex Monitoring и в наш микросервис для отслеживания состояния кластеров и хостов MDB Health.
  • Push client, который отправляет логи СУБД и логи биллинга в сервис Logbroker — специальное решение для сбора и поставки данных.
  • MDB cron — наш велосипед, отличающийся от обычного cron умением выполнять периодические задания с точностью до секунды.

Сетевая топология


image

Каждый хост Data Plane имеет два сетевых интерфейса:

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


Да, в одну такую managed-сеть втыкаются хосты разных клиентов, но это не страшно, потому что на managed-интерфейсе (почти) ничто не слушает, с него только открываются исходящие сетевые соединения в Control Plane. Почти никто, потому что есть открытые порты (например, SSH), но они закрыты локальным файрволом, разрешающим соединения только с конкретных хостов. Соответственно, если злоумышленник получит доступ к виртуальной машине с базой, дотянуться до чужих баз данных он не сможет.

Безопасность Data Plane


Раз уж речь зашла про безопасность, надо сказать, что сервис мы изначально проектировали из расчёта получения злоумышленником root на виртуальной̆ машине кластера.

В итоге мы вложили много сил, чтобы сделать следующее:

  • Локальный и большой файрвол;
  • Шифрование всех соединений и бэкапов;
  • Всё с аутентификацией и авторизацией;
  • AppArmor;
  • Самописную IDS.


Теперь рассмотрим компоненты Control Plane.

Control Plane


Internal API


Internal API — первая точка входа в Control Plane. Давайте посмотрим, как тут все работает.

image

Допустим, в Internal API поступает запрос на создание кластера базы данных.

В первую очередь Internal API обращается к сервису облака Access service, который отвечает за проверку аутентификации и авторизации пользователя. Если пользователь проходит проверку, Internal API проверяет на валидность сам запрос. Например, запрос на создание кластера без указания его имени или с уже занятым именем проверку не пройдет.

А еще Internal API умеет отправлять запросы в API других сервисов. Если вы хотите создать кластер в некой сети А, а конкретный хост в определённой подсети B, Internal API должен удостовериться, что у вас есть права и на сеть A, и на указанную подсеть B. Заодно будет проведена проверка, что подсеть B принадлежит сети A. Для этого и нужен доступ к API инфраструктуры.


Если запрос валиден, информация про создаваемый кластер будет сохранена в метабазу. Мы называем её MetaDB, она развёрнута на PostgreSQL. В MetaDB есть таблица с очередью операций. Internal API сохраняет информацию об операции и ставит задачу транзакционно. После этого пользователю возвращается информация об операции.

В общем-то для обработки большинства запросов Internal API достаточно походов в MetaDB и API смежных сервисов. Но есть ещё два компонента, в которые Internal API ходит для ответов на некоторые запросы — LogsDB, где лежат логи пользовательских кластеров, и MDB Health. Про каждый из них будет подробнее написано ниже.

Worker


Workers — это просто набор процессов, которые опрашивают очередь операций в MetaDB, хватают их и выполняют.

image

Что именно делает worker в случае создания кластера? Сначала обращается к API инфраструктуры для создания виртуальных машин из наших образов (в них уже установлены все необходимые пакеты и настроено большинство вещей, образы обновляются раз в сутки). Когда виртуальные машины созданы и в них взлетела сеть, worker обращается к Deploy-инфраструктуре (подробнее про неё расскажем дальше), чтобы она развернула на виртуальных машинах то, что нужно пользователю.

Помимо этого worker обращается и к другим сервисам Облака. Например, к Yandex Object Storage для создания бакета, в который будут сохраняться резервные копии кластера. К сервису Yandex Monitoring, который будет собирать и визуализировать метрики БД. Worker должен создать там метаинформацию про кластер. К DNS API, если пользователь хочет назначить публичные IP-адреса хостам кластера.

В целом worker работает очень просто. Он получает задачу из очереди метабазы и обращается к нужному сервису. После выполнения каждого шага worker сохраняет в метабазу информацию о прогрессе операции. Если происходит сбой, задача просто перезапускается и выполняется с того места, на котором остановилась. Но даже перезапустить её с самого начала — не проблема, потому что практически все типы задач для workers написаны идемпотентно. Так сделано потому, что worker может тот или иной шаг операции выполнить, но в MetaDB информацию про это не донести.

Deploy-инфраструктура


В самом низу используется SaltStack, довольно распространённая система управления конфигурациями с открытым исходным кодом, написанная на Python. Система очень расширяемая, за что мы её и любим.

Основными компонентами salt являются salt master, хранящий информацию о том, что и куда должно быть применено, и salt minion — агент, который ставится на каждый хост, взаимодействует с мастером и умеет непосредственно применять к хосту полученное с salt-мастера. Для целей этой статьи нам этого знания хватит, а подробнее можно почитать в документации SaltStack.

Один salt master не отказоустойчив и не масштабируется на тысячи миньонов, мастеров нужно несколько. Взаимодействовать с этим напрямую из worker’а неудобно, и мы написали свою обвязку над Salt, которую мы называем инфраструктурой Deploy.

image

Для worker единственной точкой входа является Deploy API, который реализует методы вида «Примени весь state целиком или отдельные его кусочки на такие-то миньоны» и «Расскажи статус вот такой-то выкатки». Deploy API сохраняет информацию про все выкатки и конкретные её шаги в DeployDB, где мы тоже используем PostgreSQL. Там же хранится информация обо всех миньонах и мастерах и о принадлежности первых ко вторым.

На salt-мастерах устанавливаются два дополнительных компонента:

  • Salt REST API, с которым и взаимодействует Deploy API для запуска выкаток. REST API ходит в локальный salt-мастер, а тот уже по ZeroMQ общается с миньонами.
  • Сущность, что ходит в Deploy API и получает публичные ключи всех миньонов, которые должны быть подключены к этому salt-мастеру. Без публичного ключа на мастере миньон просто не сможет подключиться к мастеру.


В Data Plane кроме salt minion тоже установлено два компонента:

  • Returner — модуль (одна из расширяемых частей в salt), который доносит результат выкатки не только до salt-мастера, но и в Deploy API. Deploy API инициирует deploy походом в REST API на мастере, а результат получает через returner от миньона.
  • Master pinger, который периодически опрашивает Deploy API, к какому мастеру должны быть подключены миньоны. Если Deploy API возвращает новый адрес мастера (например, потому что старый умер или перегружен), pinger производит переконфигурацию миньона.


Ещё одним местом, в котором мы используем расширяемость SaltStack, является ext_pillar — возможность откуда-то извне получить pillar (некоторую статическую информацию, например, конфигурацию PostgreSQL, пользователей, баз данных, расширений и т.п.). Мы из нашего модуля ходим в Internal API, чтобы получить специфичные для кластера настройки, так как они хранятся в MetaDB.

Отдельно заметим, что pillar содержит в том числе и конфиденциальную информацию (пароли пользователей, TLS-сертификаты, GPG-ключи для шифрования бэкапов), а потому, во-первых, всё взаимодействие между всеми компонентами осуществляется с шифрованием (ни в одну нашу базу нельзя прийти без TLS, HTTPS повсюду, миньон с мастером тоже шифруют весь трафик). А во-вторых, все указанные секреты лежат в MetaDB зашифрованными, и мы применяем разделение секретов — на машинах Internal API лежит публичный ключ, которым шифруются все секреты перед сохранением в MetaDB, а на salt-мастерах лежит приватная его часть и только они могут получить секреты в открытом виде для передачи в качестве pillar на миньон (опять же по зашифрованному каналу).

MDB Health


При работе с базами данных полезно знать их состояние. Для этого у нас есть микросервис MDB Health. Он получает от внутреннего компонента виртуальной машины MDB metrics информацию о статусе хоста и сохраняет в собственной базе (в данном случае Redis). А когда в Internal API приходит запрос о состоянии конкретного кластера, Internal API использует данные из MetaDB и MDB Health.

image

Информация по всем хостам обрабатывается и отдаётся в понятном виде в API. Помимо состояния хостов и кластеров для некоторых СУБД MDB Health дополнительно возвращает, является ли конкретный хост мастером или репликой.

MDB DNS


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

image

Как это происходит? Как мы уже говорили выше, внутри виртуальной машины есть MDB cron, который периодически передает в MDB DNS heartbeat примерно следующего содержания: «В данном кластере CNAME-запись должна указывать на меня». MDB DNS принимает такие сообщения со всех виртуальных машин и принимает решение о необходимости смены CNAME-записей. Если необходимость есть, он через DNS API меняет запись.

Почему мы сделали отдельный сервис для этого? Потому что у DNS API управление доступом разграничивается только на уровне зон. Потенциальный злоумышленник, получив доступ к отдельной виртуальной машине, мог бы менять CNAME-записи других пользователей. MDB DNS исключает такой сценарий, потому что проверяет авторизацию.

Доставка и отображение логов базы данных


Когда база данных на виртуальной машине делает запись в лог, специальный компонент push client читает эту запись и отправляет только что появившуюся строку в Logbroker (про него на Хабре уже писали). Взаимодействие push client с LogBroker построено с семантикой exactly-onсe: обязательно отправим и обязательно строго один раз.

Отдельный пул машин — LogConsumers — забирает логи из очереди LogBroker и сохраняет в базе данных LogsDB. Для базы данных логов используется СУБД ClickHouse.

image

Когда в Internal API приходит запрос на вывод логов за определенный интервал времени для конкретного кластера, Internal API проверяет авторизацию и отправляет запрос в LogsDB. Таким образом, контур доставки логов полностью независим от контура отображения логов.

Биллинг


Схема биллинга построена похожим образом. Внутри виртуальной машины есть компонент, который с определенной периодичностью проверяет, что с базой данных всё в порядке. Если всё хорошо, можно проводить биллинг за этот интервал времени с момента последнего запуска. В этом случае делается запись в billing log, а дальше push client отправляет запись в LogBroker. Данные из Logbroker передаются в систему биллинга и там проводятся расчёты. Это схема биллинга для запущенных кластеров.

Если же кластер выключен, перестаёт тарифицироваться использование вычислительных ресурсов, однако взимается плата за дисковое пространство. В этом случае биллить из виртуальной машины невозможно и задействуется второй контур — контур offline-биллинга. Существует отдельный пул машин, выгребающих список выключенных кластеров из MetaDB и пишущих лог в том же формате в Logbroker.

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

image

Создание резервных копий


Схема создания резервных копий может несколько отличаться для разных СУБД, но общий принцип всегда одинаков.

Для каждого движка базы данных используется свой собственный инструмент создания бэкапов. Для PostgreSQL и MySQL это WAL-G. Он создаёт резервные копии, сжимает их, шифрует и складывает в Yandex Object Storage. При этом каждый кластер размещается в отдельном бакете (во-первых, для изоляции, а во-вторых, чтобы было проще биллить место под бэкапы) и шифруется своим собственным ключом шифрования.

image

Вот так устроены Control Plane и Data Plane. Из всего это и складывается сервис управляемых баз данных Яндекс.Облака.

Почему всё устроено именно таким образом


Безусловно, на глобальном уровне что-то можно было реализовать по более простым схемам. Но у нас были свои причины, чтобы не идти по пути наименьшего сопротивления.

Прежде всего, мы хотели иметь общий Control Plane для всех типов СУБД. Неважно, какую вы выбрали, в конце концов ваш запрос приходит в один и тот же Internal API и все компоненты под ним тоже общие для всех СУБД. Это несколько усложняет нам жизнь с точки зрения технологий. С другой стороны, так намного проще вводить новые функции и возможности, затрагивающие все СУБД. Это делается один раз, а не шесть.

Второй важный для нас момент — мы хотели обеспечить независимость Data Plane от Control Plane настолько, насколько это возможно. И сегодня, даже если Control Plane будет полностью недоступен, все базы данных продолжат работать. Сервис будет обеспечивать их надёжность и доступность.

В-третьих, разработка практически любого сервиса — всегда компромисс. В общем смысле, если говорить грубо, где-то важнее скорость выпуска релизов, а где-то дополнительная надёжность. При этом сейчас никто не может позволить себе делать один или два релиза в год, это очевидно. Если посмотреть на Control Plane, здесь мы делаем упор на скорость разработки, на быстрый ввод новых возможностей, выкатывая обновления несколько раз в неделю. А Data Plane отвечает за сохранность ваших баз данных, за отказоустойчивость, поэтому здесь совсем другой цикл выпуска релизов, измеряемый уже неделями. И вот эту гибкость в плане разработки тоже обеспечивает нам их взаимная независимость.

Ещё один пример: обычно сервисы управляемых баз данных предоставляют пользователям только сетевые диски. Яндекс.Облако предлагает ещё и локальные диски. Причина проста: их скорость работы намного выше. С сетевыми дисками, например, проще масштабировать виртуальную машину вверх и вниз. Проще делать бэкапы в виде снапшотов (snapshots) сетевых хранилищ. Но многим пользователям нужна высокая скорость, поэтому мы делаем средства резервного копирования уровнем выше.

Планы на будущее


И пару слов о планах по улучшению сервиса на среднесрочную перспективу. Это планы, которые затрагивают весь Yandex Managed Databases в целом, а не отдельные СУБД.

В первую очередь мы хотим дать больше гибкости по настройке периодичности создания бэкапов. Бывают сценарии, когда необходимо, чтобы в течение дня резервные копии делались раз в несколько часов, в течение недели — раз в сутки, в течение месяца — раз в неделю, в течение года — раз в месяц. Для этого мы разрабатываем отдельный компонент между Internal API и Yandex Object Storage.

Другой важный момент, важный и для пользователей, и для нас, — скорость выполнения операций. Недавно мы провели серьёзные изменения в Deploy-инфраструктуре и сократили время выполнения почти всех операций до нескольких секунд. Не охваченными остались только операции создания кластера и добавления хоста в кластер. Время выполнения второй операции зависит от объёма данных. А вот первую мы и будем ускорять в ближайшее время, потому что пользователи часто хотят создавать и удалять кластеры в своих CI/CD пайплайнах.

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

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

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

© Habrahabr.ru