Обзор
Справочник по YAML
Нативный запуск управляется двумя файлами: определение теста (-f)
описывает, что делать; конфиг (-c) описывает, сколько нагрузки
подавать.
Оба файла валидируются по JSON Schema до выполнения — ошибки указывают на
путь конкретного поля, а не на сырой вывод парсера. Схемы лежат в
schema/ и могут включать автодополнение в редакторе через
модлайн:
# yaml-language-server: $schema=https://raw.githubusercontent.com/Perfscale/perfscale/main/schema/test.schema.json
Определение теста (-f test.yaml)
Один массив steps. Каждый виртуальный пользователь (VU) выполняет весь
список в цикле, пока не истечёт заданная длительность. Опциональный
верхнеуровневый import: наследует базовый документ — см.
Композиция документов. Опциональный
верхнеуровневый libraries: объявляет библиотеки генераторов значений для
токенов ${alias.fn(...)} — см. Библиотеки.
steps:
- name: login # необязательная метка для вывода в лог
use: std/http@v1 # обязательно — идентификатор действия
with: # параметры действия (см. ниже)
method: POST
url: https://api.example.com/login
body:
user: demo
check: # необязательные инлайн-проверки
status: 200
duration_ms_lt: 500
outputs: login # необязательно — сохранить вывод для следующих шагов
Поля шага
| Поле | Обязательно | Описание |
|---|---|---|
use | да | Идентификатор действия: std/http@v1, std/graphql@v1, std/tcp@v1, std/udp@v1, std/pubsub@v1, std/llm@v1, std/ws@v1, std/ws-connect@v1, std/ws-send@v1, std/ws-recv@v1, std/ws-ping@v1, std/ws-close@v1, std/grpc@v1, std/grpc-connect@v1, std/grpc-call@v1, std/grpc-stream-open@v1, std/grpc-stream-send@v1, std/grpc-stream-recv@v1, std/grpc-stream-close@v1, std/db-connect@v1, std/db-query@v1, std/db-tx-begin@v1, std/db-tx-commit@v1, std/db-tx-rollback@v1, std/db-close@v1, std/check@v1, std/random@v1, std/sleep@v1, std/log@v1, std/file-read@v1, std/read-json@v1, std/pick@v1, std/file-write@v1, std/child_process@v1, std/kill_process@v1, std/thresholds@v1, std/set_shared_variable@v1, std/get_shared_variable@v1 (также работают короткие алиасы http, graphql, tcp, udp, pubsub, llm, ws, ws-connect, ws-send, ws-recv, ws-ping, ws-close, grpc, grpc-connect, grpc-call, grpc-stream-open, grpc-stream-send, grpc-stream-recv, grpc-stream-close, db-connect, db-query, db-tx-begin, db-tx-commit, db-tx-rollback, db-close, check, random, sleep, log, file-read, read-json, pick, file-write, child_process, kill_process, thresholds, set_shared_variable, get_shared_variable). uses: принимается как алиас для use: |
name | нет | Человекочитаемая метка, отображаемая в строках лога |
with | нет | Параметры действия — см. Действия |
check | нет | Проверки вывода этого шага — те же ключи, что у std/check@v1 |
outputs | нет | Имя переменной, под которой сохраняется вывод шага |
severity | нет | Только для std/thresholds@v1: во что превращается нарушенный порог — fail (по умолчанию; запуск завершается с ненулевым кодом), warn или info |
message | нет | Только для std/thresholds@v1: пользовательская метка, добавляемая к сводке нарушений (интерполируется) |
abort_on_fail | нет | Только для std/thresholds@v1-шагов в after: (по умолчанию false): ранний останов в стиле k6 — пока работают VU, монитор переоценивает порог по уже собранным метрикам каждые 250 мс, и нарушенный порог с severity: fail досрочно останавливает всю нагрузку (между шагами; after:, репортеры и сводка отрабатывают как обычно). См. Останов по порогу и диагностика ошибок |
http_debug | нет | Только вместе с abort_on_fail: true на after:-шаге std/thresholds@v1: error (пока единственный режим) пишет одну маскированную строку http_debug: {…} на каждое упавшее действие и каждый упавший check: во время фазы VU — id VU, итерация, интерполированные параметры запроса (включая тело) и вывод ответа (включая тело). Успешные запросы и проверки ничего не выводят |
labels | нет | Метки метрик уровня шага (скаляры; ${{ inputs.* }} / ${{ env.* }} интерполируются один раз до запуска): проставляются на всех рядах метрик шага для стриминг-репортеров, приоритет слияния step > reporter > run. run_id и worker зарезервированы (проставляет движок) |
if / when | нет | Выполнять шаг только при истинном условии — см. Условные шаги |
Условные шаги (if: / when:)
if: и when: — эквивалентные алиасы; одновременное задание обоих на одном
шаге является ошибкой разбора. Шаг пропускается, если условие ложно. Сначала
раскрываются ${{ … }} и ${…}; результат — сравнение (==, !=, <,
<=, >, >= — числовое, если обе стороны являются числами, иначе
строковое; литералы можно заключать в '…') или простое значение, ложное
при пустой строке, false, 0 или null. Условия объединяются через &&
и || (&& имеет приоритет, скобки не поддерживаются). Пропущенный шаг не
создаёт результат и метрики и не меняет __last__.
steps:
- use: std/random@v1
when: "${{ __iter__ }} == 0"
with: { type: float, min: 0, max: 5 }
outputs: startup_delay
- use: std/sleep@v1
when: "${{ __iter__ }} == 0" # только первая итерация
with: { seconds: "${{ startup_delay }}" }
- use: std/http@v1
with: { url: "https://api.example.com/feed" }
outputs: feed
- use: std/random@v1
with: { type: float, min: 0, max: 1 }
outputs: like_roll
- use: std/http@v1
when: "${{ feed.status }} == 200 && ${{ like_roll }} < 0.15"
with: { method: POST, url: "https://api.example.com/like" }
Переменные (${{ ... }})
Шаги передают данные последующим шагам через плейсхолдеры в стиле GitHub
Actions. Шаг сохраняет свой вывод под именем, заданным в outputs:; любое
строковое значение в with: или check: последующего шага может затем
ссылаться на него:
| Выражение | Раскрывается в |
|---|---|
${{ name }} | Весь сохранённый вывод, приведённый к строке |
${{ name.field }} | Одно поле сохранённого объекта (например, .status, .body, .duration_ms) |
${{ name.a.b }} | Вложенный путь — один уровень JSON на каждую ., например ${{ resp.headers.x-request-id }} |
${{ name.arr.0 }} / ${{ name.arr[0] }} | Индексация массивов — числовой сегмент индексирует массив; точечный и скобочный синтаксис взаимозаменяемы |
${{ name.body.a[0].b }} | Путь, продолжающийся после строкового листа, парсит его как JSON по требованию — например, HTTP body — и идёт дальше по документу |
${{ __last__ }} / ${{ __last__.field }} | Вывод непосредственно предыдущего шага — всегда доступен, outputs: не нужен |
${{ config.<name>.<field> }} | Вывод шага инициализации before: из конфиг-файла (см. Конфиг) |
${{ vars.<key> }} | Статическое значение из блока variables: конфиг-файла |
${{ inputs.<name> }} | Разрешённое типизированное значение из блока inputs: конфига (см. Типизированные входы) |
${{ env.NAME }} | Переменная окружения процесса NAME — для секретов (api_key, токенов, DSN), которые не должны храниться в YAML |
${{ __vu__ }} / ${{ __iter__ }} | 1-based id текущего VU и 0-based номер его итерации (аналоги __VU / __ITER в k6). Сохранённый результат с таким же именем имеет приоритет |
Для std/http@v1 сохранённый вывод имеет вид
{ "status": <int>, "body": <string>, "duration_ms": <float>, "headers": <object> }
— имена заголовков приводятся к нижнему регистру, поэтому заголовок сессии
или токена из ответа 1 может перейти в запрос 2:
steps:
- use: std/http@v1
with: { url: "https://api.example.com/first" }
outputs: r1
- use: std/http@v1
with:
url: "https://api.example.com/second"
headers:
x-session: "${{ r1.headers.x-session }}"
steps:
- name: login
use: std/http@v1
with:
method: POST
url: "https://api.example.com/token"
body: { user: demo }
outputs: auth # ← сохраняется как `auth`
- name: fetch profile
use: std/http@v1
with:
url: "https://api.example.com/me"
headers:
authorization: "Bearer ${{ auth.body }}" # вложенные значения работают
check:
body_contains: "${{ auth.body }}" # и в check тоже
- use: std/log@v1
with:
message: "login took ${{ auth.duration_ms }}ms → ${{ auth.status }}"
Правила и граничные случаи:
- Плейсхолдеры работают в строковых значениях на любой глубине
with/check— во вложенных объектах (заголовки), элементах массивов, телах запросов. Ключи никогда не интерполируются. - Пробелы внутри фигурных скобок игнорируются:
${{auth.status}}≡${{ auth.status }}. - Отсутствующая переменная или поле раскрываются в пустую строку —
запуск не падает. Проверяйте значение через
check:, если отсутствие должно быть ошибкой. Исключение:${{ env.NAME }}с неустановленнымNAMEпроваливает шаг с ошибкойenv var 'NAME' is not setдо запуска действия — иначе молча пустойapi_keyпроявился бы как непонятный 401 от API. ${{ env.NAME }}читает окружение процессаperfscale(всё послеenv.— это имя переменной).env— зарезервированный префикс: сохранённый вывод с именемenvбудет им перекрыт. Раскрытые значения подставляются только в параметры шагов — движок сам никогда не пишет их в логи или сводки. Он также записывает каждое раскрытое значениеenv.*в реестр уровня запуска и маскирует его в логе запуска: значения длиной 4+ символа заменяются на***везде, где встречаются (в том числе внутри URL и DSN), более короткие — только как целые токены, — так что секрет, случайно попавший в URL запроса или сообщениеstd/log@v1, всё равно не утечёт в вывод. Именно поэтомуenv.*— правильный канал для секретов.- Сегменты пути спускаются на один уровень JSON-объекта за каждую
.— заголовки с точками в имени адресовать нельзя (редкий случай; всё остальное работает). - Плейсхолдеры раскрываются отдельно для каждого виртуального пользователя и каждой итерации — каждый VU видит выводы только своей цепочки шагов, никогда чужой.
- Шаги без
${{выполняются как есть: движок полностью пропускает проход интерполяции, поэтому обычные шаги не платят за эту возможность ничем. - Кавычки в YAML: работают и без кавычек (
Bearer ${{ auth.body }}), и в кавычках ("${{ auth.body }}"); ставьте кавычки, если значение начинается с символа, который YAML трактует специально ({,[,*, …).
Конфиг (-c config.yaml)
vus: 10 # виртуальные пользователи, по умолчанию 1
duration: 5m # "30s", "500ms", "1.5s", "1m", "5m30s", "1h" — по умолчанию "1m"
report: # необязательно — пересылка в serve + отчёты в конце запуска
url: http://localhost:7999
during_run: true # также стримить накопительные снапшоты во время работы
labels: # метки запуска в идентичности отчёта report@v1
testid: smoke
reporters: # отчёты в конце запуска (report@v1) — use:/with: как у шагов
- use: std/report/json@v1
with: { path: report.json }
- use: std/report/junit@v1
with: { path: junit.xml }
- use: std/report/html@v1 # самодостаточная тёмная HTML-страница, открывается офлайн
with: { path: report.html }
- use: std/report/json@v1
with:
url: https://collector.example.com/reports # HTTP push
headers: { Authorization: "Bearer ${REPORT_TOKEN}" } # ${VAR} резолвится на хосте, никогда не логируется
max_samples: 10000 # сырые сэмплы по шагам (по умолчанию 100000, 0 отключает)
- use: wasm:./reporters/summary.wasm # WASM-репортер (компонент perfscale:reporter@0.1.0)
with:
output: ./reports # куда попадают файлы emit-file гостя (по умолчанию ./perfscale-report-<module>)
config: { style: short } # произвольный блок, передаётся гостю как есть
- use: std/report/prometheus-rw@v1 # стриминг: Prometheus remote write
during_run: true # ключи движка — рядом с use:
interval_ms: 5000
labels: { env: staging } # метки репортера (step > reporter > run)
max_series: 10000 # защита от кардинальности (по умолчанию 10000)
with:
url: http://alloy:8429/api/v1/metrics/write
histogram: classic # classic (_bucket{le}) | native
retry: { max_attempts: 5, backoff: 1s..30s }
max_pending: 24 # ограниченная очередь; переполнение теряет СТАРЕЙШИЙ снапшот
final_timeout: 30s # предел синхронного финального сброса
- use: std/report/otel@v1 # стриминг: OTLP/gRPC коллектор
during_run: true
with: { endpoint: http://otel-collector:4317 }
- use: std/report/serve@v1 # стриминг: инстанс perfscale serve
during_run: true
with: { url: http://localhost:7999 }
gpu: # необязательно — метрики GPU во время запуска (нативный движок)
enabled: true
interval_ms: 1000
source: nvidia-smi
| Поле | По умолчанию | Описание |
|---|---|---|
vus | 1 | Одновременные виртуальные пользователи (фиксированный профиль) |
duration | 1m | Длительность запуска по реальному времени; голые числа — секунды; доли секунды ("500ms", "1.5s") поддерживаются нативным движком |
stages | — | Профиль ramping-VU (в стиле k6): список стадий { duration, target }. Переопределяет vus/duration; взаимоисключающий с arrival. Только нативный движок |
arrival | — | Профиль arrival-rate (открытая модель): { max_vus, pre_allocated_vus?, co_correct?, stages: [{ duration, rate }] } — поддержание заданной частоты итераций/с. Взаимоисключающий со stages. Только нативный движок |
report.url | — | Базовый URL инстанса perfscale serve; флаг CLI --report переопределяет его |
report.during_run | false | Также стримить накопительные снапшоты метрик во время запуска (батчевые POST на <url>/api/v1/metrics), а не только сводку в конце — см. Метрики во время запуска. Только нативный движок |
report.interval_ms | 5000 | Интервал снапшотов/сброса во время запуска, миллисекунды (мин. 100) |
report.batch_size | 500 | Сбрасывать батч, как только в нём накопится столько сэмплов |
report.max_cpu_percent | 90 | Порог по CPU (стриминг в serve): пока загрузка CPU хоста ≥ этого значения, доставка приостанавливается, а снапшоты копятся в ограниченной очереди (переполнение теряет старейшие после max_pending); 0 отключает; вне Linux не действует |
report.max_pending | 24 | Ограниченный бэклог для legacy-стрима в serve: переполнение теряет СТАРЕЙШИЙ снапшот/батч и считает это в perfscale_report_dropped_snapshots_total (значения кумулятивны — следующий снапшот их восстанавливает) |
report.labels | — | Метки запуска (ключ → значение) в идентичности запуска report@v1 |
report.reporters | — | Репортеры: [{ use, during_run?, interval_ms?, labels?, max_series?, max_samples?, with }]. Ключи движка — рядом с use:: during_run включает экспорт во время запуска для стриминг-репортера (final-only репортеры его отклоняют), interval_ms переопределяет интервал снапшотов для репортера, labels — метки уровня репортера, max_series (по умолчанию 10000) ограничивает число выходных рядов (превышение считается в perfscale_report_dropped_series_total{reporter}), max_samples ограничивает сырые сэмплы по шагам (вынесено из with:; старый with.max_samples по-прежнему читается). Стриминг-встроенные: std/report/serve@v1 (путь report.url как репортер), std/report/prometheus-rw@v1 (with.url, with.histogram: classic|native) и std/report/otel@v1 (with.endpoint, OTLP/gRPC) — см. Стриминг-репортеры. Параметры доставки в with: (только стриминг — на final-only репортерах отклоняются): retry: { max_attempts, backoff: "1s..30s" }, batch_size (по умолчанию — report.batch_size уровня запуска; батчи никогда не превышают его — слишком большой снапшот разбивается), max_pending, final_timeout: "30s" (при закрытии бэклог схлопывается к финальному кумулятивному снапшоту — доставляется только он). Финальные репортеры (report@v1): [{ use, with }]. Встроенные: std/report/json@v1 (канонический JSON), std/report/junit@v1 (JUnit XML — testsuite = запуск, testcase на шаг, failures из вердиктов thresholds) и std/report/html@v1 (одна самодостаточная HTML-страница — тёмная тема, идентичность запуска, вердикты, таблица шагов со спарклайнами сэмплов, семейства метрик; весь CSS/JS инлайн, открывается офлайн). Назначение: ровно одно из with.path (файл) или with.url (HTTP push; в with.headers можно использовать ${VAR} — резолвятся на хосте, не логируются). with.max_samples ограничивает сырые сэмплы по шагам (по умолчанию 100000, 0 отключает; при превышении шаг помечается truncated: true). use: wasm:./path.wasm подключает WASM-репортер (perfscale:reporter@0.1.0, см. wit/reporter/reporter.wit; относительные пути — от файла-источника): гость получает документ report@v1 и свой with.config, а вывод пишет только через host-функцию emit-file в with.output (path traversal отклоняется; WASI fs/net не выдаются). Ошибки конфигурации (неизвестный репортер, нет назначения, не установлен ${VAR}, нечитаемый WASM-модуль) фейлят lint/validate до старта; ошибки доставки — best-effort (engine_report_errors), запуск завершается |
gpu | — | Сбор метрик GPU — см. Метрики GPU. Только нативный движок |
engine_metrics | — | Самонаблюдаемость движка — см. Собственные метрики движка. Только нативный движок |
before | [] | Одноразовые шаги инициализации — см. Инициализация и переменные |
after | [] | Одноразовые шаги завершения — см. Завершение |
variables | {} | Статические значения, доступные шагам как ${{ vars.* }}. Не храните секреты в этом блоке — это обычный YAML, попадающий в репозитории и диффы; для них используйте ${{ env.NAME }} (окружение процесса, маскируется в логах запуска) |
shared_variables | {} | Изменяемое общее состояние между VU для std/set_shared_variable@v1 / std/get_shared_variable@v1: отображение имя → начальное JSON-значение (тип выводится из него). Объявление обязательно — шаг, ссылающийся на необъявленное имя, или op, несовместимая с объявленным типом, проваливает валидацию до старта запуска. См. руководство по общим переменным. Только нативный движок |
allow_process_actions | false | Разрешить шагам запускать/сигналить процессам ОС (std/child_process@v1, std/kill_process@v1). Fail-closed: список шагов из недоверенного источника не может трогать процессы, пока вы явно не разрешите |
allow_library_capabilities | false | Разрешить записям libraries: нести непустые выдачи capabilities:. Fail-closed, тот же паттерн, что у allow_file_actions. Выдача process (RFC 009, опосредованный хостом exec из WASM-шага) дополнительно требует allow_process_actions: true |
seed | — | Seed детерминизма уровня запуска: каждый генератор ${…} и экземпляр библиотеки выводит свой seed как hash(seed, vu_id, conn_seq), поэтому запуск с seed воспроизводит те же сгенерированные значения. Реальное время остаётся недетерминированным |
max_response_body_bytes | 16777216 (16 МиБ) | Лимит размера тела ответа для std/http@v1 / std/graphql@v1. Больший ответ завершает шаг понятной ошибкой (никогда не обрезается молча — обрезанное тело могло бы пройти check:). Только нативный движок |
libraries | — | Библиотеки генераторов значений для токенов ${alias.fn(...)} — см. Библиотеки |
import | — | Базовый документ для наследования — см. Композиция документов |
Типизированные входы (inputs:)
Конфиг может объявлять типизированные входы запуска (issue #11); шаги
ссылаются на них через ${{ inputs.<name> }} везде, где работает
интерполяция ${{ ... }}, а CLI переопределяет их при запуске:
inputs:
users: number # короткая форма — обязателен, без default
host: { type: string, default: localhost }
verbose: { type: bool, default: false } # coercion: 0/1/false/true
env: { type: choice, options: [staging, prod], default: staging }
pacing_ms: { type: number, min: 1, max: 10000 } # границы min/max (только number)
perfscale run -f test.yaml -c config.yaml -i users=10 -i env=prod
perfscale run -f test.yaml -c config.yaml -i users=10,env=prod # форма через запятую
perfscale run -f test.yaml -c config.yaml -e staging.env # env-файл KEY=VALUE
perfscale lint config.yaml -i users=10 # печатает разрешённые значения
Правила:
- Приоритет (высший побеждает):
-i key=value> env-файл-e>default:из объявления. env-файл может содержать посторонние переменные — используются только ключи объявленных входов. - Обязательность: вход без
default:и без переопределения — жёсткая ошибка до старта. Неизвестные ключи-i, несовпадение типа, выход за границы и невалидныеchoice— тоже жёсткие ошибки, на lint и на старте. - Запятая в значении: экранируется как
\,(бэкслеш —\\). - Входы фиксированы на весь запуск. Типизированные скаляры конфига (
vus:,duration:) остаются статически типизированными — входы в профиле нагрузки это follow-up; сегодня входы интерполируются вwith:/check:.
Метрики GPU (gpu:)
Сэмплирование GPU хоста (utilization, VRAM, температура, мощность) на
протяжении всего запуска с секцией gpu в сводке запуска — в первую очередь
чтобы коррелировать нагрузку std/llm@v1 с состоянием GPU.
По умолчанию выключено; сбои сбора (нет GPU, отсутствуют утилиты) пишут одно
предупреждение и никогда не проваливают запуск.
gpu:
enabled: true
interval_ms: 1000 # по умолчанию 1000 (мин. 10)
source: nvidia-smi # nvidia-smi (по умолчанию) | dcgm
dcgm_url: http://127.0.0.1:9400/metrics # для source: dcgm
devices: [0, 1] # необязательно; по умолчанию — все GPU
Полное руководство: core/gpu.md.
Собственные метрики движка (engine_metrics:)
Движок измеряет тестируемую систему, но по умолчанию не измеряет себя. С
engine_metrics.enabled: true задача-пробник просыпается каждые
interval_ms во время фазы VU и записывает собственные накладные расходы
движка в сводку запуска:
engine_scheduler_lag_ms(гистограмма) — лаг планировщика tokio: насколько позже расписания проснулся пробник. Голодание рантайма видно здесь раньше, чем в перцентилях нагрузки.engine_log_channel_depth_max(gauge) — пиковое число ожидающих строк в ограниченном канале лога запуска (ёмкость 512). Значение, близкое к ёмкости, означает, что логирование шагов тормозит VU (back-pressure).
engine_metrics:
enabled: true
interval_ms: 100 # по умолчанию 100 (мин. 10) — также разрешение лага
По умолчанию выключено: без enabled: true ничего не запускается, обычный
запуск ничего не платит. Только нативный движок.
Профили нагрузки
Нативный движок поддерживает три профиля нагрузки. stages/arrival
переопределяют vus/duration (lint предупреждает, если задано и то, и
другое) и взаимоисключают друг друга; длительность запуска — сумма
длительностей стадий.
Фиксированный (по умолчанию): vus воркеров крутят шаги в цикле в
течение duration.
vus: 10
duration: 5m
Ramping VUs (stages:, в стиле k6): целевое число VU линейно
интерполируется между целями стадий — первая стадия поднимается с 0, каждая
следующая — от target предыдущей стадии. Снижение происходит плавно: VU,
который убирается, завершает текущий шаг и выходит на следующей границе шага.
stages:
- { duration: 30s, target: 10 } # рост 0→10 VU
- { duration: 1m, target: 50 } # рост 10→50 VU
- { duration: 30s, target: 0 } # снижение до нуля
Arrival-rate (arrival:, открытая модель): движок поддерживает профиль
частоты итераций в секунду и масштабирует пул воркеров, чтобы успевать, —
новые итерации стартуют по расписанию, даже когда тестируемая система
замедляется (там, где цикл с фиксированным числом VU растянулся бы). rate
линейно растёт между стадиями (допустимы дроби: 0.5 = одна итерация каждые
2 с).
arrival:
max_vus: 100 # потолок пула воркеров — обязательно, ≥ 1
pre_allocated_vus: 10 # воркеры, созданные заранее (по умолчанию 1); пул растёт лениво до max_vus
stages:
- { duration: 30s, rate: 5 } # рост 0→5 итераций/с
- { duration: 1m, rate: 20 } # рост 5→20 итераций/с
Разрешение, пришедшее, пока все max_vus воркеров заняты, отбрасывается и
учитывается в сводной метрике dropped_iterations (плюс предупреждение в
лог не чаще раза в 5 с) — увеличьте max_vus или снизьте частоту, если она
растёт.
По умолчанию co_correct: false. co_correct: true включает коррекцию
coordinated omission в стиле zrk/wrk2:
разрешения при насыщенном пуле ставятся в очередь, а не отбрасываются, поэтому
подвисание видно как задержка в engine_arrival_iteration_ms /
engine_arrival_queue_wait_ms (обе считаются от запланированного старта), а
не пропадает в dropped_iterations; диспетчер при этом выдерживает темп с
субмиллисекундной точностью (отдельный поток-пейсер спит почти до момента
старта и досчитывает только последние ~20 мкс). Отброшенными считается только очередь, оставшаяся к дедлайну.
Включайте, когда вопрос — «какую задержку увидит пользователь, приходящий с
такой частотой»; без опции сохраняется k6-совместимая семантика отбрасывания.
arrival:
max_vus: 1024
co_correct: true # по умолчанию false (отбрасывание при насыщении, как в k6)
stages:
- { duration: 1s, rate: 10000 }
- { duration: 20s, rate: 10000 }
Для запусков со stages/arrival строка vus в сводке показывает
наблюдаемый параллелизм (vus....................: <last> min=<min> max=<max>),
периодическая строка [stats] получает в конце поле vus=N, а экспорты
сводки (--summary-export) возвращают vus: null с duration, равным сумме
длительностей стадий.
Длительности стадий используют ту же грамматику "500ms"/"30s"/"1m30s"/"1h",
что и duration, но валидируются строго: нечитаемая или нулевая длительность
стадии проваливает запуск (и perfscale lint) с понятной ошибкой. stages и
arrival доступны только в нативном движке — с --locust они отклоняются
(используйте vus/duration), с --k6 конфиг-файл всё равно игнорируется.
См. examples/ramping.config.yaml,
examples/spike.config.yaml и
examples/arrival-rate.config.yaml.
Библиотеки (libraries:)
Библиотеки предоставляют функции-генераторы значений для токенов ${...}
(RFC 005) — настоящие UUID, ULID, данные в стиле faker, даты — помимо
встроенных ${seq}/${rand(a,b)}/${uuid}/${now}. Объявляются в
конфиг-файле, файле теста или в обоих (объявления конкатенируются;
дублирующийся алиас — ошибка валидации):
libraries:
- use: '@std/random@v1' # встроенная; алиас по умолчанию: random
capabilities: [] # обязательный ключ — [] означает «без разрешений»
- use: '@std/random@v1'
as: ids # алиас → префикс токена ${ids.fn(...)}
capabilities: []
- use: ./libs/fixer-ids.wasm # локальный WASM-компонент (путь относительно этого файла)
capabilities: []
- use: 'https://vendor.example.com/fixer-ids.wasm' # удалённый: perfscale install
sha256: '9f2c…64 hex…'
capabilities: []
- use: 'git+https://github.com/org/repo.git@v1.2.3#libs/fixer-ids.wasm' # git: perfscale install
capabilities: []
Полезные нагрузки в действиях, раскрывающих ${...} (http, ws, gRPC,
GraphQL, tcp, udp, llm, параметры db, pubsub, file-write — всё, кроме
std/log), могут затем вызывать функции библиотеки:
with:
send: '{ "id": "${random.ulid()}", "who": "${random.email()}", "n": "${ids.int(1000,9999)}" }'
Правила:
- Встроенные токены (
${seq},${rand}, …) сопоставляются первыми и не изменяются. Алиас не может перекрывать их имена. - Неизвестный алиас → токен остаётся как есть. Известный алиас + неизвестная функция (или неверные аргументы) → шаг проваливается — опечатка не должна молча попасть в полезную нагрузку.
- Аргументы отображаются текст → JSON по фиксированному контракту: разбиение
по запятым вне двойных кавычек, обрезка пробелов, снятие одной пары
окружающих кавычек, разбор как JSON при возможности, иначе — строка. Так
pick(a|b|c)передаёт одну строку"a|b|c", аint(1,100)— два числа. - Опциональный завершающий аргумент
keyлюбой функции мемоизирует результат в пределах одного сообщения:${random.uuid4(order)}, встретившийся дважды в одном сообщении, даёт один id; следующее сообщение генерирует новый. seed: 42в конфиге делает запуск воспроизводимым: seed экземпляров выводятся какhash(seed, vu_id, conn_seq).capabilities:обязателен в каждой записи — явный пустой список (capabilities: []) означает «без разрешений»; пропущенный ключ — ошибка валидации при загрузке,lintи запуске. Непустые разрешения (fs,clock,processили{ net: [hosts] }) требуютallow_library_capabilities: true(fail-closed, тот же паттерн, что уallow_file_actions). Выдачаprocess(RFC 009: опосредованный хостом одноразовыйexecWASM-шага) дополнительно требуетallow_process_actions: true.@std/random@v1не объявляет никаких capability — выдача ей любых — ошибка валидации. Библиотека, чейinfo()сообщает"pure": true, не нуждается в разрешениях для импортовwasi:filesystem/wasi:clocks/wall-clock(шум тулчейна jco) — и не получает preopen'ов, поэтому реальный доступ к файлам падает в рантайме.
WASM-библиотека может также экспортировать пользовательские шаги
(RFC 009): когда её манифест info() перечисляет
steps: [{name, description}], каждый становится доступен в use: как
lib/<alias>/<step> — см.
Библиотеки. Блок
with: шага интерполируется заранее, результат компонента — стандартная
форма действия (success / output / logs / metrics / http_sample), а строка
libraries: сводки прогона сообщает step_calls / step_errors /
step_p95_ms рядом со счётчиками токен-вызовов.
Настройки запуска (CallCtx.settings_json)
Каждый вызов библиотеки получает настройки запуска как JSON-объект, замороженный один раз при старте запуска, — строка идентична для каждого VU, итерации и вызова в запуске:
{
"vus": 10,
"duration_ms": 300000,
"seed": 42,
"stages": null,
"arrival": null,
"variables": {},
"extensions": {
"configlib": {
"endpoint": "https://api.test",
"pool_size": 16
}
}
}
vus/duration_ms задаются только для фиксированного профиля нагрузки;
запуски со стадиями и arrival-rate оставляют их null и несут профиль в
stages/arrival (длительности нормализованы до миллисекунд). seed — это
seed из конфига или null. Элементы stages содержат duration_ms и
target; arrival содержит max_vus, pre_allocated_vus и собственные
элементы с duration_ms и rate. variables — это блок variables: из
конфига, раскрытый через ту же интерполяцию, что используют шаги, — поэтому
значение из env, которое теперь получает библиотека, также записывается в
реестр секретов запуска и остаётся замаскированным в логе. extensions
содержит блоки конфига, принадлежащие библиотекам, после проверки по их JSON
Schema; пример соответствует расширению configlib, объявленному ниже.
Компоненты, собранные под ABI WIT 0.1, получают "{}" (в контексте 0.1 нет
поля настроек).
Правила политики: secret / allow / deny / log
Запись библиотеки может ограничивать, какие функции можно вызывать, и принудительно маскировать результаты (всё опционально):
libraries:
- use: '@std/random@v1'
capabilities: [] # обязательно в каждой записи; [] = без разрешений
secret: true # маскировать каждый результат этой библиотеки в логе запуска
allow: [uuid4, ulid] # белый список: остальные вызовы проваливают шаг
deny: [email] # чёрный список: побеждает allow
log: [uuid4] # всегда маскировать результаты этих функций
-
deny:побеждаетallow:, если имя встречается в обоих; заблокированный вызов проваливает шаг с сообщением, называющим правило, аperfscale lintпомечает его в токенах${alias.fn(...)}. -
Маскирование аддитивно: результат маскируется, если функция объявляет
secret: trueв своих метаданных, запись задаётsecret: trueили функция находится в спискеlog:записи. Способа снять маску намеренно нет. Замаскированные значения записываются в реестр секретов запуска и отображаются как***в логе запуска; полезная нагрузка по сети сохраняет реальное значение. -
Каждое имя в
allow:/deny:/log:должно быть функцией, которую библиотека действительно экспортирует, — опечатка — жёсткая ошибка валидации (иначе она молча никогда бы не сработала). -
Локальные пути (
use: ./libs/fixer-ids.wasm) загружают библиотеки WASM-компонентов (WASI Preview 2; актуальный WIT —perfscale:library@0.3.0, при этом компоненты@0.1.x/@0.2.xпродолжают загружаться, но компоненты 0.1 не видят настройки запуска, а расширения конфига требуют 0.3; см. RFC 005). Пути разрешаются относительно директории объявляющего файла, как путиimport:. Бинарник perfscale CLI поставляется с поддержкой WASM; встраивающимperfscale-coreнужна cargo-фичаwasm-libs.
Удалённые источники (https://… и git+…) — это ссылки на дистрибуцию —
они никогда не скачиваются во время запуска. perfscale install <files>
скачивает каждую удалённую библиотеку один раз, проверяет её дайджест,
складывает в контентно-адресуемый кэш (<cache>/libraries/<sha256>.wasm) и
пишет perfscale.lock рядом с объявляющим файлом. perfscale run и
perfscale lint затем разрешают каждую удалённую ссылку через lock + кэш,
полностью офлайн: отсутствующий lock, отсутствующая запись или
отсутствующий артефакт в кэше — жёсткая ошибка с указанием запустить
perfscale install.
# HTTPS: sha256 закрепляет артефакт — переопубликованный артефакт с другим
# дайджестом — жёсткая ошибка, никогда не молчаливая подмена. Закрепление
# опционально: без него `perfscale install` закрепляет вычисленный дайджест
# в lock-файле и печатает предупреждение с советом добавить его сюда.
- use: 'https://vendor.example.com/fixer-ids.wasm'
sha256: '9f2c…(64 hex)…'
# git: URL репозитория + ref (тег/ветка/коммит) + путь к артефакту внутри репозитория.
# Ref разрешается в коммит во время установки и закрепляется вместе с
# дайджестом артефакта; `perfscale install --refresh` разрешает его заново.
- use: 'git+https://github.com/org/repo.git@v1.2.3#libs/fixer-ids.wasm'
perfscale.lock — это TOML, ключованный точной строкой use:, и он
предназначен для коммита:
version = 1
[[libraries]]
use = "https://vendor.example.com/fixer-ids.wasm"
sha256 = "…"
[[libraries]]
use = "git+https://github.com/org/repo.git@v1.2.3#libs/fixer-ids.wasm"
commit = "…resolved sha…"
sha256 = "…artifact digest…"
Замечания:
- Кэш учитывает
PERFSCALE_CACHE_DIR(затемXDG_CACHE_HOME/perfscale, по умолчанию~/.cache/perfscale) — тот же корень, что и кэш git-клонов дляimport:. - Библиотеки, объявленные в документе, импортированном из git
(
import: { git: … }), закрепляются вperfscale.lockвнутри того репозитория (в его корне); запуститеperfscale installдля импортирующего файла, и он запишется туда. Install следует по цепочкамimport:(включая удалённые) именно по этой причине. - Для ссылок
git+YAML-ключsha256:опционален: закреплённый коммит — якорь целостности. Если он задан, install проверяет его, и несовпадение — жёсткая ошибка.
AOT-компиляция (burn-кэш) и perfscale burn
Компиляция WASM-компонента (Cranelift) стоит реального времени при каждом
run/lint — секунды для больших компонентов. Два механизма убирают её:
- Burn-кэш.
perfscale installпредкомпилирует каждую объявленную WASM-библиотеку — удалённую и локальную./path.wasm(локальные библиотеки не получают записи вperfscale.lock; сам файл уже локален) — в<cache>/libraries/<sha256>.cwasm. Загрузчик сначала проверяет этот кэш и десериализует артефакт (нативный код, быстрый mmap); при любом промахе или несовпадении он молча откатывается на полную компиляцию, так что кэш чисто опционален. Артефакты несут заголовок, закрепляющий версию wasmtime, целевой триплет, конфигурацию движка и дайджест исходника: обновление perfscale инвалидирует их — перезапуститеperfscale install, чтобы перепрожечь. WASM-репортеры (use: wasm:./reporter.wasm) используют тот же формат артефактов в<cache>/reporters/<sha256>.cwasm, но со сквозной записью: первая компиляция репортера сама кладёт артефакт в кэш, а внутри одного запуска валидация и доставка делят одну компиляцию. Валидация (lint / перед запуском) пишет артефакт в фоне, поэтому для гарантированно прогретого кэша используйтеperfscale install— он тоже предкомпилирует репортеры. - Встраивание в бинарник.
perfscale burn -f test.yaml [-c config.yaml] -o ./perfscale+libsпишет копию запущенного бинарника с дописанными.cwasm-артефактами библиотек документов (трейлерPFSEMBED— без патчинга исполняемых секций, работает для ELF/Mach-O/PE). Производный бинарник разрешает эти библиотеки из самого себя по точной строкеuse:, поэтому работает без.wasm-файлов, кэша иperfscale.lockна диске — один файл для доставки на генераторы нагрузки. Удалённые ссылки должны быть сначала установлены. Поскольку встраивание запекает артефакты, прожжённый бинарник, обнаруживший несовпадение заголовка, падает с ошибкой «перепрожгите», а не откатывается; перезапуститеperfscale burnс текущим perfscale.
Что burn не меняет: накладные расходы на вызов (JSON-маршалинг аргументов, store/память на экземпляр на VU) неизменны — burn убирает компиляцию на запуск, а не стоимость вызова.
Правила WASM-библиотек:
- Песочница fail-closed: WIT-импорты компонента инспектируются при загрузке,
и всё, что выходит за выдачу
capabilities:в YAML, — жёсткая ошибка загрузки с указанием нужных и выданных capability — если толькоinfo()компонента не объявляет"pure": true; в этом случае импортыwasi:filesystem/wasi:clocks/wall-clockпрощаются как шум тулчейна (гость всё равно не получает preopen'ов, поэтому реальный доступ к файлам падает в рантайме).fs— это read-only preopen, ограниченныйfs_root(по умолчанию: директория, содержащая.wasm);clock— этоwasi:clocks;net: [hosts]пока не поддерживается (выдачаnetпроваливает загрузку с понятной ошибкой);wasi:randomи сырые сокеты не предоставляются никогда — библиотеки берут случайность из PRNG с seed (ctx.seed) в своём SDK. with:передаётся вinit()компонента как JSON; сбой init фатален для запуска.- Трап или бесконечный вызов проваливает шаг: каждый вызов выполняется под бюджетом fuel (~50M), а каждый экземпляр ограничен 64 МиБ памяти.
- Файл компонента может быть не больше 64 МиБ при загрузке (тот же лимит, что
perfscale installприменяет к удалённым скачиваниям) — память компиляции масштабируется с размером модуля, и лимит применяется раньше, чем ограничения fuel/памяти смогут. - Кэш артефактов проверяется во время запуска, а не только при установке:
закэшированный
.wasm, чьи байты больше не хешируются в закреплённый дайджест (повреждение или подделка), — жёсткая ошибка с указанием перезапуститьperfscale install. Артефакты burn (.cwasm, встроенные полезные нагрузки) несут заголовок, закрепляющий дайджест исходника, версию wasmtime, целевой триплет и конфигурацию движка, и десериализуются только после полного совпадения заголовка. - Пишите библиотеки на Rust с
perfscale-library-sdk.
Функции @std/random@v1:
| Функция | Возвращает |
|---|---|
uuid4([key]) | Случайный UUID RFC 4122 v4 |
uuid7([key]) | Временно́ упорядоченный UUID RFC 9562 v7 (48-битный префикс unix-мс) |
ulid([key]) | 26-символьный ULID Crockford-base32 (префикс unix-мс) |
nanoid([len], [key]) | URL-безопасный id из A-Za-z0-9_- (длина по умолчанию 21) |
int(a, b, [key]) | Случайное целое в [a, b] включительно |
float(a, b, [dp], [key]) | Случайное число в [a, b], dp знаков после запятой (по умолчанию 2) |
pick(a|b|c, [key]) | Случайный выбор среди вариантов, разделённых | |
weighted(a:10|b:90, [key]) | Взвешенный случайный выбор среди пар значение:вес |
seq(name) | Именованный монотонный счётчик на экземпляр генератора, начиная с 1 |
pattern("ORD-####-????") | Заполнение шаблона: #→цифра, ?→a-z, ^→A-Z, *→буква или цифра |
first_name() / last_name() / name() | Случайные имена (небольшие встроенные корпуса) |
username() / email() / company() | Случайный username / email / компания |
lorem([words]) | Слова lorem ipsum (по умолчанию 5) |
phone() | Случайный номер телефона (+1-NNN-NNN-NNNN) |
date(a, b) | Случайная дата между YYYY-MM-DD a и b включительно, на выходе YYYY-MM-DD |
timestamp(a, b) | Случайная метка времени unix-мс в [a, b] |
datetime(a, b) | Случайный datetime RFC 3339 между датами a и b (весь конечный день включён) |
Функции @std/json@v1 (fail-closed, как файловые шаги: прогон должен
включить allow_file_actions: true; пути подчиняются fs_root):
| Функция | Возвращает |
|---|---|
read(file) | Весь распарсенный JSON-файл компактной JSON-строкой (общий на процесс кэш, разделённый с std/read-json@v1, перевалидация по (mtime, size)) |
read(file, query) | Подзначение по jq-подобному пути (.posts[0].id ≡ posts.0.id); строковый лист — как есть, остальные значения — компактным JSON. Обязательный промах → ошибка раскрытия с именем сегмента; суффикс ? разрешает промах в пустую строку |
pick(file) / pick(file, index) | Элемент JSON-массива из file — случайный на сообщение (детерминирован при seed: прогона) или index по модулю длины массива. Не-массив, пустой массив или нецелый index → ошибка раскрытия |
Расширения конфига (топ-левел ключи библиотек, issue #10)
WASM-библиотека, собранная против perfscale:library@0.3.0, может
экспортировать config-schema, объявляя один топ-левел ключ конфига и
JSON Schema для его значения. Экспорт возвращает JSON-строку, например:
{
"key": "configlib",
"schema": {
"type": "object",
"properties": {
"endpoint": { "type": "string" },
"pool_size": { "type": "integer", "minimum": 1 }
},
"required": ["endpoint"],
"additionalProperties": false
}
}
Эта декларация заявляет ключ configlib. Если необязательный блок задан, он
должен содержать endpoint, может содержать pool_size не меньше 1 и не
может содержать другие поля. Если блок отсутствует, в extensions нет записи
configlib. Соответствующий конфиг:
libraries:
- use: ./configlib.wasm
capabilities: []
configlib: # ключ заявлен config-schema библиотеки
endpoint: https://api.test # обязательная строка
pool_size: 16 # необязательное целое, минимум 1
На lint и на старте движок загружает объявленные библиотеки, регистрирует
заявленные ключи и валидирует каждый блок по схеме библиотеки — fail-fast.
Проверенный блок попадает гостю в run settings (extensions; в TS SDK —
ctx.settings.extensions["configlib"]). Ключ должен соответствовать
[a-z][a-z0-9_]* и не должен пересекаться со встроенным ключом конфига или
ключом другой загруженной библиотеки. Любой топ-левел ключ, который не заявлен
ни одной загруженной библиотекой, остаётся жёсткой ошибкой (защита от
опечаток живёт тут). В TS SDK блок объявляется через
defineLibrary({ configSchema: { key, schema }, ... });
perfscale-library-build сам выбирает world perfscale-library-config,
когда configSchema присутствует. Полный пример создания библиотеки есть в
руководстве по SDK.
Отложено (не в v1): интерполяция внутри extension-блоков, эволюция схем
между пинами perfscale.lock.
Композиция документов: import
И определения тестов, и конфиги принимают верхнеуровневый import: с именем
базового документа. База загружается первой (она может импортировать свою
базу, рекурсивно), затем текущий документ глубоко сливается поверх:
объекты сливаются поключно, скаляры и массивы (включая steps:)
заменяются импортирующей стороной. Единственное исключение — libraries:,
который конкатенируется по цепочке, — дублирующийся алиас — ошибка
валидации.
# конфиг команды — наследует общий базовый документ организации, переопределяет одну переменную
import: ../shared/_base.yaml
variables:
region: us
Три формы источника:
# относительный путь в файловой системе (разрешается относительно директории импортирующего файла)
import: ../shared/_base.yaml
# сырой HTTP(S) URL — закрепите ref в пути
import: "https://raw.githubusercontent.com/org/repo/v1.2.0/perf/config/_base.yaml"
# любой git-хост (SSH или HTTPS remote, включая self-hosted)
import:
git: git@gitlab.example.com:group/repo.git
ref: v1.2.0 # тег, ветка или SHA коммита
file: perf/config/_base.yaml
Удалённые импорты (URL и git) fail-closed: они выполняются только когда
вызывающая сторона передаёт --allow-remote-import. Разрешение принадлежит
вызывающей стороне, потому что разрешение импортов происходит до проверок
allow_file_actions / allow_process_actions — иначе удалённая база могла
бы выдать себе эти права и подтянуть шаг std/child_process@v1. Документ
никогда не может сам разрешить себе сеть.
Источники остаются ограниченными: документ, полученный по URL, разрешает
относительные импорты относительно своего собственного URL; документ из
git-репозитория может импортировать только файлы внутри того же клона
(выходы через ../ отклоняются). Удалённый документ никогда не может
прочитать вашу локальную файловую систему. Циклы импортов падают с выводом
цепочки.
Git-импорты клонируются с --depth 1 через ваш системный git (SSH-ключи и
credential helpers применяются) и кэшируются в
~/.cache/perfscale/imports/. Теги и SHA коммитов неизменны — кэшируются
навсегда. Ветки ревалидируются по удалённому репозиторию после короткого
TTL, так что ref: main следует за веткой; --refresh-imports принудительно
перескачивает. Полное руководство — в
docs/core/imports.md.
Инициализация и переменные
Шаги before: выполняются один раз, по порядку, до старта любого VU —
для одноразовой инициализации вроде получения токена или построения профиля
соединения. Каждый шаг before — обычный шаг (use/with/outputs); его
имя в outputs доступно каждому тестовому шагу в пространстве имён
config. variables: хранит статические значения, доступные в vars.
vus: 50
variables:
region: eu-west
before:
- uses: std/http@v1
with:
method: POST
url: "https://api.example.com/token"
body: { user: demo, region: "${{ vars.region }}" }
outputs: auth # ← доступен тестовым шагам как config.auth
# test.yaml
steps:
- uses: std/http@v1
with:
url: "https://api.example.com/me"
headers:
authorization: "Bearer ${{ config.auth.body }}" # из шага before
x-region: "${{ vars.region }}" # из variables
beforeвыполняется независимо от--quiet(его сбои всегда печатаются). Если любой шаг инициализации падает, запуск прерывается до запуска VU — сломанная инициализация привела бы к одинаковому провалу каждой итерации.- Шаги
beforeвидят${{ vars.* }}и выводы предыдущих шагов инициализации (под их собственными именамиoutputs). Тестовые шаги видятconfig.*иvars.*, но не выводы друг друга. - Интерполяция всегда даёт строку, поэтому числовое значение конфига
вроде
${{ config.fix_config.port }}доходит до действия как"1111". Действия, принимающие числа, принимают и строковую форму.
Полный жизненный цикл (включая фоновые процессы, запущенные в before:) —
см. Инициализация и завершение.
Завершение (after:)
Шаги after: выполняются один раз после остановки нагрузки — при
нормальном завершении, при провалившемся запуске, при провалившемся before:
и при Ctrl-C/SIGTERM одинаково. Они видят те же ${{ config.* }} и
${{ vars.* }}, что и тестовые шаги. В отличие от before:, провалившийся
шаг завершения логируется, но не прерывает оставшиеся (очистка по принципу
best-effort). Типичный шаг after: — std/kill_process@v1 для сервера,
запущенного в before::
allow_process_actions: true
before:
- name: web
uses: std/child_process@v1
with:
command: python3
args: ["-m", "http.server", "8080"]
port: 8080
waitUntil: { port_open: 8080, timeout: 10s }
outputs: web
after:
- name: stop web
uses: std/kill_process@v1
with: { name: web, signal: TERM }
Процессы, ещё живые после шагов after:, останавливаются автоматически, так
что явный kill — про чистую и своевременную остановку, а не про
предотвращение утечек. См. std/child_process@v1
для полного списка параметров (политика рестарта, захват вывода, waitUntil),
examples/with-processes.config.yaml
для готового примера и Инициализация и завершение
для всего жизненного цикла (прерывания, авто-kill, поток данных).
after: — также дом для SLO-порогов уровня запуска: std/thresholds@v1
вычисляет выражения в стиле k6 (p95<500, rate<0.05, count==0) по
метрикам, собранным за весь запуск, один раз и проваливает запуск (ненулевой
код выхода), когда нарушен порог с severity: fail:
after:
- name: slo gate
use: std/thresholds@v1
with:
db_query_duration: ["p95<500", "max<2000"]
db_query_failed: ["rate<0.05"]
db_errors: ["count==0"]
severity: fail # fail (по умолчанию) | warn | info
message: "checkout SLO" # необязательно, интерполируется
См. std/thresholds@v1 для грамматики
выражений, видов метрик и поля thresholds в JSON сводки запуска.
Останов по порогу и диагностика ошибок
С abort_on_fail: true порог с severity: fail становится монитором в стиле
k6 abortOnFail: пока работают VU, движок переоценивает порог по уже
собранным метрикам каждые 250 мс, и первое нарушение останавливает все
расписания нагрузки (между шагами, как Ctrl-C — текущий шаг завершается),
пишет одну строку
abort_on_fail: threshold gate 'slo gate' violated — stopping load early (after: teardown and the summary still run): …
и проходит обычный путь after:-завершения, репортеров и сводки — оценка
самого after:-шага остаётся единственным записанным результатом порога, так
что fail-порог по-прежнему даёт ненулевой код выхода. Запуск, завершившийся
по расписанию (или по Ctrl-C) без нарушений, не печатает строку останова.
Метрики порога, по которым ещё нет сэмплов, пропускают тик, а не останавливают
запуск; монитор стартует только после успешного before: и останавливается
перед after:-завершением. Оценки монитора чистые — промежуточные результаты
никуда не записываются.
Добавьте http_debug: error (пока единственный режим; валиден только вместе
с abort_on_fail: true и только на after:-шагах std/thresholds@v1 — всё
остальное — ошибка конфигурации на lint/запуске), чтобы получать диагностику
ошибок в стиле k6
#6618 прямо во время запуска:
каждое упавшее действие и каждый упавший check: пишут одну маскированную
строку http_debug: {…} с полями vu, iteration, step, action,
интерполированными параметрами request (включая тело запроса), выводом
response (включая тело ответа) и — для проверок — деталями упавших
failed_checks. Успешные запросы и проверки ничего не выводят, и ничего не
буферизуется на потом: вывод порождают только ошибки.
after:
- name: slo gate
use: std/thresholds@v1
with:
http_req_failed: ["rate<0.05"]
http_req_duration: ["p95<500"]
severity: fail
abort_on_fail: true # остановить нагрузку досрочно при нарушении порога
http_debug: error # логировать упавшие запросы/проверки (с маскированием)
С --locust тот же конфиг отображается в --users/--spawn-rate/--run-time
locust. С --k6 конфигурация нагрузки живёт в собственном блоке options
скрипта, а конфиг-файл игнорируется.
Валидация без запуска: perfscale lint
Проверяйте файлы заранее — в CI, pre-commit-хуках или прямо во время написания:
perfscale lint test.yaml config.yaml
Помимо валидации по схеме, lint помечает неизвестные и опечаточные имена
полей с подсказками «возможно, вы имели в виду» (chek → check, vsu →
vus, std/htp@v1 → std/http@v1), включая параметры with: для
конкретных действий. См.
Команды CLI → lint.
Ошибки валидации
perfscale валидирует перед запуском. Разбор строгий: неизвестные ключи
отклоняются, а не игнорируются — опечатка вроде iterations: 1 (или chek:
вместо check:) завершает валидацию ошибкой с именем ключа, а не молча
меняет профиль нагрузки. Это действует на верхнем уровне, для фиксированных
полей каждого шага и для блоков report:, gpu:, stages:/arrival: и
libraries:. Примеры того, что вы увидите:
error: schema validation failed:
/steps/0 — every step must name an action: `use: std/http@v1` (or the `uses:` alias)
error: invalid YAML: found unexpected end of stream
Перегенерируйте схемы после изменения Rust-типов:
cargo run -p perfscale-core --example gen_schema
(Тест CI shipped_schemas_match_generated_ones падает, если schema/
устарел.)