Ядро
Метрики
Нативный движок записывает каждый шаг, похожий на запрос, в HDR-гистограмму
фиксированного размера и сворачивает пользовательские счётчики/гистограммы из
выводов действий в те же агрегаты запуска. Два выхода: машиночитаемая строка
[stats] каждые 5 секунд во время запуска и k6-совместимая сводка в конце.
Метрики запросов (http_req_*)
Действия, выполняющие измеряемый по времени запрос, возвращают HttpSample
(duration_ms, status, failed). Длительности записываются в
микросекундах — вызовы через loopback короче миллисекунды остаются различимыми.
| Действие | Что попадает в http_req_duration | Что считается ошибкой |
|---|---|---|
std/http@v1 | Каждый запрос | Статус ≥ 400, транспортная ошибка, таймаут (логируется как → TIMEOUT after …ms; status указывается как 0) |
std/tcp@v1 | Connect + обмен send/read | Ошибка соединения, таймаут, несовпадение с expect |
std/udp@v1 | Отправка (+ опциональное ожидание ответа) | Таймаут, несовпадение с expect |
std/ws@v1 | Вся one-shot-сессия целиком (один сэмпл) | Ошибка рукопожатия/транспорта, правило until_*, не выполненное вовремя |
std/ws-connect@v1 | Только рукопожатие | Ошибка рукопожатия (connected: false) |
Намеренно не попадают в http_req_duration — шаг, чья длительность
ничего не говорит о задержке цели, отравил бы общие процентили:
| Действие | Почему |
|---|---|
std/ws-recv@v1 | То, сколько сервер ждёт перед отправкой, — не задержка цели |
std/ws-ping@v1 | Транспортный RTT; вместо этого ограничивайте его через check: { duration_ms_lt: … } |
std/ws-send@v1, std/ws-close@v1 | Нет осмысленной задержки запроса |
std/grpc*@v1 (всё семейство) | У gRPC свои гистограммы (grpc_req_duration, grpc_msg_rtt); время жизни стримов выходит за пределы шагов пользователя, поэтому стримы не попадают даже в них |
std/db-*@v1 (всё семейство) | У DB-шагов свои гистограммы (db_connect_duration, db_query_duration) и счётчики (db_rows, db_errors) |
std/child_process@v1, std/kill_process@v1 | Жизненный цикл процесса, а не запросы |
std/check@v1, std/sleep@v1, std/log@v1, std/file-*@v1 | Нет сетевого ввода-вывода |
Обратите внимание на асимметрию: ошибки gRPC-утверждений попадают в
grpc_req_failed (пользовательский счётчик, управляемый expect_status),
а не в http_req_failed.
Итоговая сводка
Выводится в конце каждого запуска, k6-совместимая, чтобы нижестоящие парсеры
(дашборды, perfscale serve) обрабатывали все движки единообразно:
vus....................: 10 min=1 max=10
iterations..............: 4521 150.23/s
http_req_duration......: avg=0.42ms p(50)=0.31ms p(90)=0.88ms p(95)=1.02ms p(99)=1.90ms min=0.09ms max=3.10ms
http_req_failed........: 0.00%
http_reqs..............: 4521 150.23/s
vus/iterations(+ скорость в секунду) выводятся всегда, даже для запусков, состоящих только из sleep.dropped_iterationsвсегда выводится для запусков с arrival rate — 0, если пул справлялся, — чтобы пороговые проверки (dropped_iterations: ["count==0"]) разрешались, а не падали с ошибкой на неизвестной метрике.- Блок
http_req_*появляется только когда записан хотя бы один сэмпл. - Процентили берутся из HDR-гистограммы фиксированного размера (диапазон 1 мкс – 1 ч, два значащих разряда → ошибка квантиля ≤1%) — потребление памяти не растёт независимо от длительности soak-теста ценой погрешности, невидимой на выводимой точности.
Живые строки [stats]
Каждые 5 секунд, пока работают VU, выводится одна машиночитаемая строка:
[stats] ts=1720000000000 rps=246.80 err_pct=0.00 p50=1.20 p90=3.40 p95=4.10 p99=8.20 reqs=1234 iters=456
ts— миллисекунды unix-эпохи;rps— запросы за только что завершившееся 5-секундное окно;err_pct— кумулятивный процент ошибок;p50…p99— кумулятивные процентили в мс (гистограмма никогда не сбрасывается, поэтому они сходятся, а не дрожат);reqs— суммарное число запросов;iters— суммарное число итераций.- Пока запросов не было, процентили опускаются:
[stats] ts=… rps=0.00 reqs=0 iters=3. - Эти строки существуют для потоковых потребителей (controlplane разбирает их
из потока логов); о поведении консоли см.
--quiet.
Пользовательские метрики (value.metrics)
Любое действие может прикрепить зарезервированный объект metrics к выводу
своего шага; раннер сворачивает его в агрегаты запуска:
- Число становится счётчиком, суммируется по всем VU и итерациям,
выводится как
<name>: <total> <rate>/s. - Массив чисел становится сэмплами HDR-гистограммы (миллисекунды),
выводится как
<name>: avg=…ms p(50)=… p(90)=… p(95)=… p(99)=… min=… max=… count=N.
Встроенные эмиттеры, по метрикам (подробности — на странице каждого семейства):
WebSocket — websocket.md:
ws_msgs_sent— счётчик; отправленные WS-сообщения. Эмитируетсяstd/ws@v1(итог за всю сессию) иstd/ws-send@v1(за шаг), при успехе и при ошибке.ws_msgs_received— счётчик; прочитанные WS-сообщения. Эмитируетсяstd/ws@v1иstd/ws-recv@v1.ws_msg_rtt— HDR-гистограмма (мс); отправка → первый ответ, подходящий подuntil_*-правило (RTT на уровне приложения). Один сэмпл на каждый совпавший ответ; эмитируется только когда правило сработало после отправки на том же соединении — чистое ожидание push не записывает ничего (намеренно, см. websocket.md).
Pub/Sub — pubsub.md:
pubsub_msgs_published— счётчик; сообщения, принятые транспортом. Эмитируется каждым завершённым обменомstd/pubsub@v1(также 0 для шагов только с подпиской).pubsub_msgs_received— счётчик; сообщения, учтённые вsubscribe.count. Только когда у шага есть блокsubscribe.pubsub_e2e_ms— HDR-гистограмма (мс); начало фазы публикации → сообщение обработано, один сэмпл на каждое совпавшее сообщение. Только сsubscribeи только когда совпало хотя бы одно сообщение (ожидание подписки, не совпавшее ни с чем, сэмплов не эмитирует). Для шагов только с подпиской это время ожидания.
Общие переменные:
shared_variable_wait_ms— HDR-гистограмма (мс); время блокировки вwait_forдо выполнения условия, один сэмпл на каждое ожидающее чтение. Эмитируетсяstd/get_shared_variable@v1только сwait_for.
LLM — llm.md:
llm_ttft_ms— HDR-гистограмма (мс); начало запроса → первый чанк контента (время до первого токена). Только потоковые запросы.llm_tokens_per_sec— HDR-гистограмма; токены завершения / время генерации (после первого токена при стриминге). Только когда сервер сообщает число токенов завершения.llm_prompt_tokens/llm_completion_tokens— счётчики по даннымusageсервера; отсутствуют, когда сервер не сообщает usage.llm_chunks— счётчик; полученные SSE-чанки (0 для непотоковых запросов).
gRPC — grpc.md:
grpc_req_duration— HDR-гистограмма (мс); задержка унарного вызова, один сэмпл на завершённый вызов. Толькоstd/grpc@v1иstd/grpc-call@v1— время жизни стримов выходит за пределы шагов и намеренно ничего не эмитирует; неудачное подключение не порождает метрик вообще (RPC не было).grpc_msg_rtt— HDR-гистограмма (мс); отправка → RTT подходящего ответа. При унарном вызове эмитируется только когда статус OK (тогда равенgrpc_req_duration); наstd/grpc-stream-recv@v1— только когда сработало правилоuntil_*и перед этим на том же стриме былgrpc-stream-send.grpc_msgs_sent— счётчик; отправленные сообщения, за вызовstd/grpc-call@v1(1) и за шагstd/grpc-stream-send@v1.grpc_msgs_received— счётчик; прочитанные сообщения, за вызовstd/grpc-call@v1, за шагstd/grpc-stream-recv@v1и заstd/grpc-stream-close@v1(дочитанные при закрытии).grpc_req_failed— счётчик, 0/1 на вызов; вызовы, не прошедшиеexpect_status. Эмитируетсяstd/grpc-call@v1иstd/grpc-stream-close@v1(close превращает финальный статус стрима в этот счётчик). Учтите: производная одноимённая rate-метрика затеняет этот счётчик — см. ниже.
GraphQL — graphql.md:
graphql_req_duration— HDR-гистограмма (мс); полный цикл операции, один сэмпл на каждый реально отправленный запрос (успех или ошибка).graphql_errors— счётчик; ошибки уровня GraphQL, включая ответы с частичными данными, которые проходят шаг. Эмитируется всегда (0 на чистых ответах), поэтому проверкиgraphql_errors: ["count==0"]разрешаются и на здоровых прогонах.graphql_op_<operationName>_duration— HDR-гистограмма (мс); задержка по операциям, эмитируется только для именованных операций (явныйoperationили единственная именованная операция в документе), поэтому кардинальность ограничена определением теста.
Базы данных:
db_connect_duration— HDR-гистограмма (мс); задержка соединения + настройки пула,std/db-connect@v1при успехе.db_query_duration— HDR-гистограмма (мс); задержка запроса дляstd/db-query@v1иstd/db-tx-*@v1; в режиме соединения на каждый запрос включает новый connect.db_rows— счётчик; строки, возвращённыеstd/db-query@v1, либо затронутые строки, если выражение не вернуло ни одной.db_errors— счётчик; неудачные DB-шаги, всего, эмитируется каждым шагомstd/db-*@v1. Успешные шаги эмитируютdb_errors: 0, поэтому счётчик существует (равный 0) даже на полностью здоровых прогонах — проверки видаdb_errors: ["count==0"]работают в обоих случаях.db_errors_connection/_constraint/_deadlock/_timeout/_other— счётчики; то же, разбитое по классам (SQLSTATE / errno / код результата SQLite), эмитируются при ошибке.
Нижестоящие действия используют тот же канал — например, WebRTC-плагин
эмитирует серии webrtc_* (WebRTC (pro)), а
проприетарное FIX-действие эмитирует fix_messages_sent.
Метрики доли ошибок (<family>_failed)
Зачем они нужны. Выражения rate в std/thresholds@v1 — естественная
форма SLO-гейта: «менее 1% вызовов может падать», — но одна гистограмма
длительностей на него не отвечает: в ней лежат задержки, и ничто в сэмпле
задержки не говорит, успешным ли был породивший его вызов. Поэтому помимо
полезной нагрузки metrics раннер универсально вычисляет сигнал ошибок по
вызовам.
Как они вычисляются. Для каждой гистограммной (массивной) метрики,
которую эмитирует вызов шага, раннер записывает один сэмпл 0/1 — 1, если
вызов завершился ошибкой, 0 при успехе — под именем семейства метрики:
хвост _duration/_rtt заменяется на _failed, а если такого хвоста нет,
_failed добавляется к полному имени:
| Метрика длительности | Производная метрика ошибок |
|---|---|
http_req_duration | http_req_failed (нативная для HTTP-пути — см. ниже) |
db_query_duration | db_query_failed |
db_connect_duration | db_connect_failed |
grpc_req_duration | grpc_req_failed |
graphql_req_duration | graphql_req_failed |
graphql_op_<name>_duration | graphql_op_<name>_failed (по каждой именованной операции) |
ws_msg_rtt | ws_msg_failed |
pubsub_e2e_ms | pubsub_e2e_ms_failed |
shared_variable_wait_ms | shared_variable_wait_ms_failed |
llm_ttft_ms | llm_ttft_ms_failed |
llm_tokens_per_sec | llm_tokens_per_sec_failed |
webrtc_setup_ms, webrtc_ttff_ms, webrtc_call_duration_ms, … | <тем же именем>_failed — см. WebRTC (pro) |
Они выводятся как <name>: <pct>% (в форме k6 http_req_failed). Поскольку
один сэмпл записывается на вызов (а не на сэмпл длительности — вызов,
совпавший с тремя сообщениями, всё равно записывает один сэмпл ошибки),
failed/total по ним — это в точности доля ошибок вызовов семейства шагов —
именно её оценивает std/thresholds@v1 с rate:
- use: std/thresholds@v1
with:
graphql_req_failed:
- "rate<0.01" # менее 1% неуспешных операций
pubsub_e2e_ms_failed:
- "rate<0.05" # ожидания подписки в основном успевали
Учтите: неудачный шаг, не эмитировавший сэмпл длительности (например,
db-connect, который так и не подключился, или ожидание подписки без
единого совпавшего сообщения), не записывает и сэмпл ошибки, поэтому доля
семейства покрывает только вызовы, дошедшие до измерения.
Нативные и производные. http_req_failed — нативная для HTTP-пути:
каждое действие, возвращающее HttpSample, само помечает его успех/ошибку
(статус ≥ 400, транспортная ошибка, таймаут), и никакого вычисления нет.
Семейство gRPC — асимметричный случай: std/grpc-call@v1 и
std/grpc-stream-close@v1 эмитируют счётчик grpc_req_failed (0/1 на
вызов, управляемый expect_status), а раннер дополнительно вычисляет
rate grpc_req_failed из grpc_req_duration. Когда существуют обе,
rate-метрика затеняет счётчик и в сводке, и при оценке порогов — поэтому
работает grpc_req_failed: ["rate<0.05"], а выражения с count по этому
имени счётчик не видят. У семейства WebRTC есть собственные счётчики ошибок
(webrtc_connect_errors, webrtc_call_errors_<stage>), сосуществующие с
производными rate — см. WebRTC (pro).
Пороговые проверки запуска (std/thresholds@v1)
Шаг std/thresholds@v1 (обычно в after:) оценивает выражения в стиле k6
по агрегатам запуска и выводит одну машиночитаемую строку после сводки метрик:
thresholds: {"status":"fail","message":"db_query_failed rate=1 ≥ 0.05; checkout SLO","violations":[{"metric":"db_query_failed","expr":"rate<0.05","actual":1.0}]}
Агрегаты берутся из тех же HDR-гистограмм/счётчиков, что и текстовая сводка,
поэтому числа проверок совпадают с выводом сводки. Строка собирается в
вывод perfscale run --summary-export под ключом thresholds
({status, message, violations}), а статус fail заставляет CLI завершиться
с ненулевым кодом. См. actions.md.
Метрики GPU (gpu:)
При gpu.enabled: true в конфигурации запуска
нативный движок сэмплирует каждый GPU на хосте — загрузку %, занято/всего
VRAM, температуру, потребляемую мощность — раз в interval_ms на протяжении
всей фазы VU (через nvidia-smi или эндпоинт dcgm-exporter). После сводки
метрик запуск выводит компактный блок по устройствам плюс одну машиночитаемую
строку gpu: {...} с полным временным рядом, которую --summary-export
вкладывает под ключом gpu:
gpu: 1 device, 300 samples every 1000ms (nvidia-smi)
gpu0: util avg=64.3% max=100.0% vram max=41088/81559MiB temp max=71.0C power max=512.3W
gpu: {"source":"nvidia-smi","interval_ms":1000,"devices":[{"index":0,"samples":[…],…}]}
Сбор — наилучшее усилие: отсутствие GPU / необходимых утилит логируется одним
предупреждением, и запуск продолжается без метрик GPU. Метки времени сэмплов
делят общую шкалу epoch-мс со строками [stats], поэтому нагрузка и
состояние GPU коррелируют напрямую — основной сценарий использования:
запуски std/llm@v1 против локальных серверов моделей. Полное
руководство: gpu.md.
--quiet
Два независимых слоя:
- В источнике (нативный движок): вывод об успешных итерациях — строки
запросов, маркеры sleep, прошедшие проверки — даже не форматируется и не
отправляется. Ошибки, провалившиеся проверки, строки
[stats]и итоговая сводка всегда попадают в поток. - В принтере CLI: при
--quietстроки stdout выводятся только если это строки сводки в формате k6 (vus,iterations,http_req_*); stderr и системные строки выводятся всегда. Строки пользовательских метрик и[stats]остаются в потоке для потребителей логов, но скрываются из консоли.
Пересылка сводки (report)
Укажите запуску экземпляр perfscale serve, и строки сводки будут пересланы
по завершении запуска:
# config.yaml
report:
url: http://localhost:7999
perfscale run -f test.yaml -c config.yaml --report http://localhost:7999
Флаг CLI имеет приоритет над блоком конфига. После запуска CLI отправляет POST
с собранными строками сводки (только в формате k6) как
{"lines": […]} на <url>/api/v1/metrics с таймаутом 5 с; проблемы доставки
логируются как [report] … в stderr и никогда не роняют сам запуск.
Метрики во время запуска (report.during_run)
Тот же блок report: может также стримить метрики пока запуск ещё идёт —
кумулятивные снапшоты всех семейств метрик, пакетируемые и отправляемые POST
на тот же эндпоинт <url>/api/v1/metrics:
report:
url: http://localhost:7999
during_run: true # default false — only the end-of-run summary
interval_ms: 5000 # snapshot/flush interval, min 100
batch_size: 500 # flush a batch once it holds this many samples
max_cpu_percent: 90 # CPU gate; 0 disables it
max_pending: 24 # soft warn cap — batches are never dropped mid-run
Движок эмитирует один снапшот на interval_ms (независимо от 5-секундного
репортёра [stats]), а shipper их пакетирует: батч отправляется POST, как
только достигает batch_size сэмплов, а всё открытое сбрасывается на каждом
тике интервала. Каждое тело POST — {samples, seq} (плюс taskId, если
вызывающая сторона его задаёт), где seq — монотонно возрастающий номер
батча, переиспользуемый при повторах — получатели могут дедуплицировать
повторные доставки.
Именование сэмплов следует соглашениям Prometheus для summary/counter, с конвертацией длительностей из миллисекунд движка в секунды:
| Метрика движка | Отправляемые сэмплы |
|---|---|
histogram (http_req_duration, db_query_duration, …) | <name>{quantile="0.5"/"0.9"/"0.95"/"0.99"} (секунды), <name>_count, <name>_sum (секунды) |
counter (db_rows, …) | <name>_total |
доля ошибок (http_req_failed, …) | <name>_total (вызовы), <name>_failed_total (ошибки) |
Все значения кумулятивные с начала запуска — HDR-гистограммы и счётчики
никогда не сбрасываются во время запуска. Чтобы получить показатели за окно на
стороне потребителя, вычитайте соседние снапшоты: в стиле
rate(x_count[window]) для пропускной способности, Δ_sum / Δ_count для
средней задержки за окно.
Гейджи GPU стримятся тоже, когда в запуске также включено
gpu.enabled: true (см. метрики GPU): каждый снапшот несёт сэмплы
GPU, снятые с момента предыдущего, как мгновенные гейджи с собственными
метками времени коллектора — отображайте их как есть, без дифференцирования:
| Отправляемый сэмпл | Метки | Единицы |
|---|---|---|
gpu_utilization_pct | gpu="<index>" | проценты, 0–100 |
gpu_memory_used_mib | gpu="<index>" | MiB |
gpu_memory_total_mib | gpu="<index>" | MiB |
gpu_temperature_c | gpu="<index>" | °C |
gpu_power_w | gpu="<index>" | ватты |
Поля, которые источник сообщил как N/A, пропускаются (никогда не отправляются
как нули), а дополнительные поля pro-коллектора передаются под собственными
именами. Без gpu.enabled снапшоты вообще не содержат данных GPU.
Защитные стратегии (цикл VU всегда в приоритете; стриминг уступает первым):
- CPU-гейт: пока загрузка CPU хоста (из
/proc/stat) равна или вышеmax_cpu_percent, ожидающие батчи удерживаются — без POST — а входящие снапшоты откладываются (ставятся в очередь, каждый десятый логирует предупреждение), затем сбрасываются в порядке поступления, когда гейт открывается. Вне Linux показаний CPU нет, поэтому гейт бездействует.max_cpu_percent: 0отключает его. - Неограниченный бэклог, мягкий предел: запечатанные, но недоставленные
батчи никогда не отбрасываются, пока запуск жив; превышение
max_pendingлишь логирует предупреждение с ограничением частоты. Доставка возобновляется по порядку, когда цель отвечает. - Повторы: сетевые ошибки, 5xx и 429 повторяют тот же батч с экспоненциальной задержкой ×2 от 1 с, ограниченной 60 с. Любой другой 4xx считается ядом — батч отбрасывается и не повторяется.
- Завершение: когда запуск заканчивается, всё ещё ожидающее — включая снапшоты, отложенные CPU-гейтом, — сбрасывается с короткими ограниченными повторами (1/2/5/10 с), после чего shipper завершается. CLI ждёт этого финального дренажа (ограниченного), чтобы последний батч дошёл до выхода процесса.
Движок никогда не блокируется на стриминге: снапшоты идут через ограниченный
канал с try_send, и полный канал отбрасывает снапшот вместо обратного давления
на VU — единственное место, где точка может быть потеряна в середине запуска.
При отсутствующем или ложном during_run нет ни дополнительной задачи, ни
трафика канала вовсе.
Замечание по аутентификации: CLI отправляет без аутентификации (как и сводка
в конце запуска). Аутентифицированный путь — у агента: он запускает тот же
shipper с провайдером токенов Keycloak и taskId (также проставляемым как
метка task_id на каждом сэмпле).
На платформе (perfscale.su или self-hosted controlplane) поток оживляет
страницу запуска в реальном времени — квантили задержки, пропускная
способность, доля ошибок и кривые GPU рисуются по мере поступления сэмплов.
Агент подключает всё сам: report.during_run: true в конфигурации запуска —
единственное действие пользователя — он аутентифицирует поток своим машинным
токеном и помечает каждый сэмпл task_id и machine_id. Те же сэмплы питают
дашборд Metrics, Prometheus-совместимый query API (направьте Grafana на
/api/v1/integrations/prometheus) и Remote Write в ваш собственный
Prometheus/Mimir.