Как стать автором
Обновить

Как Go спасал нашу «Чёрную пятницу»

Время на прочтение7 мин
Количество просмотров8.2K
Ранее мы уже рассказывали о том, что по мере роста нагрузки постепенно ушли от использования Python в бэкенде критичных сервисов на продакшене, заменив его на Go. А сегодня я, Денис Гирько, тимлид команды разработки Madmin, хочу поделиться деталями: как и почему это происходило на примере одного из важнейших для нашего бизнеса сервисов — расчета цены с учетом скидок по купонам.



Механику работы с купонами представляет, наверное, каждый, кто хотя бы раз совершал покупки в интернет-магазинах. На специальной странице или прямо в корзине ты вводишь номер купона, и цены пересчитываются в соответствии с обещанной скидкой. Расчет зависит от того, какую именно скидку предоставляет купон — в процентах, в виде фиксированной суммы или с использованием какой-то иной математики (у нас, например, дополнительно учитываются баллы программы лояльности, акции магазина, типы товаров и т.п.). Естественно, заказ оформляется уже с новыми ценами.

Бизнес в восторге от всех этих механизмов работы с ценами, но мы хотим поговорить о сервисе с несколько иной точки зрения.

Как это работает


За расчет цен с учетом всех этих сложностей на бэкенде сейчас у нас отвечает отдельный сервис. Однако самостоятельным он был не всегда. Сервис появился через год или два после начала работы интернет-магазина, и к 2016 году это была часть большого монолита на Python, включавшего самые разнообразные компоненты для маркетинговой активности (Мадмин). В самостоятельный «блок» он выделился позже, по мере движения в сторону микросервисной архитектуры.

Как это обычно и бывает с монолитами, Madmin видоизменялся и частично переписывался большим числом разработчиков. Туда интегрировались сторонние библиотеки, которые упрощали разработку, но зачастую не самым лучшим образом сказывались на производительности. Однако на тот момент мы не особо заботились об устойчивости к большим нагрузкам во время распродаж, поскольку сервис отлично справлялся с поставленной задачей. Но 2016 год все изменил.



В США «Черная пятница» известна с 60-х годов прошлого века. В России ее начали запускать в 2010-х, при этом акцию пришлось фактически создавать с нуля — рынок был к ней не совсем готов. Однако усилия организаторов не прошли зря, и с каждым годом пользовательский трафик на наш сайт в дни распродаж увеличивался. А поэтому наше столкновение с нагрузкой, непосильной для той версии сервиса расчета цены, было лишь вопросом времени.

«Черная пятница» 2016. И мы ее проспали


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

Теперь к каждой новой «черной пятнице» мы готовимся, имитируя ожидаемую нагрузку,  но в 2016 году мы еще поступали иначе. Тестируя Мадмин перед важным днем, мы проверяли устойчивость к нагрузкам, используя сценарии поведения пользователей в обычные дни. Как выяснилось, тест этот не совсем отражает реальную ситуацию, поскольку в «черную пятницу» приходит множество людей с одним и тем же купоном. В результате сервис расчета цены с учетом этой скидки, не справившись с трехкратной (по сравнению с обычными днями) нагрузкой, в самый жаркий пик распродажи блокировал нам возможность обслуживать клиентов в течение двух часов.

Сервис «лег» за час до полуночи. Все началось с обрыва подключения к БД (на тот момент — MySQL), после которого не все запущенные копии сервиса расчета цены смогли подключиться обратно. А те, что все-таки подключились, не выдержали пришедшей нагрузки и перестали отвечать на запросы, застряв на блокировках базы.

По стечению обстоятельств на дежурстве тогда остался джуниор, который в момент падения сервиса находился в дороге из офиса домой. Подключиться к проблеме он смог лишь приехав на место и вызвав «тяжелую артиллерию» — запасного дежурного. Совместными усилиями они нормализовали ситуацию, правда, только через два часа.

По мере разбирательств начали открываться подробности о том, насколько неоптимально работал сервис. К примеру выяснилось, что для расчета одного купона делалось 28 запросов в базу (не удивительно, что все работало со 100% загрузкой CPU). Упомянутые выше пользователи с одним и тем же купоном «черной пятницы» не упрощали ситуацию, тем более тогда для всех купонов у нас существовал счетчик применений — так что каждое использование увеличивало нагрузку, обращаясь к этому счетчику.

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


Итоги Black Friday 2016

«Черная пятница» 2017. Мы готовились серьезно, но...


Получив хороший урок, к следующей «черной пятнице» мы готовились заранее, серьезно перестроив и оптимизировав сервис. Например, мы наконец-то создали два типа купонов: лимитные и безлимитные — чтобы избежать блокировок на одновременном доступе к базе, мы убрали из сценария применения популярного купона запись в базу. Параллельно за 1 — 2 месяца до «черной пятницы» мы в сервисе перешли с MySQL на PostgreSQL, что вместе с оптимизацией кода дало сокращение количества обращений к базе с 28 до 4 — 5. Эти усовершенствования позволили дотянуть сервис на тестировании до требований SLA — ответ за 3 секунды по 95 перцентилю при 600 RPS.

