
Привет! Я Сергей Самойлов — техлид направления слоя Control Plane для блочных устройств. В этой статье рассмотрим модель реконсиляции ресурсов в облаке на примере блочных устройств. Мы рассмотрим, что такое реконсиляция, когда она применяется и как это всё выглядит в MWS.
Начнём с начала
Прежде чем переходить к самому механизму реконсиляции, давайте разберём, с какими трудностями мы сталкиваемся. Так гораздо лучше получится передать мотивацию.
Итак, у нас есть облако. Любое облако делится на несколько частей, как слоёный пирог.

SaaS (Software as a Service) — готовые сервисные решения. Если вам нужно какое-то приложение и вам не хочется думать, что под капотом, то вам сюда. Пользователям SaaS-приложения абсолютно не важно, сколько нод в кластере Postgres или как осуществляется репликация данных в Kafka. Все действия происходят поверх уже готовых и настроенных технологий.
Paas (Platform as a Service) — система уже более низкого уровня, на основе которой мы можем построить своё приложение. Здесь мы уже можем создать и настроить кластер Kafka, Kubernetes, Postgres и т. д. И уже поверх них строить своё приложение.
IaaS (Infrastructure as a Service) — слой управления дисками, сетью и вычислениями. Именно сюда попадают запросы от пользователей, которые хотят развернуть собственное решение в обход PaaS. И так как я из команды Storage, давайте разберём архитектуру системы на нашем примере.
Архитектура блочных устройств в облаке
Архитектурно мы разбиваем систему на два слоя: Data Plane — слой управления потоками данных — и Control Plane — слой управления объектами в Data Plane.

Схема представляет собой высокоуровневое описание системы. Сверху идёт взаимодействие с API. При этом запросы могут поступать как от пользователей, так и от PaaS-слоя. Снизу расположены Kubernetes-ноды, где и разворачиваются виртуальные машины, которые видит пользователь. Там же находятся наши агенты — приложения, которые опрашивают текущее состояние в Control Plane и корректируют логику взаимодействия виртуальных машин с дисками. По центру находится сам Control Plane. Давайте рассмотрим его подробнее.
Как видно из схемы, он отвечает за 3 основных действия в системе:
-
Внешний API и валидации.
-
Взаимодействие с агентами для управления ресурсами.
-
Бизнес-логика. Диски у нас бывают разных типов со своим жизненным циклом и различными переходами по состояниям.
Схема может показаться достаточно сложной, но на самом деле в ней многое не учитывается. Давайте заглянем в более детальную схему.

Эта схема даёт более подробное объяснение, но всё ещё для простоты указано не всё: авторизация, взаимодействия с compute (вычислительной системой, которая отвечает за создание виртуальных машин), разный набор ceph-кластеров, шифрование и многое другое. Кроме того, всё усугубляется ещё и тем, что IaaS-системы сильно связаны друг с другом. К примеру, нам не особо интересно создавать диски без возможности подключения их к виртуальной машине. Верно и обратное: виртуальные машины без дисков и сети — не самое работоспособное решение.
Управление ресурсами
В такой системе выстраивается граф зависимостей как внутри одной системы, так и взаимосвязи с другими. Предлагаю посмотреть на очень упрощённую схему зависимостей ресурсов.

Как же управлять таким графом? На каждый узел возлагается большая ответственность. Зачастую нам недостаточно просто обновить узел и забыть. Обычно мы хотим, чтобы изменение одного узла провоцировало изменение или хотя бы уведомление всей ветви зависимостей. Ресурсу не легко. И вот здесь мы приходим к реконсиляции и декларативному API.

Начнём с декларативного API
Ключевой особенностью декларативного API является возможность вносить в систему более чёткие правила по управлению ресурсами. В случае императивного API нам пришлось бы проходить по графу ресурсов и самостоятельно приводить каждый ресурс к нужному состоянию. В декларативном API пользователь сам передаёт конечное ожидаемое состояние.

Понятие декларативного API легче всего понять на примере. Если вы работали с Kubernetes или подобными системами, то уже наверняка сталкивались с таким понятием. Посмотрим простой вымышленный пример:

Здесь изначально клиент задаёт то, что он хочет получить. При этом, оставив размер диска пустым, он указывает, что ему не важен этот размер. Вместо этого он говорит: «Я хочу какой-то диск с Ubuntu:24.04!» Мы, увидев это, знаем, что минимальный размер диска для данного образа — 6 ГБ. Именно такой размер мы задаём в статусе. При этом спецификация диска остаётся неизменной. Через какое-то время мы успешно создали диск, и пользователь может легко увидеть это, посмотрев в ресурс. Это уберегает нас от сложности управления ресурсами в императивном стиле. При тесной взаимосвязи систем следить за зависимостью ресурсов — задача крайне непростая. Лёгкое дуновение ветра в виде непродуманного корнер-кейса в логике — и мы можем уйти в рекурсию без условия выхода. Предлагаю рассмотреть такой пример:

