Привет!
Мы команда «Исходного кода“ и уже полгода системно занимаемся нагрузочным тестированием (НТ). Раньше такие проверки были от случая к случаю — оттуда и взяли базу знаний. Сегодня хотим поделиться историей одного показательного фейла, который заставил нас пересмотреть весь подход и прийти к системе, которая показала себя, как работающая.
Все мы знаем эту боль: фича идеально работает на деве и предпроде, проходит все тесты, а когда под реальной нагрузкой на нее заходят сотни пользователей одновременно — все начинает тормозить, сыпать ошибками или просто падать. Чтобы этого избежать, мы решили, что НТ должно стать обязательным этапом для всех фичевых задач, которые серьезно меняют логику, затрагивают запросы к серверу, кэширование или обработку данных.
Главный толчок был простой и жизненный: уже на стадии рассмотрения сервиса мы понимаем, какая нагрузка на него ляжет, поэтому мы выводили правило: «Сервис должен стабильно держать N запросов в секунду», и мы берем эту планку и начинаем работу.
DevOps - обязательный напарник
Сразу скажем: без DevOps-инженера здесь никуда. Он ключевой игрок, который настраивает ресурсы для подов и контейнеров в Kubernetes (CPU, RAM и все по документации). А главное, прямо во время теста он может оперативно вносить корректировки в конфиги, если мы упираемся в инфраструктурные лимиты.
Реальный фейл: гонка за токеном и 403 ошибки
А теперь самое интересное - история, как мы наступили на грабли.
Дано: Новая фича, которая успешно прошла все тесты на деве и предпроде. Команда уверена в успехе.
Задача: Запустить первый полноценный нагрузочный тест на предпрод-контуре.
Результат: При запуске теста на бэкенде массово начинают сыпаться 403 ошибки. Сначала часть запросов проходит, но с ростом нагрузки все становится хуже и хуже.

