Привет, Хабр.

Это вторая статья в цикле, в котором мы рассматриваем архитектуру основных сервисов OpenStack. Первая, в которой я рассказывал о Nova и общих паттернах построения платформы, находится здесь. Сегодня погрузимся в детали реализации сервиса DBaaS, который в платформе носит название Trove. Мы посмотрим, как устроены основные компоненты сервиса и их взаимодействие, затронем некоторые особенности реализации механизмов безопасности, а также кратко обсудим особенности code-style.

Архитектура Trove

Функционально сервис можно разделить на 2 основных компонента: 

  • control-plane, состоящий из сервисов conductor, task manager и API; 

  • агент, для управления узлами СУБД. 

Control-plane запускается на контроллерах кластера; агент — непосредственно на клиентских инстансах.

API, как и в Nova, является несколькими тонкими слоями поверх нескольких WSGI-сервисов.

Conductor — тонкая прослойка, задача которой  проксировать сообщения между агентом и Trove. Идея сервиса conductor, как и в случае с Nova — обеспечить коммуникацию с пользовательским агентом без прямого допуска последнего к БД сервиса. Однако, если в предыдущем случае conductor отвечает и за выполнение задач, то здесь ответственность разделена и задачами занимается task manager.

Task manager принимает на себя всю рабочую нагрузку. Задачи поступают через RPC, заворачиваются в объект и запускаются в рамках этого сервиса. Таким образом достигается асинхронность.

У Trove собственный RPC, построенный по тем же принципам и на тех же технологиях, что и в Nova, но полностью обособленный от всех остальных. Подробно семантику RPC-вызовов в рамках OpenStack я описывал ранее.

Общая схема взаимодействий компонентов Nova и Trove
Общая схема взаимодействий компонентов Nova и Trove

Также Trove может взаимодействовать с другими сервисами платформы через собственные драйверы. Например, с Neutron (сетевой сервис) или Compute. Таким образом, сервис может запрашивать конкретные ресурсы, например, инстанс для размещения СУБД.

Возможности платформы

Управление реплика-сетом СУБД — основная цель сервиса. Trove поддерживает достаточно стандартный набор баз данных (datastore) для управления, при этом часть функций помечена, как work in progress.

Запуск кластера из ванильной версии сервиса доступен в экспериментальном режиме лишь для нескольких datastore. В общем случае предлагается использовать нативные механизмы репликации конкретной СУБД. Например, MariaDB использует GTID. Несмотря на это, в статье я буду использовать термин «кластер» как синоним «реплика-сета» СУБД.

CLO на текущий момент работает лишь с Mysql и Postgresql. Это обусловлено тем, что наш сервис не просто проксирует запросы в OpenStack, мы также расширяем его функциональность. Например, наша команда реализовала следующий функционал: 

  • возможность делать резервные копии отдельных БД;

  • мониторинг «здоровья» узлов кластера. Механизм который пересоздает реплика-сет если обнаружит, что один из узлов кластера перестал отвечать и обеспечивает восстановление кластера.

Мы непременно добавим и другие СУБД в ближайшем будущем.

Рассмотрим основные функции, которые Trove предоставляет пользователю.

Запуск сервиса datastore

Trove эксплуатирует сервис Nova для размещения СУБД в рамках виртуального сервера. Контроль за ресурсами инстанса остается на стороне Nova, и все дальнейшее взаимодействие с кластером control-plane осуществляет через агента. На своей стороне Trove поддерживает маппинг ID в БД Nova. Часть кода сервиса — это вспомогательные функции, которые реализуют взаимодействие с другими сервисами платформы, например, ресайза инстанса на стороне Compute.


После того, как API валидирует параметры вызова, запускается задача на создание инстанса — Trove обращается к Nova и ожидает запуска. Trove узнает о том, что datastore развернулся корректно, когда начнет получать heartbeat-сообщения от агента в conductor. В зависимости от конфигурации предполагаемой схемы, можно передать в запрос на создание инстанса ID master-ноды. В этом случае сервис сделает его снапшот и развернет slave уже из снапшота.

Компрометация агента как вектор атаки

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

Чтобы минимизировать возможный ущерб, архитектура взаимодействия с агентом выстроена следующим образом:

  1. Начиная с версии Victoria, Trove использует docker-контейнер для деплоя самого datastore. Даже в случае успешной атаки на СУБД, нужно будет еще эскалировать привилегии до родительской ОС.

  2. Как я уже писал выше, Trove имеет собственный брокер сообщений, не связанный с другими сервисами.

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

Коммуникация компонентов с ключами K1, K2, K3. Схема из блога одного из разработчиков, где он объясняет архитектуру безопасности.
Коммуникация компонентов с ключами K1, K2, K3. Схема из блога одного из разработчиков, где он объясняет архитектуру безопасности.

