Нагрузочное тестирование — одна из самых избегаемых тем, когда речь заходит о контроле качества ПО. Корпорации, конечно, не обходят его стороной, но если говорить о продуктах меньшего масштаба, то нагрузочное тестирование часто пропускается. Команда (и, в целом, справедливо) полагает, что продукт справится с нагрузкой — на малых объёмах это обычно прокатывает. А потом внезапно наступает день, когда пользователей стало больше, а система не готова.
Почему команды не тащат нагрузку в релизный цикл? Потому что это чаще всего просто не окупается: нужно выбрать движок, описать сценарий, гонять тесты вручную или тратить время на создание собственной обвязки для встраивания в CI, придумать критерии качества и анализировать результаты. Всё это занимает значительное время, а на короткой дистанции часто оказывается оверинжинирингом. Но если формирование требований упростить концептуально невозможно, то всё остальное вполне можно собрать в переиспользуемый инструмент, позволяющий командам легко интегрировать нагрузочное тестирование и регрессионный анализ в свой процесс доставки.
В CI/CD мы хотели простую штуку: на каждый PR запускать короткий перф‑смоук и получать ответ уровня «PASS / WARNING / DEGRADATION», а не 15 минут медитировать над CSV и тратить ценное время на анализ, который, вероятно, не пригодится в ближайшей перспективе. Посмотрим, к чему мы в итоге пришли.
Пример HTML‑отчёта (KPI‑карточки + графики + сравнение с baseline)
Тёмная тема

Кастомная тёмная тема

Брендированный отчёт

Preset: errors

Preset: throughput

Preset: latency

