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






Идея, в общем-то, лежит на поверхности: ловить падение производительности не на проде и не в ночь перед релизом, а ровно в момент, когда оно появились — в PR. Та же самая логика «раннего обнаружения», что и в юнит‑тестах: быстрый фидбек, меньше рисков, меньше ночных алертов. В индустрии это обычно называют shift-left performance testing — нагрузка становится частью пайплайна, а не отдельным ритуалом, который проводят по большим праздникам. [1] [1]
Цель была приземлённая: сделать так, чтобы нагрузочный тест в CI по UX напоминал обычный юнит‑тест. Запустил — получил результат — пошёл дальше.
Если подробнее, нам нужно было получить следующее CI‑поведение:
предсказуемо запускать нагрузку (smoke на PR, длиннее на main/ночью);
сравнивать с baseline и сигналить о регрессиях;
проверять строгие пороги и валить сборку;
генерировать отчёт, который не стыдно кинуть в PR;
хранить результаты как артефакты, чтобы открыть любой прогон из истории.
Locust отлично решает свою задачу — генерировать нагрузку. Headless‑режим для CI, экспорт статистики и отчётов (--csv, --html, --json) — всё это есть из коробки. [6] [2]
Но дальше начинается взрослая жизнь. CI нужны не числа, а решение — падать сборке или нет. Нужны пороги: p95 < X, error_rate < Y — и это должно быть машинно проверяемо. Нужен baseline и сравнение «как было / как стало», потому что абсолютные цифры без контекста обманчивы. И нужен нормальный отчёт, который можно открыть в артефактах и за тридцать секунд понять, что именно поехало.
Можно, конечно, собрать это самому — парочка скриптов, jq, питон, слёзы… Мы решили пойти другим путём и вынести всё это в отдельный слой поверх Locust.
В итоге получился Locomotive [3] — Python‑библиотека с CLI, которая запускает нагрузку через Locust, а сверху добавляет всё то, чего обычно не хватает для CI: декларативные сценарии, baseline‑анализ, пороги и отчёты.
Что есть из коробки:
Декларативная конфигурация в JSON/YAML — можно описать сценарий без locustfile.py (хотя он тоже поддерживается, если нужна сложная логика).
Генерация конфига из OpenAPI через loco init --openapi … — удобно, чтобы стартануть быстро и не набивать руками десяток эндпоинтов.
Gate checks — абсолютные пороги по метрикам (скажем, p95_ms < 500, error_rate < 1). [9] [4]
Regression rules — сравнение текущего прогона с baseline по настраиваемым правилам (относительные/абсолютные отклонения, направление, уровень реакции warn/fail). [10] [5]
HTML‑отчёт с графиками и дельтами + настраиваемые темы.
Пресеты отчёта (например, errors и throughput) — если нужно быстро сфокусироваться на конкретном аспекте.
Готовый GitHub Action, который сам ставит пакет, подтягивает baseline, запускает тест, складывает артефакты и комментирует PR.
Дисклеймер: мы не хотим сказать, что другие инструменты плохие. Более того, при разработке собственного решения мы опирались на фичи существующих решений. У Grafana k6 есть thresholds, которые фейлят тест при нарушении условий — штука, задизайненная прямо под CI. [14] [6]
У Taurus есть pass/fail‑критерии. [16] [7]
JMeter умеет CLI/non‑GUI режим (GUI — только для сборки/отладки плана). [18] [8]
Мы просто хотели сохранить Locust‑экосистему и при этом получить CI‑first поведение без самописного зоопарка.
Теперь — к самому вкусному: что происходит между «PR opened» и «сборка упала, потому что p95 поехал».
Внутри Locomotive логика разбита на несколько компонентов:
CLI — единая точка входа. Через неё происходит всё: «запусти», «сравни», «сгенерь отчёт».
Launcher — запуск Locust с нужными параметрами в headless‑режиме и сбор сырых результатов. По сути, это обёртка над тем, что Locust и так умеет делать — генерировать CSV, JSON‑статы и прочее.
Storage — складывает результаты в артефакты, чтобы CI мог их сохранить и показать.
Analyzer — берёт текущий прогон, сравнивает с baseline и применяет правила. Именно он решает: PASS, WARNING или DEGRADATION. [10] [5]
Reporter — собирает из всего этого HTML‑отчёт, который можно открыть одним кликом в артефактах.
Проверки бывают двух типов, и их полезно разделять.
Первый — 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] [5]
Дальше всё просто: если зафиксирована серьёзная деградация (DEGRADATION), CI падает прямо в PR.
Установка и генерация конфига
pip install locomotive
loco init
Locomotive ставится из PyPI и требует Python 3.9+. Locust подтягивается как зависимость — отдельно ставить не нужно.
Если у вас есть OpenAPI‑спека, можно сразу сгенерировать конфиг из неё. А если хотите GitHub Actions — и заготовку workflow:
loco init --openapi openapi.json
loco init --github-workflow
В конфиге обычно живут две вещи: абсолютные пороги (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, так что на него можно завязывать дальнейшую логику пайплайна.
Во‑вторых — артефакты, по которым можно поднять «историю болезни» без археологии. Структура выглядит так:
artifacts/
├── baseline.json
├── history.json
└── runs/
└── <run_id>/
├── run.json
├── metrics.json
├── analysis.json
├── report.html
└── generated/
Если включить историю (artifacts.history > 0), появляется возможность строить тренды между прогонами. Например, увидеть, что p95 тихонько ползёт вверх последние 20 запусков — до того, как он прорвёт порог и всё загорится красным.
Перед тем как тащить нагрузку в пайплайн, полезно честно ответить на два вопроса: где она даст быстрый выхлоп, а где превратится в шум. Ниже — короткий чеклист: когда использовать, когда не использовать, и какие грабли вас всё равно ждут.
Это хорошо заходит, если у вас:
частые 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
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/451345
Ссылки в тексте:
[1] [1]: https://docs.gitlab.com/ci/testing/load_performance_testing/
[2] [6]: https://docs.locust.io/en/stable/configuration.html
[3] Locomotive: https://github.com/locomotive-lib/locomotive
[4] [9]: https://github.com/locomotive-lib/locomotive/blob/master/loconfig.example-scenario.json
[5] [10]: https://github.com/locomotive-lib/locomotive/blob/master/rules.example.json
[6] [14]: https://grafana.com/docs/k6/latest/using-k6/thresholds/
[7] [16]: https://gettaurus.org/docs/PassFail.md
[8] [18]: https://jmeter.apache.org/usermanual/get-started.html
[9] Locomotive на PyPI: https://pypi.org/project/locomotive/
[10] Источник: https://habr.com/ru/articles/1033590/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1033590
Нажмите здесь для печати.