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

По всем значимым урлам на бэке при нагрузке время стремительно растет. Нагрузка начинает подаваться в 10:17, и уже через две минуты система полностью деградирует.
Начали разбираться в логах. Наш бэкенд выступал как прокси-прослойка между фронтендом и смежным сервисом, откуда мы получали данные. Для обращения к нему мы получали токен и кэшировали его у себя через 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.
Настройте хорошее логирование до начала тестов, а не после. Без нормальных логов диагностика узких мест превращается в угадайку.
Не запускайте НТ на устаревшем стеке. Новые версии фреймворков и инструментов регулярно включают оптимизации, которые вы получаете «бесплатно».
Большое спасибо команде «Исходного Кода» - разработчикам Тимофею, Денису, Ярославу, тимлиду Даниле и техническому директору Илье и всем причастным к этому кейсу.
А у вас был опыт, когда нагрузка все ломала? Или крутые лайфхаки, как избежать проблем? Делитесь в комментах, будет полезно обсудить.