По всем значимым урлам на бэке при нагрузке время стремительно растет. Нагрузка начинает подаваться в 10:17, и уже через две минуты система полностью деградирует. Интенсивность неуспешных запросов. В 18:01-18:02 - резкий всплеск ошибок. Больше всего 502-х на статике Next.js, JS-бандлах и медиафайлах. Фронт-сервер не успевает отдавать ресурсы, бэкенд-прокси падает с Bad Gateway. Такие всплески каждые 1-3 минуты и загнали общий процент ошибок до 46% в тесте №3.
Начали разбираться в логах. Наш бэкенд выступал как прокси-прослойка между фронтендом и смежным сервисом, откуда мы получали данные. Для обращения к нему мы получали токен и кэшировали его у себя через opcache.
Проблема оказалась в классической гонке процессов. Под нагрузкой несколько параллельных запросов одновременно видели, что токен протух, и пытались его обновить. В результате более старый токен мог записаться в кэш позже, перетирая свежий. После этого часть запросов начинала ходить во внешний сервис с уже невалидным токеном - отсюда и 403 ошибки.
Сделали → сломали → починили лучше:
-
Добавили блокировку на получение токена, чтобы исключить гонку при его обновлении.
-
Вынесли кэширование токена в кластерный memcached, чтобы кэш был единым для всех процессов, а не локальным для каждого.
Чтобы быстро проверить гипотезу и не ждать нового полноценного НТ, мы использовали Postman. В нем есть возможность запускать нагрузочные сценарии. Прогнали серию параллельных запросов и убедились, что проблема с токеном больше не воспроизводится. Сработало!
Этот кейс всем нам показал: обычное тестирование часто пропускает проблемы, которые вылезают только, когда система работает под настоящей нагрузкой.
Наш текущий флоу: от чек-листа до метрик
Три панели, которые смотрели на каждом НТ: Success Rate - 100% на протяжении всего теста. Доля 5xx в среднем 0,017%, пик 0,042%. В пределах допустимого. Ingress RPS – нагрузка нарастала с 0 до 300 RPS, стабилизировалась на 226 RPS в среднем, максимум 321 RPS. AVG response time - среднее устоялось на 99-104 мс (скользящее за 10 минут).
А вот что показывает График 2. На бэкенде также внесли правки. После них сервис стабильно держит нагрузку в допустимых пределах.
Success Rate: 100% (среднее и минимум)
5xx (среднее): 0,017%
5xx (пик): 0,042%
RPS (среднее): 226
RPS (максимум): 321
AVG response time (среднее): 99-104 мс
AVG response time (пик при прогреве): 123 мс
RPS плавно нарастал с нуля до 300-321 и стабилизировался.
Этот фейл заставил нас создать системный подход. Теперь у нас есть подробный чек-лист, который мы проходим перед каждым НТ. Он позволяет последовательно проверять систему на разных уровнях.
1. Подготовка системы (чек-лист бэкенд-разработчика):
-
Оптимизация БД:
-
Добавляем индексы на все WHERE / JOIN запросы.
-
Оптимизируем медленные запросы через EXPLAIN анализ.
-
Настраиваем репликацию и connection pooling для высоких нагрузок.
-
-
Кэширование данных:
-
Используем memcached для частых запросов.
-
Кэшируем HTTP-ответы.
-
Кэшируем скомпилированные Twig-шаблоны.
-
-
Асинхронная обработка:
-
Используем очереди задач (RabbitMQ / Redis Queue).
-
Выносим «тяжелые» операции в фон (отправка email, генерация отчетов).
-
-
Оптимизация PHP-кода:
-
Предотвращаем проблемы N+1.
-
Оптимизируем циклы и рекурсии.
-
Снижаем потребление памяти через unset для больших объектов.
-
-
Конфигурация инфраструктуры:
-
Настраиваем php-fpm (pm.max_children, pm.max_requests).
-
Используем Minio для хранения большого количества файлов.
-
Конфигурируем Horizontal Pod Autoscaler (HPA).
-
Используем Rate Limiter для настройки троттлинга.
-
Как бы круто вы ни оптимизировали бэк, не забывайте про точку входа - фронтенд. Именно он генерирует основной поток запросов, и простые оптимизации на его стороне могут кардинально снизить входящую нагрузку.
На что смотреть в первую очередь:
-
Кэширование статики (nginx/assets). Самый простой и эффективный шаг. Картинки, документы, статический код должен отдавать nginx - такие запросы до вашего бэкенда просто не дойдут.
-
Кэширование API-запросов на клиенте. Если у вас есть данные, которые меняются редко (список новостей, виджет погоды), смело кэшируйте их в memcached или аналогичное (в зависимости от объема данных и требований к работе с этими данными). Это лучше, чем дергать бэк на каждый чих.
-
Пагинация. Запрашиваем только то, что видит пользователь, а не выгружаем тысячи элементов разом. База, о которой иногда забывают.
-
Генерация страниц заранее (SSG/ISR). Если контент не меняется в реальном времени, нет смысла собирать страницу на каждый запрос.
-
Использовать SSE/Webhooks вместо постоянного опроса (polling). Чтобы не долбить сервер каждые 5 секунд с вопросом: «Ну что, есть что-то новое?». Сервер сам сообщит, когда появятся обновления.
2. Планирование и запуск:
От лида разработки и PM требуется минимум за неделю до готовности запланировать НТ. Совместно с инженерами по НТ они готовят сценарий, список API, оценку нагрузки и конфигурацию подов. Дальше команда НТ запускает тесты.
Важный момент: мы проводим НТ на прод-контуре, когда нагрузка пользователей минимальная (ночью). Наши тестовые окружения не являются полной копией прода, поэтому целевые результаты там недостижимы.
3. Анализ:
Мы совместно смотрим на метрики через Grafana и Graylog. Успешным прохождением считается стабильная работа сервиса при целевых показателях. Если находим узкие места, возвращаемся к оптимизации и повторяем тест. НТ проводится до тех пор, пока не будет найдена точка деградации системы, что позволяет нам понимать реальный запас прочности наших сервисов.
Этот процесс позволяет своевременно находить и устранять большую часть неочевидных проблем связанных с высокой нагрузкой.
Какая реальная польза получилась?
-
Пользовательский опыт не страдает. Нет троттлинга (долгого ответа от сервиса) при высокой нагрузке, меньше отказов (HTTP 500).
-
Снизилась нагрузка на железо. За счет оптимизаций мы эффективно используем ресурсы.
-
Прокачали экспертизу. Теперь мы учитываем нюансы нагрузки уже на этапе проектирования решения, а не прикручиваем «костыли» в последний момент.
Стало заметно стабильнее - освободилось время для других задач.
Выводы и рекомендации для тех, кто только начинает
3 ключевых вывода из нашего опыта:
-
Многие проблемы проявляются только под реальной нагрузкой. То, что отлично работает на дев, может сломаться при большом количестве параллельных запросов.
-
Важно учитывать гонку процессов. Заранее думайте про блокировки, единый кэш и корректную работу с токенами и сессиями.
-
Среда тестирования должна быть максимально приближена к продакшену. Конфигурация, кэширование и взаимодействие с внешними сервисами должны быть prod-like.
Практические советы, чтобы не наступить на наши грабли:
-
Проверяйте параллельные запросы заранее. Postman или k6 позволяют отловить простые гонки еще до полноценного НТ.
-
Кэширование и рендеринг продумывайте в начале, а не потом. Если данные статические, используйте SSG или ISR в Next.js.
-
Настройте хорошее логирование до начала тестов, а не после. Без нормальных логов диагностика узких мест превращается в угадайку.
-
Не запускайте НТ на устаревшем стеке. Новые версии фреймворков и инструментов регулярно включают оптимизации, которые вы получаете «бесплатно».
Большое спасибо команде «Исходного Кода» - разработчикам Тимофею, Денису, Ярославу, тимлиду Даниле и техническому директору Илье и всем причастным к этому кейсу.
А у вас был опыт, когда нагрузка все ломала? Или крутые лайфхаки, как избежать проблем? Делитесь в комментах, будет полезно обсудить.
Автор: codesrc
