Ядро
Раннеры
Четыре движка, один интерфейс: каждый раннер возвращает
mpsc::Receiver<LogLine>, который потоково отдаёт вывод в реальном времени и
закрывается по завершении прогона. Выбор движка задаётся через ExecutionPlan.
Нативный движок шагов (step::runner)
Чистый Rust, без внешних бинарников. run_steps(steps, config, tx) выполняет
список шагов под одним из трёх профилей нагрузки, который вычисляется из
конфигурации через step::schedule::Schedule:
- Fixed —
vus+duration:config.vusзадач tokio, каждая из которых циклически выполняет список шагов, пока не истечёт длительность. - Ramping VUs —
stages:(в стиле k6): задача-супервизор каждые ~100 мс пересчитывает целевое число VU линейной интерполяцией между целями ступеней (первая ступень наращивает с 0, каждая следующая — от предыдущей цели). При увеличении нагрузки порождаются новые задачи VU; при уменьшении помечаются самые новые VU, которые завершают текущий шаг и выходят на ближайшей границе шага (плавная остановка). Длительность прогона равна сумме длительностей ступеней. - Arrival-rate —
arrival:(открытая модель): диспетчер вычисляет моменты старта итераций, обращая кусочно-линейный интеграл интенсивности, и выдаёт разрешения пулу воркеров, который стартует сpre_allocated_vus(по умолчанию 1) и лениво растёт доmax_vus. В отличие от закрытых моделей с циклом VU, новые итерации стартуют по расписанию, даже когда тестируемая система замедляется — разрешение, которое некому обслужить (пул насыщен доmax_vus), отбрасывается, учитывается в сводной метрикеdropped_iterationsи логируется не чаще одного раза в 5 с. Счётчик выводится даже при значении 0, поэтому гейтыstd/thresholds@v1видаdropped_iterations: ["count==0"]корректно срабатывают на чистых прогонах, а не падают с ошибкой о неизвестной метрике. Сarrival.co_correct: trueпул ставит разрешения в очередь вместо отбрасывания (семантика zrk/wrk2): подвисание становится задержкой вengine_arrival_iteration_ms, отсчитанной от запланированного старта.
Для прогонов со ступенями/arrival строка vus в сводке показывает
наблюдаемый параллелизм — vus....................: <last> min=<min> max=<max> —
а периодическая строка [stats] дополняется хвостом vus=N с текущим
значением. Прогоны Fixed сохраняют исторические форматы без изменений.
Contextна каждый VU — выводы шагов и интерполяция${{ }}изолированы между VU и сохраняются между итерациями одного VU
HTTP-движок
У std/http@v1 два клиента за одним действием:
- Нативный HTTP/1.1 fast-path. Работает по умолчанию для
http://иhttps://в стандартномpool. Каждый VU пишет запрос одним буфером и разбирает ответ в своей задаче. Keep-alive соединения хранятся в пуле шарда: до 64 простаивающих на хост, закрываются после 60 с простоя. TLS работает на rustls/ring с корнями webpki, а приinsecure: trueпринимает любой сертификат. Формат запроса и вывод такие же, как у reqwest:accept,accept-encoding: gzipс прозрачной распаковкой,host,content-length, тела chunked и до EOF,timeoutшага иmax_response_body_bytes. - reqwest обрабатывает всё остальное:
multipart,source_ips, логин в URL,pool: shared/pool: dedicatedи продолжение редиректа (начиная с первого 3xx).
Ещё две оптимизации включаются автоматически:
- Статичные шаги. Если в исходном
with:нет ни одного${…}(ни интерполяции, ни генераторов), собранный запрос переиспользуется. - Облегчённый вывод. Если ни один шаг не читает
__last__и нет отдельного шагаstd/check, то шаг безoutputs:/check:сохраняет толькоstatusиduration_ms. Тело всё равно читается целиком, но не декодируется.
VU работают на однопоточном event loop, по одному на шард HTTP-клиента (модель gcannon/zrk), а не на runtime с work-stealing.
| Переменная окружения | Эффект |
|---|---|
PERFSCALE_HTTP_CLIENT=reqwest | Отключить fast-path, все запросы идут через reqwest |
PERFSCALE_VU_RUNTIME=shared | Запускать VU на общем многопоточном runtime (исполнитель до 0.32) |
Маркер завершения в потоках агента
Когда прогон выполняется через машинного агента (perfscaled), поток логов
каждого движка завершается ровно одной системной строкой после всего вывода
движка:
[sys] __perfscale_exit__=<code|signal|unknown>
code— код завершения движка (0= успех; нативный движок возвращает1при нарушенном гейте пороговseverity: failили при некорректном профиле нагрузки)signal— процесс движка был завершён сигналом (например, OOM killer)unknown— код завершения определить не удалось
Потребителям следует считать маркер авторитетным результатом прогона: шум в
stderr (например, k6 console.log) не означает неудачу, если код
завершения равен 0. Потребители старше агента 0.3.6 не видят маркера и
возвращаются к эвристикам; старые потребители, читающие поток нового агента,
просто выводят эту строку как обычное системное сообщение.
- Тайминги HTTP из
std/http@v1питают общие метрики; другие действия (WebSocket, gRPC, TCP/UDP) вносят счётчики и гистограммы задержек (например,ws_msg_rtt,grpc_req_duration) через тот же коллектор - Завершается k6-совместимым блоком сводки +
Done — Xs wall clock vus: 0приводится к 1; строки длительности парсятся черезparse_duration_ms("90","500ms","1.5s","1m30s","1h"— разрешение в миллисекундах, минимум 1 мс). Длительности ступеней проверяются строго: непарсящиеся или нулевые значения завершают прогон (иperfscale lint) с понятной ошибкой, так же как и сочетаниеstagesсarrival, иarrivalбезmax_vus >= 1
k6 (runner::k6)
Обёртка вокруг существующей установки k6:
- скрипт записывается в
$TMPDIR/perfscale-<uuid>.js - запускается
k6 run --no-color <script>с перенаправленными stdio - stdout/stderr стримятся как
LogLine; временный файл удаляется на выходе
Два режима:
| Функция | Возвращает | Применение |
|---|---|---|
run_streaming(script) | Receiver<LogLine> | вывод в реальном времени |
run_oneshot(script) | RunResult { exit_code, success, stdout, stderr, script } | сначала собрать, потом проверить |
Конфигурация нагрузки (VU, ступени, пороги) задаётся в собственном блоке
options скрипта — perfscale не подставляет флаги k6.
Отсутствующий бинарник → k6 not found in PATH — install from https://k6.io/....
locust (runner::locust)
Обёртка вокруг существующей установки locust в headless-режиме:
locust -f <script> --headless -u <users> -r <spawn_rate> -t <duration> --csv <tmp-prefix> [--host <host>]
LocustOpts { users, spawn_rate, duration, host } маппится из общего
RunConfig через LocustOpts::from_run_config (vus → users и spawn_rate).
Во время прогона собственные stdout/stderr locust стримятся напрямую. После
выхода раннер парсит строку Aggregated из <prefix>_stats.csv и выводит тот
же блок сводки, что и остальные движки:
http_req_duration......: avg=42.50ms p(50)=40ms p(90)=60ms p(95)=68ms p(99)=85ms min=10ms max=120ms
http_req_failed........: 2.00%
http_reqs..............: 100 10.50/s
Временные CSV-файлы (_stats, _stats_history, _failures, _exceptions)
удаляются после завершения. Отсутствующий или усечённый CSV даёт строку
system (failed to read locust stats: ...), а не ошибку — вывод процесса
уже был отстримлен.
Отсутствующий бинарник → locust not found in PATH — install with pip install locust.
JMeter (runner::jmeter)
Обёртка вокруг существующей установки jmeter в не-GUI режиме:
jmeter -n -t <plan.jmx> -j <temp>/jmeter.log -l <temp>/results.jtl -J…
План полностью определяет форму нагрузки (группы потоков, таймеры,
пропускная способность); свойства -J key=value параметризуют его из CLI.
Сэмплы прогона попадают в приватный CSV .jtl с принудительным набором
полей (saveservice-свойства раннера идут последними, поэтому
пользовательские свойства не могут его сломать); оба временных файла
удаляются после прогона. Во время выполнения собственные stdout/stderr
jmeter стримятся напрямую, а с report.during_run: true из .jtl
стримятся кумулятивные снапшоты метрик. После выхода .jtl складывается в
общее хранилище метрик для полной k6-совместимой сводки:
http_req_duration......: avg=4.00ms p(50)=3.00ms p(90)=6.00ms p(95)=8.00ms p(99)=21.00ms min=1.00ms max=42.00ms
http_req_failed........: 0.25%
http_reqs..............: 1200 80.0/s
Серии по JMeter-label (ограниченная кардинальность, bucket переполнения
__jmeter_other__), http_req_latency/http_req_connect и
data_received/data_sent живут в том же хранилище, поэтому для прогонов
JMeter работают гейты std/thresholds@v1 из after:, отчёты
--report json/junit/html и потоковые репортёры. План с нулевым числом
сэмплов откатывается на трансляцию консольной summary = (только
avg/min/max); повреждённые строки .jtl пропускаются с предупреждением,
.jtl без достоверных строк (или с невалидным заголовком) либо нарушенный
гейт fail завершают прогон с кодом 1. abort_on_fail не поддерживается (отклоняется с понятной
ошибкой) — прогоном управляет jmeter, гейты вычисляются после.
Отсутствующий бинарник → jmeter not found in PATH — install from https://jmeter.apache.org/download_jmeter.cgi (requires a JRE).
Выбор движка
| native | k6 | locust | JMeter | |
|---|---|---|---|---|
| Требуется установка | не нужна | бинарник k6 | python + locust | JRE + jmeter |
| Язык сценариев | шаги YAML | JavaScript | Python | тест-план .jmx (XML) |
| Возможности скриптинга | низкие (4 действия) | высокие | высокие | высокие |
| Модель нагрузки | фиксированные VU, stages: с ростом или arrival-rate | stages/thresholds/scenarios | users/spawn-rate | группы потоков/таймеры (в плане) |
| Лучше всего для | смоук-тесты, гейты CI, простые API-сценарии | сложные наборы k6, которые у вас уже есть | команды с фокусом на Python | существующие наборы JMeter |