Не имея представлений о том, на сколько именно наши доработки ускорили работу старой версии сервиса на продакшене, на тот момент к «черной пятнице» готовилось сразу две версии кода на Python — сильно оптимизированная существующая версия и полностью новый код, написанный с нуля. В продакшн выкатили вторую, которую перед этим днями и ночами тестировали. Однако, как выяснилось уже «в бою», немного недотестировали.

В день «ЧП» с приходом основного потока покупателей нагрузка на сервис начала расти в геометрической прогрессии. Некоторые запросы обрабатывались до двух минут. Из-за долгой обработки одних запросов росла нагрузка на другие воркеры.

Нашей главной задачей было обслужить такой ценный для бизнеса трафик. Но стало очевидно, что «забрасывание железом» не решает проблему и с минуты на минуту количество занятых воркеров достигнет 100%. Не зная тогда, с чем именно мы столкнулись, приняли решение активировать harakiri в uWSGI и просто прибивать длинные запросы (которые обрабатываются более 6 секунд), чтобы освободить ресурсы для нормальных. И это действительно помогло устоять — воркеры стали освобождаться буквально за пару минут до их полного исчерпания.

Чуть позже мы разобрались в ситуации…  Выяснилось, что это были запросы с очень большими корзинами — от 40 до 100 товаров — и со специфическим купоном, имеющим ограничения на ассортимент. Именно эта ситуация плохо отрабатывалась новым кодом. В нем обнаружилась некорректная работа с массивом, которая превращалась в бесконечную рекурсию. Любопытно, что кейс с большими корзинами мы тогда тестировали, но не в сочетании с хитрым купоном. В качестве решения мы просто переключились на другую версию кода. Правда, произошло это часа за три до конца «черной пятницы». С этого момента все корзины начали обрабатываться корректно. И хотя план по продажам мы на тот момент выполнили, глобальных проблем из-за нагрузки, впятеро превышающей обычный день, мы избежали чудом.

«Черная пятница» 2018


К 2018 году для высоконагруженных сервисов, обслуживающих сайт, мы постепенно начали внедрять Go. Учитывая историю предыдущих «черных пятниц», сервис расчета скидок был одним из первых кандидатов на переработку.



Конечно, мы могли сохранить уже «проверенную в бою» версию Python, а перед новой «черной пятницей» заняться отключением тяжелых библиотек и выкидыванием неоптимального кода. Однако Golang к тому моменту уже прижился и выглядел более перспективным.

На новый сервис мы перешли летом этого года, так что до очередной распродажи успели хорошо его протестировать, в том числе, на возрастающем профиле нагрузки.

В ходе тестирования выяснилось, что слабым местом с точки зрения высоких нагрузок у нас остается база. Слишком длительные транзакции приводили к тому, что мы выбирали весь пул коннектов, и запросы стояли в очереди. Так что нам пришлось немного переделать логику работы приложения, сократив использование базы до минимума (обращаясь к ней только тогда, когда без этого никак) и закешировав справочники из БД и данные по популярным в «черную пятницу» купонам.

Правда, в этом году мы ошиблись с прогнозами нагрузки в большую сторону: готовились к 6-8 кратному росту в пиках и добились хорошей работы сервисов именно для такого объема запросов (добавили кеши, заранее отключили экспериментальные функции, упростили некоторые вещи, развернули дополнительные ноды Kubernetes и даже серверы БД для реплик, которые в итоге не потребовались). На деле всплеск пользовательского интереса был меньше, так что все прошло в штатном режиме. Время ответа сервиса не превышало 50 мс по 95 перцентилю.

Для нас одна из важнейших характеристик — то, как приложение масштабируется при нехватке ресурсов одной копии. Go эффективнее расходует аппаратные ресурсы, поэтому при той же нагрузке требуется запускать меньше копий (в конечном счете обслуживая больше запросов на тех же аппаратных ресурсах). В этом году в самый пик распродажи работало 16 экземпляров приложения, которые обрабатывали в среднем 300 запросов в секунду с пиками до 400 запросов в секунду, что примерно в два раза выше обычной нагрузки. Отмечу, что в прошлом году сервису на Python потребовалось 102 экземпляра.

Казалось бы, сервис на Go с первого подхода закрыл все наши потребности. Но Golang — не «универсальное решение всех проблем». Здесь не обошлось без некоторых особенностей. К примеру, нам пришлось ограничить количество потоков, которые может запустить сервис на многопроцессорной ноде Kubernetes, чтобы при масштабировании не мешать «соседним» приложениям на продакшене (по умолчанию у Go нет ограничений на то, сколько процессоров он займет). Для этого во всех приложениях на Go мы задали GOMAXPROCS. Будем рады комментариям о том, насколько это было полезно — в нашей команде это была лишь одна из гипотез относительно того, как следует бороться с деградацией «соседей».

Еще одна «настройка» — количество соединений, которые удерживаются как Keep-Alive. Штатные клиенты http и БД в Go по умолчанию удерживают только два соединения, поэтому если есть много конкурентных запросов, и нужно экономить на трафике установки TCP-соединения, имеет смысл увеличить это значение, задав MaxIdleConnsPerHost и SetMaxIdleConns соответственно.

Однако даже с учетом этих ручных «докручиваний» Golang обеспечил нам большой запас по производительности на будущие распродажи.
Теги:
Хабы:
+6
Комментарии37

Публикации

Информация

Сайт
tech.lamoda.ru
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия