
Привет, Хабр!
За последние три года рекламная система VK выросла в три раза по количеству кампаний, таргетингов и RPS. При этом мы столкнулись с физическими ограничениями bare-metal инфраструктуры: 128 CPU и 512 ГБ памяти на сервер стали потолком, в который мы упёрлись. Сервис баннерной карусели потреблял всё больше ресурсов, а время деплоя достигало 24 часов.
Меня зовут Артём Букин, я разрабатываю инфраструктурные проекты. В этой статье расскажу о технических деталях миграции ядра рекламной системы VK в облако: как перешли от MySQL-репликации к P2P-доставке снапшотов через торрент-протокол, научились применять данные без downtime и в итоге сократили потребление памяти в 2 раза, а время деплоя — в 4.
Масштабы и ограничения
Рекламная система VK обрабатывает около миллиона запросов в секунду на фронтах, из которых примерно 500 тысяч доходит до баннерной карусели — сервиса подбора рекламы. За последние три года количество рекламных кампаний выросло в три раза, количество таргетингов тоже утроилось, что привело к кубическому росту нагрузки: RPS × количество кампаний × количество таргетингов.
Ограничения на железе:
Рост единицы развёртывания по памяти.
Дорого держать «железо» под пик нагрузки.
Рост стоимости подбора рекламы.
Рост времени задержки подбора.
Долгая выкладка обновлений.
Доставка по libslave.
При этом у нас есть жёсткое ограничение — подбор рекламы должен укладываться в 300 миллисекунд, а в большинстве случаев даже в 150. Нельзя просто взять и увеличить время обработки — это критично для пользовательского опыта и конверсии рекламы.
На bare-metal мы столкнулись с пределами аппаратных возможностей. При этом баннерный движок держал два слепка данных в памяти: для обработки запросов и для применения обновлений в реальном времени.
С ростом объёма данных наш прод вырос почти в 10 раз, до 2500 серверов с запасом 60% под пиковые нагрузки. К началу 2024 года трёхкратный рост всех метрик привёл к тому, что дальнейшее масштабирование на железе стало экономически нецелесообразным.
Время выкладки новой версии достигло 24 часов. Это неприемлемо долго для современной разработки, где нужно быстро выкатывать фичи и исправления. А главная архитектурная проблема заключалась в использовании протокола MySQL slave для доставки данных — при росте до 100-150 реплик это создавало критическое узкое место.
Архитектура на железе
Баннерный движок находится в центре всей рекламной системы. Он имеет монолитную архитектуру и включает в себя механизмы работы с ML-моделями, форматирование выдачи и интеграцию с профилями пользователей. Вокруг него выстроена экосистема сервисов: хранение баннеров, сбор статистики и профилей пользователей, хранилище ML-моделей, аукционы.
Исторически все данные поступали в master MySQL, а баннерные демоны подключались к нему через libslave — библиотеку, которая позволяла демону представляться MySQL-репликой. По протоколу репликации мы напрямую получали все апдейты. Это работало, пока система была относительно небольшой.
С ростом системы баннерные демоны разделились на кластеры, каждому из которых требовались свои реплики. В пике у нас было до 150 реплик MySQL, которые постоянно тянули обновления с мастера и раздавали их на свои кластеры баннерных. При нештатных ситуациях или пиковых нагрузках происходил перекос обращений к репликам, что могло положить весь кластер БД. Выкладка большого количества инстансов при первоначальной загрузке создавала такую нагрузку на БД, что могла положить и реплики, и даже мастер.
Зачем в One-cloud?
Переход на единую облачную платформу One-cloud был обусловлен необходимостью. Основные драйверы миграции:
Экономика и гибкость. Содержание 2500 серверов с запасом 60% под пики — дорогое удовольствие. В едином облаке мы можем более плотно утилизировать ресурсы и масштабироваться по требованию. При унифицированной инфраструктуре снижаются издержки на содержание железа.
Скорость разработки. При унификации инфраструктуры мы становимся ближе к сервисам машинного обучения, что критично для развития рекламы. Современная реклама должна быть не назойливой, а полезной — подсказывать то, что действительно нужно пользователю. Плечо до ML-сервисов должно сокращаться, а в облаке мы можем локализовать все компоненты рядом.
Управляемость. Гибкий деплой, оркестрация, автоматическое масштабирование — всё это даёт облачная инфраструктура из коробки. Time-to-market новых фич сокращается кратно.
Адаптация архитектуры: от часа до минут
Первая задача при переезде — адаптировать архитектуру под облачную инфраструктуру, которая сложилась за множество лет разработки. Исторически загрузка одного инстанса могла занимать больше часа, что совершенно недопустимо в облаке, где инстансы должны быстро подниматься и падать.

