Нагрузочное тестирование: как спроектировать и не получить мусорные цифры
«Запустили 1000 потоков, средняя 200 мс — норм, релизим». Так выглядит нагрузочный тест, который ничего не доказал. Хорошее нагрузочное отвечает на два вопроса: выдержит ли система ожидаемую нагрузку и где именно она ломается. Чтобы цифры были не мусорными, тест надо спроектировать. Разберём как.
Виды нагрузочных тестов — что каждый находит
- Load — ожидаемая нагрузка (целевой RPS): держим ли SLA на проде.
- Stress — выше ожидаемой, до отказа: где и как ломается, восстанавливается ли.
- Soak / endurance — обычная нагрузка, но часами: утечки памяти/коннектов, деградация, переполнение логов/дисков.
- Spike — резкий всплеск (распродажа, рассылка): как переживаем скачок и спад.
- Breakpoint / capacity — плавно поднимаем нагрузку, пока не сломается: находим реальный потолок.
Метрики: перцентили, а не средняя
Главный самообман — средняя латентность. Средняя 200 мс может скрывать, что каждый двадцатый запрос — 3 секунды. Смотрите перцентили: p95, p99, p99.9. Именно хвост чувствуют пользователи. Что мерить:
- Throughput — реальный RPS (а не число виртуальных юзеров).
- Latency по перцентилям — p50/p95/p99/p99.9; средняя — для мусорки.
- Error rate — доля ошибок и таймаутов под нагрузкой (зелёный тест с 30% 5xx — это не «прошло»).
- Saturation — насыщение ресурсов (CPU, память, IO, сеть, пулы) на стороне системы.
Открытая vs закрытая модель — где врут чаще всего
Это решает, насколько тест реалистичен:
- Закрытая модель: фиксированное число виртуальных юзеров, каждый шлёт следующий запрос только после ответа на предыдущий. Если система тормозит — нагрузка сама снижается. Итог: вы маскируете проблему — очередь не растёт, потому что вы её не создаёте.
- Открытая модель: фиксированная интенсивность поступления запросов (N в секунду) независимо от того, ответила ли система. Так ведут себя реальные пользователи и очереди начинают расти — вы видите настоящую деградацию. Подробно — в доке k6 про open vs closed.
Если меришь пропускную способность сервиса — почти всегда нужна открытая модель. Закрытая занижает проблемы под нагрузкой.
Реализм нагрузки
- Ramp-up и warm-up: «мгновенно 1000 юзеров» нереалистично и ловит холодные кэши/JIT/пулы. Поднимайте плавно, дайте системе прогреться, меряйте на стабильном участке.
- Профиль трафика: не долбите один эндпоинт. Воспроизводите реальную смесь (логин, листинг, поиск, оплата) в реальных пропорциях.
- Think time: реальные юзеры думают между действиями — закладывайте паузы (в открытой модели — через интенсивность).
- Данные: не пустая БД. На пустой таблице любой запрос быстрый; под реальным объёмом включаются индексы, блокировки, план запроса меняется.
Мерить НА СТОРОНЕ системы, а не только клиента
Клиентские цифры (латентность, RPS) говорят что медленно, но не почему. Чтобы найти бутылочное горлышко, снимайте серверные метрики — по сути golden signals (latency, traffic, errors, saturation, см. Google SRE):
- Сатурация CPU/памяти/диска/сети на каждом сервисе.
- Пул соединений к БД и время ожидания в пуле (частый скрытый потолок).
- Длина очередей (брокеры, тред-пулы), GC-паузы, время GC.
- Метрики БД: медленные запросы, локи, IOPS.
Без этого вы видите симптом («стало медленно на 800 RPS»), но не причину («исчерпан пул из 20 коннектов»).
Окружение
Тест на стенде в 10 раз слабее прода даёт цифры, которые ничего не значат. Нужна прод-подобная конфигурация (ресурсы, данные, топология, сеть) — или хотя бы понятный коэффициент масштабирования. Иначе вы тестируете стенд, а не систему.
Coordinated omission — тихий убийца перцентилей
Классическая ошибка измерения (описал Gil Tene): если генератор нагрузки ждёт ответа, прежде чем послать следующий запрос, то во время затыка он не отправляет запросы, которые должны были уйти — и не учитывает их большие задержки. В итоге p99 выглядит прекрасно, а реальность ужасна. Лечится открытой моделью / коррекцией на coordinated omission в инструменте.
Инструменты: что когда
- k6 — скриптуемый на JS, нативная открытая модель, хорош для CI и как код.
- Gatling — JVM/Scala(/Java), высокая пропускная способность с одной машины, хорошие отчёты.
- Locust — на Python, сценарии кодом, удобен питонистам и распределённому запуску.
- JMeter — зрелый, GUI, много протоколов; тяжелее в версионировании сценариев.
Частые ошибки
- Средняя вместо перцентилей.
- Тест на пустой БД / прогретых данных, не похожих на прод.
- Только закрытая модель → проблема замаскирована.
- Один эндпоинт вместо профиля трафика.
- Нет серверных метрик → не найти горлышко.
- Игнор error rate (зелёно при 5xx).
- Нет warm-up и ramp-down.
- Coordinated omission не учтён.
- Один прогон без повторяемости — шум принимают за результат.
- Стенд не прод-подобный.
Чек-лист нагрузочного теста (12 пунктов)
- Определена цель и тип теста (load/stress/soak/spike/breakpoint).
- Заданы целевые SLA/SLO (p95/p99 латентность, max error rate).
- Метрики — перцентили + throughput + error rate + saturation, не средние.
- Открытая модель там, где меряем пропускную способность.
- Реалистичный профиль трафика (микс эндпоинтов и пропорции).
- Ramp-up + warm-up; измерение на стабильном участке; ramp-down.
- Прод-подобные данные (объём, индексы) и окружение.
- Снимаются серверные метрики (CPU/mem/IO, пул БД, очереди, GC).
- Учтён coordinated omission.
- Error rate — часть критерия прохождения, не только латентность.
- Тест повторяем (несколько прогонов, стабильные результаты).
- Найдено и описано бутылочное горлышко, а не только симптом.
Итог
Нагрузочный тест ценен не числом «выдержали 1000», а тем, что показывает реальный потолок, поведение на хвосте и место отказа. Перцентили вместо средних, открытая модель, прод-подобные данные и серверные метрики — вот что превращает «потыкали нагрузку» в выводы, которым можно верить перед релизом.