Ядро
Setup и teardown
Нативный запуск — это не только цикл VU. Конфигурационный файл может обрамлять
нагрузку одноразовыми шагами: before: подготавливает окружение (получить
токен, запустить локальный сервер), after: очищает его — на любом пути
завершения.
Жизненный цикл запуска
before steps (once, in order, fail-fast)
│
▼
VU loop (steps × iterations until duration expires)
│
▼
after steps (once, best-effort) ← runs on EVERY exit path:
│ normal finish, failed run,
▼ failed before, Ctrl-C/SIGTERM
auto-kill of remaining managed processes
│
▼
summary
- Шаги
before:выполняются один раз, по порядку, до создания любого VU. Если какой-либо из них завершается с ошибкой, запуск прерывается до создания VU (Setup failed, aborting run: …) — сломанная подготовка привела бы к одинаковому провалу каждой итерации. - Шаги
after:выполняются всегда: после нормального завершения, после неудачного запуска, после неудачногоbefore:(шаг подготовки мог уже запустить что-то, что teardown как раз и существует, чтобы убрать) и после Ctrl-C/SIGTERM. В отличие отbefore:, неудачный шаг очистки логируется (teardown step '<name>' failed (continuing)), а оставшиеся шаги всё равно выполняются — частичная очистка лучше, чем никакой. - После teardown каждый управляемый процесс, всё ещё живой в реестре запуска,
останавливается автоматически (SIGTERM с эскалацией до SIGKILL после
периода ожидания, вся группа процессов). Забытый
kill_processникогда не приводит к утечке сервера. - Помимо очистки, в
after:живут SLO-гейты уровня запуска: шагstd/thresholds@v1видит всё, что собрал запуск, и определяет по этому код завершения (см. actions.md). - Сводка метрик печатается последней.
Поток данных: vars.* и config.*
Шаги питаются из двух блоков конфигурации:
| Блок | Доступен как | Виден |
|---|---|---|
variables: | ${{ vars.<key> }} | шагам before, тестовым шагам, шагам after |
outputs шага before: | ${{ config.<name>.<field> }} | тестовым шагам и шагам after |
variables:
region: eu-west
before:
- uses: std/http@v1
with:
url: "https://api.example.com/token?region=${{ vars.region }}"
outputs: auth # → ${{ config.auth.status }} etc. later on
- Шаг
beforeвидит${{ vars.* }}и выходные данные предыдущих шагов подготовки (каждый под своим именемoutputs). - Тестовые шаги видят
config.*иvars.*— но никогда не видят выходные данные друг друга между VU (контексты VU независимы). - Шаги
afterвидят те жеconfig.*иvars.*, что и тестовые шаги, поэтому шаг очистки может ссылаться ровно на то, на что ссылался тест. - Интерполяция всегда возвращает строку: числовое выходное значение вроде
${{ config.keeper.port }}доходит до action как"8080". Actions, принимающие числа, принимают и строковую форму.
Что не работает в before:
Живые соединения (std/ws-connect@v1, std/grpc-connect@v1 и семейство
stream) привязаны к контексту, который их создал. Контекст подготовки
исчезает до старта первого VU, поэтому соединение, открытое в before:, уже
мёртво к моменту, когда тестовый шаг запросит его id — вместо этого открывайте
соединения на каждой итерации VU (они освобождаются в конце итерации).
Процессы — исключение: std/child_process@v1 регистрирует свой дочерний
процесс в реестре с областью видимости запуска, и именно поэтому сервер,
запущенный в before:, остаётся живым и доступным для остановки на протяжении
всего запуска.
Фоновые процессы от начала до конца
Каноническая пара setup/teardown: запустить локальный сервис в before:,
обращаться к нему из теста, остановить в after:. Здесь — мок «position
keeper» для сценария CFD-трейдинга:
# config.yaml
vus: 20
duration: 5m
allow_process_actions: true # fail-closed gate, default false
before:
- name: position-keeper
uses: std/child_process@v1
with:
command: python3
args: ["-m", "http.server", "8080"]
port: 8080 # echoed to outputs; port: 0 auto-assigns
waitUntil: # block the step until the server is ready
port_open: 8080
timeout: 15s
restart: on-failure # supervisor restarts crashes (max 3, 1s apart)
outputs: keeper # → ${{ config.keeper.port }} etc.
after:
- name: stop-keeper
uses: std/kill_process@v1
with:
name: keeper # registry lookup — always the current pid
signal: TERM
# test.yaml
steps:
- uses: std/http@v1
with:
url: "http://127.0.0.1:${{ config.keeper.port }}/positions"
check:
status: 200
Что происходит под капотом:
- Стриминг: stdout/stderr дочернего процесса стримятся в лог запуска с
префиксом
position-keeper:(тот же формат, что и у раннера k6) и накапливаются в ограниченных tail-буферах (по умолчанию 64 КиБ на поток, настраивается параметромbuffer_kb). - Супервизия: с
restart: on-failure(илиalways) задача-супервизор следит за дочерним процессом и перезапускает его послеbackoff_ms(по умолчанию 1000), не болееmax_restarts(по умолчанию 3) раз, логируя каждый перезапуск. - Выходные данные — это снимок:
{ pid, ppid, pgid, port, stdout, stderr, restart_count }.ppid— это сам perfscale;pgid— группа процессов дочернего процесса. После перезапуска сохранённыйpidустаревает — именно поэтомуstd/kill_process@v1следует адресовать процесс поname(имя шага или имяoutputs): поиск в реестре всегда разрешает текущий pid.pid:(сырой pid ОС) существует как best-effort лазейка для процессов вне реестра. - Автоостановка: даже без шага
after:keeper был бы остановлен по завершении запуска. Явныйkill_processнужен для чистой и своевременной остановки — и для возможности проверить её утверждением.
Полные справочники параметров см. в
std/child_process@v1 и
std/kill_process@v1, а запускаемый вариант —
в
examples/with-processes.config.yaml.
waitUntil — гейты готовности
Шаг child_process блокируется до готовности процесса (или до истечения
таймаута гейта). Две формы:
# Object form — all listed matchers must hold:
waitUntil:
stdout_contains: "Serving HTTP" # substring in captured stdout
stderr_contains: "listening" # substring in captured stderr
stdout_matches: "on port \\d+" # regex against captured stdout
stderr_matches: "err\\d+" # regex against captured stderr
port_open: 8080 # TCP connect to 127.0.0.1:<port> works;
# 0 = the step's own `port`
timeout: 15s # duration string, default 30s
on_timeout: fail # fail (default) | continue
# String form — one matcher, defaults for the rest:
waitUntil: 'contains(stdout, "Serving HTTP")'
waitUntil: 'matches(stderr, "err\\d+")'
waitUntil: 'port_open(8080)'
- С
on_timeout: failневыполненный гейт проваливает шаг (а значит, и запуск, если он используется вbefore:);continueлогирует промах и продолжает выполнение. - Процесс, который завершился до наступления готовности, немедленно проваливает шаг со своим кодом завершения и хвостом stderr — без ожидания таймаута.
Прерывания (SIGINT / SIGTERM)
Двухступенчатая семантика:
- Первый SIGINT (Ctrl-C) или SIGTERM: цикл VU останавливается между шагами,
и запуск проходит через обычный teardown — шаги
after:, автоостановка процессов, сводка. Об этом сообщает строка в логе:Interrupt received — stopping load, running teardown (interrupt again to force-quit). - Второй сигнал: немедленный
exit(130)— сам teardown мог зависнуть, и оператор явно хочет выйти. Всё, что к этому моменту ещё не остановлено, может утечь, поэтому лучше дождаться первой стадии.
Безопасность и переносимость
- Fail-closed гейт: оба process actions завершаются с ошибкой
process actions disabled (allow_process_actions is false), если конфиг явно не включил их черезallow_process_actions: true— список шагов из недоверенного источника не может порождать OS-процессы или посылать им сигналы. Тот же паттерн, что иallow_file_actionsдля файловых actions. - Группы процессов (unix): каждый дочерний процесс возглавляет свою группу
(
pgid == pid), поэтому остановка сtree: trueпосылает сигнал точно в группу дочернего процесса — она никогда не может задеть perfscale или его родителя. - Не-unix: ни POSIX-сигналов, ни групп процессов нет.
std/kill_process@v1поnameзавершает только непосредственный дочерний процесс (treeне действует);pid:не поддерживается. Всё остальное — захват вывода, супервизия перезапусков,waitUntil, автоостановка — работает так же.