Здесь наглядно можно увидеть, что взаимодействие ресурсов представляет собой рекурсивную зависимость ресурсов друг от друга. Конечно, если наша логика написана идеально и обработка виртуальной машины происходит всегда идеальным образом, то всё будет хорошо. Но во-первых, баги вероятны, во-вторых, такую логику тяжело поддерживать и тестировать. На картинке всего один сценарий, но в реальной жизни их могут быть десятки или даже сотни. И все их придётся согласовывать друг с другом.
Как устроена реконсиляция в нашем облаке
И вот мы, наконец, подходим к теме из заголовка статьи. Своими словами, реконсиляция — это механизм приведения состояния ресурса к заданному клиентом состоянию. Разбор примера с диском и есть реконсиляция. Говоря более строго, реконсиляция ресурса — это процесс приведения состояния ресурса (status) к ожидаемому пользователем (spec).
Процесс реконсиляции включает в себя:
— Создание и удаление зависимых ресурсов облака.
— Использование и подписку на изменения связанных ресурсов облака.
— Управление и реакцию на обратную связь от физических ресурсов.
Гораздо более интересно — написание кода, который будет поддерживать такую модель управления ресурсами. Давайте рассмотрим, как мы можем это сделать:

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

Как видно из рисунка с котиками выше, алгоритм действий следующий:
-
Пользователь делает запрос.
-
В рамках сессии пользователя задача об изменении ресурса кладётся в базу данных.
-
Через какое-то время приходит таск-процессор и начинает выполнять задачу.
Весь этот механизм построен на небольшой самописной очереди поверх Postgres.
Пару слов о таск-процессоре
Таск-процессор — строительный блок для организации распределённой работы множества подсистем облака.
Роль таск-процессора:
-
Взять задачу из базы данных.
-
Запустить цикл реконсиляции (о чём ещё будет далее).
При этом поддерживаются гарантии:
-
Задача должна быть запущена как минимум один раз (at least once).
-
Задача не должна быть перезапущена до завершения предыдущего запуска (one at a time).
Итак, таск-процессор нужен для того, чтобы запустить реконсиляции, или, если более точно, — запустить цикл реконсиляции.
Цикл реконсиляции
Чтобы понять, как мы поддерживаем представленные гарантии, предлагаю кратко посмотреть на то, из чего состоит цикл реконсиляции:

Давайте разберём, что здесь происходит:
-
В рамках транзакции мы берём задачу на выполнение. Эта транзакция работает с select for update, что позволяет избежать ситуации запуска нескольких циклов реконсиляции по одной задаче. Осуществляется это путём выставления следующего время старта (тайм-аута) задачи, до наступления которого никто не может повторно выполнить действие. Здесь же происходит вычисление следующего шага реконсиляции.
-
Выполнение задачи. Почему это отдельный шаг? Дело в том, что это может быть потенциально не быстрая операция. Выполнять её в рамках транзакции в базу было бы опасно.
-
Транзакция на закрытие задачи. Зачем это нужно? Таким образом мы фиксируем в базе результат выполнения задачи: успешный он или нет, какая произошла ошибка и т. д. Это позволит через какое-то время взять все зависшие задачи (задачи, которые были взяты, но не закрыты) и попробовать их выполнить снова.
Здесь важно отметить, что «действие» не детерминировано изначально в задаче, а рассчитывается в каждом цикле.
Что происходит при выполнении задачи
Выполнение задачи — по сути, это и есть основная логика приложения. В рамках неё мы можем передвигать по статусу ресурс в этом же сервисе или даже создавать, менять или удалять дочерний ресурс в другом сервисе.
Попробую показать схематично на рисунке:

Здесь важно обратить внимание на сервис Global Control Plane. Именно в нём в рамках выполнения задачи мы идём и создаём диск в определённой зоне доступности — дата-центре.
Нотификации клиентам
Следующий логичный вопрос, который может возникнуть: а как при такой схеме клиент-серверного взаимодействия клиент поймёт, что на сервере обновился дочерний ресурс. Проще говоря, как нам понять на глобальном уровне, ближайшем к пользователю, что диск в конкретном дата-центре был успешно создан.

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

Разберём данный рисунок подробнее по шагам:
Шаг 1. Клиент создаёт ресурс на стороне сервера. При этом мы уведомляем сервер о том, что вновь созданный ресурс является дочерним к уже существующему.
Шаг 2. По инициативе третьей стороны новый ресурс изменился. На этом шаге мы понимаем, что данный ресурс является зависимым. Соответственно, требуется уведомление родительского ресурса об этом изменении. Для этого мы фиксируем событие в базе данных и временно забываем о нём.
Шаг 3. В общем случае в процесс включается специальный обработчик событий (но могут быть и исключения, которые мы сейчас опустим). И вот тут самое интересное. Вместо того чтобы передать клиенту сразу информацию об изменении, мы уведомляем о том, что изменение существует. При этом важным моментом здесь является то, что сессия не закрывается. Вместо этого клиент в рамках запроса к нему, пока сессия жива, делает запрос к серверу и забирает обновлённый ресурс. И только после этого сессия владельца ресурса закрывается.
Заключение
Надеюсь, в этой статье мне удалось передать общую идею. Множество вопросов остались не раскрыты: каким образом происходит управление транзакциями, как реализована собственная очередь и многое другое. Кроме того, мы рассмотрели модель реконсиляции исключительно для CPL — Control Plane-сервисов, которые написаны на Kotlin. Есть и Data Plane, и схема взаимодействия там другая. Тем не менее, мне было очень интересно попробовать уместить огромную тему в небольшую статью. Если тема окажется интересной читателям, то, скорее всего, в ближайшем будущем мы напишем цикл статей от меня или моих коллег.
ссылка на оригинал статьи https://habr.com/ru/articles/895390/
Добавить комментарий