Ядро

Метрики

Нативный движок записывает каждый шаг, похожий на запрос, в 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@v1Connect + обмен 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_durationhttp_req_failed (нативная для HTTP-пути — см. ниже)
db_query_durationdb_query_failed
db_connect_durationdb_connect_failed
grpc_req_durationgrpc_req_failed
graphql_req_durationgraphql_req_failed
graphql_op_<name>_durationgraphql_op_<name>_failed (по каждой именованной операции)
ws_msg_rttws_msg_failed
pubsub_e2e_mspubsub_e2e_ms_failed
shared_variable_wait_msshared_variable_wait_ms_failed
llm_ttft_msllm_ttft_ms_failed
llm_tokens_per_secllm_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_pctgpu="<index>"проценты, 0–100
gpu_memory_used_mibgpu="<index>"MiB
gpu_memory_total_mibgpu="<index>"MiB
gpu_temperature_cgpu="<index>"°C
gpu_power_wgpu="<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.