Всем привет. Меня зовут Женя, я фулстек-разработчик в команде рекламы ВКонтакте.
В этой статье расскажу про высоконагруженное место в рекламном кабинете ВКонтакте. С похожей ситуацией может столкнуться в своём проекте любой разработчик. Материал будет полезен не только тем, кто занимается бэкендом, но и всем IT-специалистам, стремящимся сделать свои продукты быстрыми и отказоустойчивыми. Спойлер: в статье не будет кода.
Сейчас я в основном занимаюсь разработкой мобильного рекламного кабинета, но периодически приходится решать разноплановые задачи. Одна из таких — показать превьюшку рекламного объявления размером 30 на 30 пикселей в таблице объявлений.
Кажется, что тут такого? Добавил дополнительное поле в запрос к базе, прокинул на фронт полученную ссылку на картинку и отрисовал её в столбце. Выглядит просто: 30 строчек кода и 20 минут, включая поход за чашечкой кофе.
Итак, поехали!
Шаг 1. Получение картинки
В рекламном кабинете ВКонтакте пользователь может создавать множество разных по формату объявлений. Они могут содержать разный контент: фотографии, граффити, музыку, видео, истории, клипы, товары, карты, прикреплённые файлы — или сразу несколько в одном объявлении.
У каждого формата свои особенности структуры данных. Для некоторых, например с фотографиями и граффити, превьюшки можно добыть из них самих. А для объявлений с видео надо использовать специальные функции, чтобы получить стоп-кадры. Есть и другие форматы, без изображений, и там приходится использовать заглушки от дизайнеров. Поэтому пишем код, который по типу контента подготавливает превьюшку.
Шаг 2. Сжимаем картинку
Если на первом шаге выяснилось, что у нас в объявлении фотография, граффити или видео, то полученная картинка, скорее всего, будет большой. А нам нужна маленькая превьюшка, 30 × 30 пикселей. Заставлять браузер пользователя работать с тяжёлыми изображениями — значит увеличивать время загрузки всего контента на странице и сжирать для нас гигабайты трафика. Поэтому все большие картинки нужно сжать. Для этого у нас есть готовое решение: сервис, который принимает ссылку на картинку и подготавливает её уменьшенную копию с нужными размерами и качеством сжатия. Прогоняем все превьюшки через эту функцию.
Шаг 3. Хранение превью
В некоторых рекламных кабинетах больше 1 000 объявлений. Искать картинки и генерировать уменьшенные превьюшки при каждом открытии страницы — это очень долго. Нужно где-то хранить связь ссылки на превью и id объявления. Для этого отлично подходит внутренний персистентный memcached. Так что пишем обёртку для записи и получения превьюшки из memcached. А если превьюшку не нашли, то возвращаемся к первому шагу.
Шаг 4. Фронтенд
Здесь всё и правда просто: получили ссылку на картинку, отрисовали её в новом столбце.
Шаг 5. Нагрузка
Кажется, что всё готово и можно запускать на пользователей. Но представим, что после выкатки новой функциональности в прод рекламодатель в первый раз открыл свой кабинет, в котором 1 000 объявлений, и начал скроллить таблицу вниз. По умолчанию объявления грузятся пачками. По каждому объявлению начнётся процесс поиска превьюшки, и запрос на получение данных будет долго обрабатываться. Логичное решение — поставить лимит на количество обрабатываемых объявлений за одну загрузку страницы. Установим ограничение в 20.
Шаг 6. И снова нагрузка
Продолжаем: пользователь с 1 000 объявлений в первый раз зашёл в обновлённый рекламный кабинет, мы нашли превьюшки для первых 20 записей. Потом он заглянул во второй раз, мы создали превью для следующих 20 объявлений. Получается, чтобы увидеть превьюшки ко всем своим объявлениям, рекламодателю нужно 50 раз зайти в кабинет. Выглядит как проблема.
Решением может быть прогревание кеша: мы запустим под капотом логику поиска превьюшек, и при каждом заходе пользователя будем генерировать 20 новых и класть их в memcached, а на фронте новую колонку не показывать.
Но возникает вопрос: почему бы сразу не собрать все превью под капотом? Ответ: из-за нагрузки. У нас в системе огромное количество объявлений — как активных, так и архивных. Архивные открываются крайне редко, и тратить ресурсы на поиск превьюшек для них в фоновом режиме нецелесообразно. Поэтому будем создавать превью только для запрашиваемых пользователями объявлений.
Шаг 7. Что, опять нагрузка?
Да. Мы сделали фоновое прогревание кеша, но если выкатим это в прод на 100% пользователей, столкнёмся с проблемой. У всех рекламодателей разом пойдёт генерация превьюшек, и будет огромная нагрузка на бэк. Так что делаем плавное включение функциональности: можем управлять процентом пользователей, на которых запускается новая фича.
Шаг 8. Регенерация превьюшки
Ну теперь-то всё готово? Взяли объявление, создали превьюшку, положили её в memcached и при каждом обращении забираем оттуда.
Но что случится, если пользователь поменял контент объявления? Это можно сделать не только в рекламном кабинете, но и, например, при редактировании продвигаемого поста. Тогда превьюшка станет неактуальной. Поэтому после таких изменений нужно запускать генерацию новой. Добавляем.
Шаг 9. Статистика
Полезно следить за нагрузкой на систему. Так что повесим логи на каждый этап генерации превью.
Шаг 10. Тестирование
Пишем документацию по функциональности, несём всю логику тестировщикам, проходим код-ревью.
Шаг 11. Выкатка в прод
Мы готовы: код написан и заапрувлен, функциональность протестирована вдоль и поперёк, статистика собирается. Катим в прод! Плавно запускаем на всех пользователей прогревание кеша и, наконец, включаем отображение колонки с превьюшками на фронте.
Шаг 12. Мониторинг
Сидим и смотрим на графики.
Читаем комментарии с благодарностями от пользователей, допиваем чашку кофе... И идём сбривать полуторамесячную бороду.