Привет, хабр!
В этой серии статей я хочу рассказать об опыте написания своего виртуального «гиперскейлера» оркестратора виртуалок.
Базовые требования, от которых я отталкивался:
Гипервизоры должны работать на базе прерываемых виртуальных машин (они же spot / preemptible instances).
Система обязана быть устойчива к внезапному отзыву любого хоста.
Гиперконвергентность (HCI): storage, compute, network и control plane живут внутри одной ноды — никаких внешних сложных сетевых топологий.
Работа на commodity‑оборудовании (буквально на любом доступном хламе — без супердорогих СХД и энтерпрайз‑коммутаторов).
Линейная масштабируемость.
Почему именно прерываемые ВМ?
Такие виртуальные машины провайдеры отдают с огромной скидкой (до 70–80% от стандартной цены), что при правильном оркестрировании позволяет получить тот же объём компьюта за копейки. К тому же хосты для таких инстансов обычно крутятся на последних архитектурах процессоров и в целом свежем железе. Можно отхватить свой кусок топовых мощностей без дикой переплаты (посмотрите цены на аренду bare‑metal на базе какого-нибудь AMD EPYC Milan и сравните с такой же конфигурацией на спотах, а потом взгляните на виртуалки на базе 10-летних Xeon, которые вам еще и с оверкоммитом продадут...).
Вдобавок запуск на прерываемых виртуалках позволяет очень гибко масштабировать кластер: нажал кнопку в облачной консоли — добавил или удалил гипервизор (а можно вообще настроить автоскейлинг через instance groups). И в конце концов, запускаясь в облаке, мы «бесплатно» получаем пачку плюшек: резервирование питания, аплинков, физическую безопасность дата‑центра...
Но вместе со съедобной частью мы получаем горькую попку
В любой момент в течение 24 часов прерываемая машина может быть выключена, отозвана, стерта в пыль, аннигилирована провайдером. Причём иногда по несколько раз за сутки: например, через 4 часа, а потом ещё через 8.
Это означает, что оркестратор должен проактивно эвакуировать гостевые нагрузки на любой другой живой гипервизор в кластере. Сделать он это обязан строго до того, как хост окончательно потушат (обычно на всё про всё есть около 120 секунд с момента получения сигнала на остановку):
перенести данные с блочных устройств;
перетащить RAM;
провернуть перенос IP виртуальной машины (IP mobility), не разорвав соединения.
В общем, полностью бесшовно смигрировать гостевую виртуалку с сохранением диска, памяти и network connectivity, чтобы гость даже не заподозрил, что его куда‑то перевезли.
У меня получилось решить эту проблему, о чём я и хочу рассказать в серии статей.
Control Plane, или «мозги» системы
В центре системы — солнце. В греческой традиции — Helios, в римской — Sol Invictus. Если вы давно не думали о Римской империи, то вот вам напоминание: именно так и называется мой оркестратор.
Чтобы решить проблему постоянно отваливающихся нод и при этом сохранить линейную масштабируемость со строгими гарантиями консистентности, архитектура control plane опирается на три слоя:

