non-functionalperformanceload-testingobservabilitytesting

Нагрузочное тестирование: как спроектировать и не получить мусорные цифры

«Запустили 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 пунктов)

  1. Определена цель и тип теста (load/stress/soak/spike/breakpoint).
  2. Заданы целевые SLA/SLO (p95/p99 латентность, max error rate).
  3. Метрики — перцентили + throughput + error rate + saturation, не средние.
  4. Открытая модель там, где меряем пропускную способность.
  5. Реалистичный профиль трафика (микс эндпоинтов и пропорции).
  6. Ramp-up + warm-up; измерение на стабильном участке; ramp-down.
  7. Прод-подобные данные (объём, индексы) и окружение.
  8. Снимаются серверные метрики (CPU/mem/IO, пул БД, очереди, GC).
  9. Учтён coordinated omission.
  10. Error rate — часть критерия прохождения, не только латентность.
  11. Тест повторяем (несколько прогонов, стабильные результаты).
  12. Найдено и описано бутылочное горлышко, а не только симптом.

Итог

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

Источники