Эксплуатация БД в рамках datastore

При создании инстанса используется специально подготовленный образ, механизм создания в данном случае никак не отличается от создания обычной ВМ в контексте OpenStack. После того, как он развернулся, агент устанавливает связь с сервисом conductor и шлет ему heartbeat-сообщения, уведомляя о текущем состоянии: NEW, BUILDING, ACTIVE и т.д.

Управление непосредственно СУБД осуществляется с помощью сервиса manager, запущенного на инстансе. За этот функционал отвечает драйвер, код которого уникален для каждого конкретного datastore. На этом уровне настраивается репликация; устанавливаются модули, если datastore это позволяет; применяются настройки конфигурации СУБД.

В ситуации, когда мы потеряли связь с мастером, можно инициировать процесс его перевыбора. Все узлы кластера переходят в статус EJECT, и Trove пытается определить реплику с самой актуальной копией данных: он опрашивает все слейвы и сортирует их по ID последней транзакции. После того, как мастер определен, Trove переподключает и синхронизирует все узлы до состояния ACTIVE. При необходимости возможно «промоутить» конкретный инстанс в реплика-сете до мастера, для этого есть отдельный метод API.

Загрузка собственной конфигурации

Trove позволяет конфигурировать СУБД. Для этого создается отдельная сущность configuration group для каждого конкретного проекта и datastore. В дальнейшем эту сущность можно применять сразу на несколько инстансов с одинаковыми СУБД.

В целом, никакой особой магии здесь нет. Конфигурация последовательно применяется на все инстансы кластера с помощью механизмов, реализованных в рамках агента конкретного datastore: буквально устанавливая значения через CLI.

Резервное копирование и восстановление

Для выполнения задач резервного копирования Trove использует 2 отдельных docker-контейнера. Их управлением занимается manager сервис агента, запуская и останавливая контейнеры при необходимости.


В ванильной версии Trove позволяет создавать резервную копию всех кластеров проекта или непосредственно одного  кластера. Поток читается из процесса резервного копирования, например, tar и заливается в хранилище. Здесь архитектура сервиса начинает «втыкать палки». OpenStack сообщество активно продвигает свою собственную реализацию объектного хранилища — Swift. Несмотря на то, что реализация функционала конкретного хранилища наследуется от абстрактного класса, API не предполагает другого типа хранилища, ссылаясь исключительно на семантику Swift-контейнеров. Это значит, что, даже если вы захотите реализовать хранение копий в другой системе, вам придется значительно изменить API. К счастью, Ceph реализует API совместимый со Swift, что позволяет использовать его как бэкенд для этих задач.

Code-style

Первое, что сразу бросается в глаза, когда читаешь код сервиса — намного более «чистый», если позволите, стиль написания. Впрочем, это легко объясняется тем, что сервис значительно меньше той же Nova. Однако разница очень сильно чувствуется. Например, логика сервисов здесь не «протекает» из одного в другой.

Здесь вы не увидите такого нагромождения декораторов, как в Nova, однако же повсюду можно встретить решения с использованием менеджера контекста.

Большим удивлением для меня стала инициализация параметров некоторых функций mutable-элементами. Для тех, кто не знает, почему это плохо, можно почитать, например, здесь. Если кратко: такой параметр инициализируется при первом вызове функции и может быть изменен, что отразится на его состоянии при последующих вызовах. Что же, остается надеятся, что разработчики не позволяют себе модифицировать объект между вызовами.

Еще одна характерная особенность: излишняя, на мой взгляд, эксплуатация архитектурных паттернов. Тут тебе и фабрика, которая по факту является лишь красивым названием для реализации абстрактного класса, получаемого через ту же динамическую загрузку, и синглтоны через глобальные переменные (тык, почему так не стоит делать)... И, разумеется, стратегии — они здесь повсюду.

Начиная с того, что пользователю предлагают создать отдельную «стратегию» для задач резервного копирования, заканчивая огромным количеством использования этого паттерна буквально на каждом слое. Енот с циркуляркой из refactoring.guru доволен.

Заключение

Сервис Trove выглядит довольно опрятно в сравнении с монструозным Nova. Код читается легко, а функционал понятен и прост. Не обходится, однако, и без архитектурных сюрпризов. Но таков мир Open Source — есть возможность дорабатывать под свои нужды.

Вопрос к тебе, Хабраюзер: как считаешь, от чего зависит качество open source проектов и изменилось ли оно за последнее время? Проблема ли это компетенций разработчиков или мэйнтейнеров проекта?

Комментарии (0)