1. Строгий консенсус на базе Multi‑Group Raft
Каждая отдельная логическая единица (виртуальная машина, сетевой интерфейс, блочный диск, таблица маршрутизации VPC) по сути находится в своем собственном независимом Raft‑кластере — назовем его шардом.
Для каждого ресурса выбирается 3 или 5 участников, внутри которых крутятся стандартные процедуры Raft: election, периодические хелсчеки и фиксация лога. Конечно, это создаёт оверхед, зато великолепно ограничивает радиус поражения (blast radius): упало что‑то одно — остальные живут как ни в чём не бывало. А чтобы сеть не легла от служебного трафика, на транспортном уровне вся эта Raft‑движуха пакуется и оптимизируется батчами.
2. Механизм Discovery на базе Gossip
Но чтобы сформировать Raft‑кластер, нужно сначала понять: а какие гипервизоры вообще сейчас существуют? Живы ли они? Хватит ли у них ресурсов принять реплику?
Именно для этого используется Gossip: все гипервизоры внутри кластера непрерывно перекидываются «секретиками»:
«Я нода XYZ в зоне доступности ABC, у меня свободно 4 vCPU и 16 RAM, могу делать чё хочу, законом не запрещено, а вообще я знаю ещё про ноды D, E и F — они вот тут вот».
В итоге каждая нода всегда имеет актуальную карту кластера, без единого координатора.
3. Периодический Drift Detection — великий и ужасный Reconciler
Знакомый многим паттерн из Kubernetes. Даже имея строгую консистентность через Raft и эвентуальное представление о соседях через Gossip, локально запущенные на хосте процессы — это совсем другое дело. Можно успешно создать сетевой интерфейс, а через 10 секунд операционка молча уведёт его в статус DOWN.
Reconciler непрерывно сравнивает запрашиваемое (сохраненное в Raft) состояние с актуальным положением дел на хосте и вправляет физическим ресурсам мозги, но только если это требуется.
Как логические шарды создают физические ресурсы
Связка этих трех слоев позволяет реализовать распределенные миграции виртуалок с чем‑то очень похожим на распределенные транзакции. Лидер каждого шарда после Raft election приступает к провиженингу физических ресурсов:
Лидер шарда блочного устройства записывает в локальный
/etc/drbd.d/конфигурацию Primary‑реплики, а реплики группы генерируют конфиги для Secondary (да, мы используем DRBD как основу стореджа).Лидер шарда сетевого интерфейса создает сетевой интерфейс, прописывает маршруты на хосте и поднимает его (
UP). Реплики шарда делают то же самое на резервных хостах, но держат интерфейсы опущенными (DOWN) и просто ждут своего часа.Лидер шарда виртуальной машины смотрит: диск доступен? Сетевой интерфейс готов? Если нижележащая инфра отрапортовала о полной готовности — лидер просто запускает процесс гипервизора (мы используем Cloud Hypervisor!).
Кросс‑шардное взаимодействие: подписки Parent‑Child
Как шардам общаться между собой без лагов и таймеров? Через механизм родительских подписок.
При создании шарда виртуальной машины мы явно прописываем его родителей — шарды диска и сетевого интерфейса. При этом шард виртуальной машины подключается к родительским шардам как non‑voting участник (Raft Learner):
[ Шард диска (DRBD) ] <─── non-voting подписка ──┐ ├── [ Шард ВМ (Cloud Hypervisor) ] [ Шард сети (L3/VPC) ] <─── non-voting подписка ──┘
Что это даёт? Шард ВМ гарантированно получает все обновления стейта от родителей и держит их локальную копию. Это даёт моментальную реакцию на события во время миграции: переключили диск на другой хост — шард ВМ узнает об этом мгновенно, вообще без таймеров, очередей и внешних костылей.
Из минусов — слегка повышенная нагрузка на лидера родительского шарда: при записи стейта ему нужно закоммитить данные не только в свои 3/5 реплик, но и доплюнуть их в адрес non‑voting подписчика.
В сухом остатке...
Мы получаем систему, в которой состояние кластера равномерно размазано по всем участникам.
Здесь нет централизованных хранилищ метаданных (вроде etcd в Kubernetes или Consul в Nomad) и связанных с ними боттлнеков. Нет разделения на выделенные compute / storage / network ноды с безумными схемами коммутации. При этом у нас есть механизм распределенных транзакций и мгновенных кросс‑шардовых уведомлений, что критически важно при оркестрации миграций.
Каждая нода Helios по сути совмещает в себе и control plane, и data plane. При этом средняя нода просто физически не сможет вместить столько гостевой нагрузки, чтобы data plane начал душить control plane.
Почему? Потому что объём нагрузки на ноду жестко завязан на то, сколько виртуалок мы физически успеем размигрировать с этого хоста за 120 секунд прерывания.
В этой вводной статье я описал базовые принципы и фундамент оркестратора. В следующей части мы по косточкам разберем живую миграцию виртуальной машины и всю хореографию вокруг неё.