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

За последние три года рекламная система 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 для агрегации ответов. Его функции:

  1. Принимает запрос от фронтов.

  2. Единожды запрашивает профиль пользователя.

  3. Обогащает запрос и направляет во все релевантные шарды.

  4. Собирает ответы от шардов.

  5. Проводит финальный аукцион между результатами.

  6. Передаёт победителя в сервис форматирования.

Gateway также выравнивает "пилу" ошибок при rolling update — когда часть инстансов перезапускается, на уровне Gateway мы можем компенсировать временную недоступность части шардов. Дополнительно собираем детальную статистику по каждому шарду для мониторинга.

Результаты миграции

После переезда в облако мы достигли впечатляющих результатов:

Оптимизация ресурсов:

  • Потребление памяти каждого пода снизилось в 2 раза.

  • Количество инстансов увеличилось в 4 раза при той же суммарной мощности.

  • Более грамотная утилизация ресурсов — вместо больших толстых инстансов много маленьких.

Ускорение процессов:

  • Время деплоя сократилось в 4 раза.

  • Быстрый старт после аварий — весь прод поднимается за считанные минуты благодаря P2P.

  • Time-to-market новых фич сократился кратно.

Надёжность:

  • Защита от «битых» данных через валидацию снапшотов.

  • Возможность отката на предыдущие версии из S3.

  • Graceful degradation при частичной недоступности.

Конечно, за это пришлось заплатить усложнением архитектуры. Вместо одного монолита теперь оркестрируем множество сервисов. Потребовались дополнительные инвестиции в мониторинг, SRE-инженеров и автоматизацию. Но это оправданная цена за гибкость и масштабируемость.

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

Мы продолжаем оптимизировать систему. В ближайших планах:

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

Полная автоматизация деплоя. Чем больше разработчиков вносят изменения, тем важнее сокращать время выкладки и уменьшать time-to-market.

Унификация по профилю нагрузки. Хотим уйти от различных конфигураций для разных типов трафика и сделать единый подход к подбору рекламы.

Конечная цель всех этих улучшений — сделать рекламу действительно удобной и полезной для пользователей. Как мы говорим, реклама не должна быть назойливой, являться вам во сне как кошмар. Она должна подсказывать то, что вам действительно может пригодиться. А для этого нужна быстрая, гибкая и надёжная инфраструктура.

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