Процесс адаптации был итеративным и занял несколько лет. Ключевые изменения:
Отказ от thread affinity. На железе многие оптимизации строились на привязке потоков к конкретным ядрам процессора. В облаке thread affinity становится врагом — виртуализация и динамическое распределение ресурсов делают такие оптимизации бессмысленными и даже вредными. Первые попытки переезда несколько лет назад провалились именно из-за этого.
Переход от stateful к stateless. Долгое время мы жили в stateful-модели, где каждый инстанс хранил своё состояние. Для облака нужна stateless-модель, где инстанс может быть убит и поднят заново без потери данных.
Реализация graceful degradation. При переезде под высокой нагрузкой критически важно уметь деградировать gracefully. Если часть функциональности недоступна, сервис должен продолжать работать с урезанным функционалом, а не падать целиком.
Миграция проходила в три этапа. Сначала мы жили в режиме «хамелеона» — одним глазом на железе, другим в облаке. Релиз-инженер держал два набора метрик и дашбордов, постоянно сравнивая производительность. Как он сам говорил: «плачет, рыдает, колется, но как-то справляется».

На втором этапе мы полностью перешли в облако с толстым сервисом, железо "закопали". Появились механизмы гибкого деплоя, но пока без шардирования. И только на третьем этапе перешли на полностью шардированный сервис с возможностью горизонтального и вертикального масштабирования.

Доставка данных: от libslave к торрентам
Ключевым архитектурным изменением стал отказ от MySQL-репликации в пользу CDC (Change Data Capture) модели с доставкой через P2P.

Мы разделили данные на два типа:
Быстрые данные — ставки, балансы, состояния кампаний. Всё, что нужно оперативно растащить на весь прод. Переехали на Kafka, данные догоняются каждым инстансом в процессе работы.
Медленные данные — всё, что укладывается в окно модерации. Данные из веб-интерфейсов, часть статистики, старт/стоп кампаний. Упаковываются в снапшоты и раскладываются на прод пачками.
При переходе на снапшоты пришлось учитывать legacy — у нас ORM на C++, которая отлично работает в существующих условиях. Для неё реализовали свой протокол упаковки данных в бинарный формат для последующей сборки в снапшоты.
P2P через торрент-протокол
Главным архитектурным решением стал переход на P2P-модель доставки снапшотов через настоящий торрент-протокол — да, тот самый, которым качали фильмы. И, судя по последним событиям, многие до сих пор пользуются.
Выделили две роли инстансов:
Seeders — варилки снапшотов. Забирают данные из MySQL, формируют готовый снапшот и становятся первичными раздающими в торрент-сети. Координируются через etcd, чтобы избежать дублирования работы — только один seeder в моменте формирует снапшот, остальные ждут.
Peers — все остальные инстансы. Получают снапшоты по P2P, обмениваясь кусочками друг с другом, как в классическом торренте.
Важный момент: планируется переход на «seeder as a tool» — когда seeder загрузился, собрал снапшот и отключился от slave. Это окончательный уход от MySQL-репликации.
Классическая торрент-схема позволила раздавать толстые снапшоты на весь прод за 5 минут. При этом нагрузка распределяется между всеми участниками, а не ложится на центральный сервер. Снапшоты сохраняются в S3 для возможности отката при проблемах.
Применение данных: как ядерный реактор
Существует два подхода к применению снапшотов:
Без downtime, но с двойной памятью. Держим два набора данных, атомарно переключаемся между ними. Долго жили в этой парадигме, но она требует слишком много памяти.
С downtime, но экономно. Держим только один набор данных и 10 % запасных инстансов. Опускаем инстансы пачками, применяем новые данные, поднимаем обратно.
Мы выбрали второй вариант с оркестрацией на уровне сервиса, а не облака. Почему не использовали стандартные readiness/liveness probes? Наш сервис пока ещё толстый, перемещение большого бинаря плюс выделение ресурсов в облаке приведёт к медленному запуску. Поэтому оркестрацию реализовали отдельным слоем, который применяет данные без перезапуска инстанса.