Зачем вообще тащить нагрузку в CI
Идея, в общем-то, лежит на поверхности: ловить падение производительности не на проде и не в ночь перед релизом, а ровно в момент, когда оно появились — в PR. Та же самая логика «раннего обнаружения», что и в юнит‑тестах: быстрый фидбек, меньше рисков, меньше ночных алертов. В индустрии это обычно называют shift-left performance testing — нагрузка становится частью пайплайна, а не отдельным ритуалом, который проводят по большим праздникам. [1]
Цель была приземлённая: сделать так, чтобы нагрузочный тест в CI по UX напоминал обычный юнит‑тест. Запустил — получил результат — пошёл дальше.
Если подробнее, нам нужно было получить следующее CI‑поведение:
-
предсказуемо запускать нагрузку (smoke на PR, длиннее на main/ночью);
-
сравнивать с baseline и сигналить о регрессиях;
-
проверять строгие пороги и валить сборку;
-
генерировать отчёт, который не стыдно кинуть в PR;
-
хранить результаты как артефакты, чтобы открыть любой прогон из истории.
Locust отлично решает свою задачу — генерировать нагрузку. Headless‑режим для CI, экспорт статистики и отчётов (--csv, --html, --json) — всё это есть из коробки. [6]
Но дальше начинается взрослая жизнь. CI нужны не числа, а решение — падать сборке или нет. Нужны пороги: p95 < X, error_rate < Y — и это должно быть машинно проверяемо. Нужен baseline и сравнение «как было / как стало», потому что абсолютные цифры без контекста обманчивы. И нужен нормальный отчёт, который можно открыть в артефактах и за тридцать секунд понять, что именно поехало.
Можно, конечно, собрать это самому — парочка скриптов, jq, питон, слёзы… Мы решили пойти другим путём и вынести всё это в отдельный слой поверх Locust.
В итоге получился Locomotive — Python‑библиотека с CLI, которая запускает нагрузку через Locust, а сверху добавляет всё то, чего обычно не хватает для CI: декларативные сценарии, baseline‑анализ, пороги и отчёты.
Что есть из коробки:
-
Декларативная конфигурация в JSON/YAML — можно описать сценарий без locustfile.py (хотя он тоже поддерживается, если нужна сложная логика).
-
Генерация конфига из OpenAPI через
loco init --openapi …— удобно, чтобы стартануть быстро и не набивать руками десяток эндпоинтов. -
Gate checks — абсолютные пороги по метрикам (скажем, p95_ms < 500, error_rate < 1). [9]
-
Regression rules — сравнение текущего прогона с baseline по настраиваемым правилам (относительные/абсолютные отклонения, направление, уровень реакции warn/fail). [10]
-
HTML‑отчёт с графиками и дельтами + настраиваемые темы.
-
Пресеты отчёта (например, errors и throughput) — если нужно быстро сфокусироваться на конкретном аспекте.
-
Готовый GitHub Action, который сам ставит пакет, подтягивает baseline, запускает тест, складывает артефакты и комментирует PR.
Дисклеймер: мы не хотим сказать, что другие инструменты плохие. Более того, при разработке собственного решения мы опирались на фичи существующих решений. У Grafana k6 есть thresholds, которые фейлят тест при нарушении условий — штука, задизайненная прямо под CI. [14]
У Taurus есть pass/fail‑критерии. [16]
JMeter умеет CLI/non‑GUI режим (GUI — только для сборки/отладки плана). [18]
Мы просто хотели сохранить Locust‑экосистему и при этом получить CI‑first поведение без самописного зоопарка.
Как устроено внутри
Теперь — к самому вкусному: что происходит между «PR opened» и «сборка упала, потому что p95 поехал».
Компонентная схема архитектуры (CI/CD Pipeline → CLI → Launcher/Analyzer/Reporter/Storage)
Внутри Locomotive логика разбита на несколько компонентов:
CLI — единая точка входа. Через неё происходит всё: «запусти», «сравни», «сгенерь отчёт».
Launcher — запуск Locust с нужными параметрами в headless‑режиме и сбор сырых результатов. По сути, это обёртка над тем, что Locust и так умеет делать — генерировать CSV, JSON‑статы и прочее.
Storage — складывает результаты в артефакты, чтобы CI мог их сохранить и показать.
Analyzer — берёт текущий прогон, сравнивает с baseline и применяет правила. Именно он решает: PASS, WARNING или DEGRADATION. [10]
Reporter — собирает из всего этого HTML‑отчёт, который можно открыть одним кликом в артефактах.
Sequence‑диаграмма (Developer → GitHub Actions → CLI → Locust → Analyzer → Reporter → Storage)
Как считается регрессия
Проверки бывают двух типов, и их полезно разделять.
Первый — gate checks, или абсолютные пороги. Это, по сути, SLA‑ворота: p95_ms не должен быть выше 500 мс, error_rate не должен превышать 2%. Задаёте в конфиге — и всё, пайплайн будет их проверять (пример конфига будет ниже).
Второй — regression rules, или сравнение с baseline. Тут идея другая: не «метрика выше порога», а «метрика стала хуже, чем была». Например, правило из rules.example.json может звучать так: p95_ms не должен вырасти больше, чем на 20% (fail), а рост на 10% — уже warning.
Правила формулируются максимально понятно: mode: relative — сравниваем в процентах; direction: increase — плохо, когда метрика растёт (для latency и error_rate); direction: decrease — плохо, когда падает (для RPS); warn / fail — уровни реакции, от деликатного предупреждения до «режь билд». [10]
Дальше всё просто: если зафиксирована серьёзная деградация (DEGRADATION), CI падает прямо в PR.
Минимальный запуск в CI
Пререквизиты
Установка и генерация конфига
pip install locomotive
loco init
Locomotive ставится из PyPI и требует Python 3.9+. Locust подтягивается как зависимость — отдельно ставить не нужно.
Если у вас есть OpenAPI‑спека, можно сразу сгенерировать конфиг из неё. А если хотите GitHub Actions — и заготовку workflow:
loco init --openapi openapi.json
loco init --github-workflow
Конфиг: гейты и baseline‑правила
В конфиге обычно живут две вещи: абсолютные пороги (gate) и правила сравнения (rules). Выглядит это примерно так: [26]
{
"load": {
"host": "https://staging.example.com",
"users": 20,
"spawn_rate": 5,
"run_time": "1m"
},
"analysis": {
"gate": {
"min_requests": 200,
"thresholds": {
"p95_ms": { "fail": 500 },
"error_rate": { "fail": 2 }
}
},
"rules": [
{ "metric": "p95_ms", "mode": "relative", "direction": "increase", "warn": 10, "fail": 25 },
{ "metric": "rps", "mode": "relative", "direction": "decrease", "warn": 10, "fail": 20 }
],
"fail_on": "DEGRADATION"
},
"report": { "output": "artifacts/report.html" }
}
Полный пример конфига «без locustfile» — со scenario.requests, headers, tags и прочим — лежит в репозитории.
Запуск
loco --config loconfig.json ci
Команда ci — это «полный цикл»: тест → анализ → отчёт. Всё за один вызов.
Если хотите сохранить текущий прогон как baseline (опорную точку для будущих сравнений), добавляете --set-baseline:
loco --config loconfig.json ci --set-baseline
Что вы получаете на выходе
Во‑первых — человекочитаемый ответ: PASS, WARNING или DEGRADATION. Не стена цифр, а один понятный статус. Его же Action возвращает как output, так что на него можно завязывать дальнейшую логику пайплайна.
Комментарий в PR от Action
Во‑вторых — артефакты, по которым можно поднять «историю болезни» без археологии. Структура выглядит так:
artifacts/
├── baseline.json
├── history.json
└── runs/
└── <run_id>/
├── run.json
├── metrics.json
├── analysis.json
├── report.html
└── generated/
Если включить историю (artifacts.history > 0), появляется возможность строить тренды между прогонами. Например, увидеть, что p95 тихонько ползёт вверх последние 20 запусков — до того, как он прорвёт порог и всё загорится красным.
Графики трендов (p95/rps/error_rate по run_id)
Перед тем как тащить нагрузку в пайплайн, полезно честно ответить на два вопроса: где она даст быстрый выхлоп, а где превратится в шум. Ниже — короткий чеклист: когда использовать, когда не использовать, и какие грабли вас всё равно ждут.
Когда использовать
Это хорошо заходит, если у вас:
-
частые PR и релизы, где деградация «по чуть‑чуть» копится незаметно;
-
критичные по UX ручки/флоу (логин, поиск, корзина, платежи), которые хочется защищать так же, как функциональность;
-
есть staging/preprod, на котором можно стабильно гонять короткий smoke и хранить историю прогонов.
Лучше не тащить это в каждый PR, если:
-
нет стабильного окружения (стейджинг постоянно меняется, шумит, «соседи» убивают CPU);
-
нагрузка у вас нужна раз в квартал «проверить потолок», а не ловить регрессии;
Даже если вам «подходит по чеклисту», есть три грабли, от которых не убежит ни один пайплайн.
Ограничения и грабли
-
Locomotive не заменяет Locust. Если вам нужны сложные пользовательские сессии, динамические данные, нестандартные протоколы или хитрые stateful‑сценарии, вы по‑прежнему пишете locustfile.py, а Locomotive выступает «обвязкой качества» поверх него.
-
Locomotive — не распределённый генератор нагрузки. Если одной машины не хватает, масштабирование — это зона ответственности Locust (master/worker или --processes). [32]
-
Шум окружения никуда не девается. Стейджинг бывает капризным, соседний джоб в CI может сожрать весь CPU, сеть может подлагать. Как идея, можно поступить следующим образом: в PR — маленький стабильный смоук с мягкими правилами; в main — более жёсткие gates и, возможно, несколько прогонов с агрегацией. Впрочем, это уже вопрос стратегии, а не инструмента.
Заключение
Нагрузочные тесты перестают быть ритуалом по праздникам, когда у них появляется UX как у обычных тестов: запустил → получил вердикт → при необходимости открыл отчёт и понял, что именно поехало.
Начните с малого: один критичный флоу, короткий smoke на PR, мягкие правила (WARNING важнее, чем ложные фейлы). А дальше уже можно ужесточать пороги и наращивать регрессионную историю — когда пайплайн и окружение к этому готовы.
Ссылки
Автор: daria021