Самое важное — защита от «битых» снапшотов. Мы используем аналогию с ядерным реактором: современные реакторы при аварии просто глохнут, а не взрываются. Так же и у нас — если снапшот кладёт инстансы при применении, система останавливает применение. Прод продолжает работать на старых данных, у нас растёт лаг на применение, но сервис не падает. Есть время починить проблему или откатиться на валидный снапшот из S3.
Оркестратор на каждом инстансе через etcd отслеживает:
Готовность кластера (cluster ready).
Версию сервиса и совместимость схемы данных.
Маску шардирования.
Приоритет устаревших инстансов для обновления.
При изменении схемы данных или бизнес-логики выкладываются только снапшоты нужной версии — это критично для консистентности данных в промышленной среде.
Шардирование: от монолита к микросервисам
После решения проблем с доставкой данных приступили к шардированию для сокращения потребления CPU и памяти.

Единицей шардирования выбрали рекламные кампании — они естественным образом группируют все связанные объекты (объявления, таргетинги, ставки). Шардирование может быть:
Автоматическим — по битовой маске равномерно распределяем кампании.
Ручным — для VIP-кампаний можем выделить отдельный шард.
При шардировании потребовался дополнительный слой — Gateway для агрегации ответов. Его функции:
Принимает запрос от фронтов.
Единожды запрашивает профиль пользователя.
Обогащает запрос и направляет во все релевантные шарды.
Собирает ответы от шардов.
Проводит финальный аукцион между результатами.
Передаёт победителя в сервис форматирования.
Gateway также выравнивает "пилу" ошибок при rolling update — когда часть инстансов перезапускается, на уровне Gateway мы можем компенсировать временную недоступность части шардов. Дополнительно собираем детальную статистику по каждому шарду для мониторинга.
Результаты миграции
После переезда в облако мы достигли впечатляющих результатов:

Оптимизация ресурсов:
Потребление памяти каждого пода снизилось в 2 раза.
Количество инстансов увеличилось в 4 раза при той же суммарной мощности.
Более грамотная утилизация ресурсов — вместо больших толстых инстансов много маленьких.
Ускорение процессов:
Время деплоя сократилось в 4 раза.
Быстрый старт после аварий — весь прод поднимается за считанные минуты благодаря P2P.
Time-to-market новых фич сократился кратно.
Надёжность:
Защита от «битых» данных через валидацию снапшотов.
Возможность отката на предыдущие версии из S3.
Graceful degradation при частичной недоступности.
Конечно, за это пришлось заплатить усложнением архитектуры. Вместо одного монолита теперь оркестрируем множество сервисов. Потребовались дополнительные инвестиции в мониторинг, SRE-инженеров и автоматизацию. Но это оправданная цена за гибкость и масштабируемость.
Планы на будущее
Мы продолжаем оптимизировать систему. В ближайших планах:

Ускорение старта инстанса. Сейчас мы перекладываем данные в бинарный формат, но в идеале хотим загружаться непосредственно со слепка памяти, минуя промежуточные преобразования.
Полная автоматизация деплоя. Чем больше разработчиков вносят изменения, тем важнее сокращать время выкладки и уменьшать time-to-market.
Унификация по профилю нагрузки. Хотим уйти от различных конфигураций для разных типов трафика и сделать единый подход к подбору рекламы.
Конечная цель всех этих улучшений — сделать рекламу действительно удобной и полезной для пользователей. Как мы говорим, реклама не должна быть назойливой, являться вам во сне как кошмар. Она должна подсказывать то, что вам действительно может пригодиться. А для этого нужна быстрая, гибкая и надёжная инфраструктура.