Производительность10 сентября 2026 г. · 8 мин чтения

Живые метрики во время прогона: что шлёт perfscale, когда — и когда отказывается слать

perfscale v0.18.0 стримит метрики на платформу, пока нагрузочный тест ещё идёт: перцентили, счётчики, мощность GPU и ANE. Но самое интересное — инженерия, которая гарантирует, что стриминг не может исказить нагрузку, которую измеряет.

Автор: Команда Perfscale

Живые метрики во время прогона: что шлёт perfscale, когда — и когда отказывается слать

Каждый, кто гоняет нагрузочные тесты, знает этот ритуал: запускаешь 30-минутный прогон и пялишься в терминал, ожидая, когда итоговая сводка расскажет о том, что уже случилось. k6 печатает summary по окончании прогона, JMeter — тоже. Хотите наблюдать за прогоном — поднимайте InfluxDB с Grafana, платите за k6 Cloud или собирайте xk6-расширение вывода — и всё это до того, как вы написали первого виртуального пользователя.

perfscale v0.18.0 переворачивает дефолт. Один YAML-блок — и движок стримит метрики на платформу прямо во время подачи нагрузки. И, что важнее, стриминг спроектирован так, чтобы он не мог исказить измерение, о котором отчитывается.

Что мы шлём

Каждые interval_ms (по умолчанию 5 с) движок снимает кумулятивный снапшот всего, что прогон насобирал к этому моменту, разворачивает его в сэмплы в стиле Prometheus и отправляет на платформу:

  • Тренды и гистограммы — например, http_req_duration — превращаются в http_req_duration{quantile="0.5|0.9|0.95|0.99"} плюс http_req_duration_count и http_req_duration_sum. Значения шлются в секундах, по конвенции Prometheus.
  • Счётчики становятся *_total.
  • Доли ошибок становятся парой: rate-метрика вроде http_req_failed шлёт <name>_total (вызовы) и <name>_failed_total (ошибки) — так rate остаётся вычислимым на любом окне.
  • Пользовательские метрики подхватываются автоматически. Всё, что эмитят ваши шаги — латентность gRPC, время запросов к БД, llm_ttft_ms, счётчики WebSocket-сессий — оказывается в снапшоте без единой лишней строчки конфигурации.
  • Телеметрия GPU, когда включён gpu.enabled: gpu_utilization_pct, gpu_memory_used_mib, gpu_temperature_c, gpu_power_w по каждому устройству. На Apple Silicon (через powermetrics) сюда же попадает ane_power_w — потребление Neural Engine вашего Mac, вживую, на одной шкале времени с HTTP-латентностью.

Каждый сэмпл помечен лейблами task_id и machine_id, так что распределённый прогон чисто раскладывается по генераторам нагрузки. Снапшоты кумулятивные (HDR-гистограммы не сбрасываются в середине прогона); потребители диффают соседние снапшоты, чтобы получить значения за интервал.

Когда мы шлём

Батч запечатывается, когда набирает batch_size сэмплов (по умолчанию 500), либо по тику в 5 секунд — что наступит раньше — и уходит POST'ом на /api/v1/metrics со строго возрастающим seq. Платформа дедуплицирует по (task_id, seq), так что переотправленный батч никогда не задвоит данные.

Стриминг покрывает ровно фазу нагрузки: стартует непосредственно перед запуском виртуальных пользователей и заканчивается вместе с ними. Лог прогона сообщает, что всё работает:

during-run metrics: snapshots every 5000ms (batch ≤ 500, cpu gate 90%)

Когда прогон завершается, shipper сливает всё, что осталось в очереди (ограниченные ретраи, жёсткий предел 30 с), а следом, как всегда, приходит классическая итоговая сводка в формате k6. Живые данные — дополнение, а не замена.

Когда мы отказываемся слать

Вот этой частью мы гордимся по-настоящему. Мониторинг, который конкурирует с генератором нагрузки за CPU, в итоге измеряет сам себя. Поэтому shipper живёт по одному правилу: генерация нагрузки всегда важнее отчётности о ней.

Конкретно:

  • Движок никогда не блокируется на метриках. Снапшоты уходят в ограниченный канал на 512 слотов через try_send. Канал полон — shipper медлит, платформа тормозит, сеть пропала — снапшот выбрасывается с предупреждением (не чаще одного на десять). Вы теряете одну точку на графике, но ни в коем случае не точность самой нагрузки.
  • Shipper — отдельная асинхронная задача с таймаутом запроса в 10 секунд. Ничто из того, что он делает, не касается цикла виртуальных пользователей.
  • CPU-гейт. Shipper следит за загрузкой CPU хоста (через /proc/stat). Пока busy-CPU держится на уровне max_cpu_percent или выше — по умолчанию 90% — он придерживает все ожидающие батчи и складывает входящие снапшоты в очередь, а не выбрасывает: ноль HTTP-запросов, пока машина не придёт в себя, затем накопленные точки уходят по порядку. Если генератор нагрузки упёрся в потолок, perfscale замолкает, а не дерётся с VU за последние ядра — и не теряет ни одной точки. Честная оговорка: гейт читает /proc/stat, поэтому на сегодня он только для Linux; на macOS и Windows остаётся ограниченный канал из пункта выше.
  • Ничего не выбрасывается в середине прогона. Запечатанные, но недоставленные батчи хранятся, сколько бы ни длился сбой — max_pending (по умолчанию 24) теперь мягкий предел, который управляет предупреждениями, а не шредером. Мёртвая платформа отложит ваши графики, но не съест их.
  • Плавная деградация вместо падений. Сетевые ошибки, 5xx и 429 ретраят тот же батч с тем же seq (бэкофф ×2 от 1 с, потолок 60 с). Любой другой 4xx означает, что payload отравлен — он выбрасывается и никогда не ретраится.

Единственное исключение — финальный дренаж: как только VU остановились, CPU свободен, гейт открывается, и всё оставшееся уходит на платформу.

Что вы видите на платформе

Страница прогона рисует графики по мере прихода сэмплов — перцентили латентности, RPS, доля ошибок, кривые GPU — без обновления страницы и ожидания сводки. Под капотом — настоящее хранилище таймсерий: платформа отдаёт Prometheus-совместимый query API, на который можно натравить Grafana, и Remote Write, если удобнее пушить всё в свой Prometheus или Mimir. А поскольку сэмплы несут machine_id, вопрос «эта вспышка — один заболевший генератор или сервер?» решается запросом по лейблу, а не гаданием.

В демо-тенанте на perfscale.su уже заведена конфигурация Live Metrics (during-run) и небольшой демо-тест: войдите под демо-аккаунтом, запустите его и смотрите, как график загорается посреди прогона.

Как включить

# config.yaml
report:
  url: https://perfscale.su
  during_run: true        # по умолчанию false — только итоговая сводка
  interval_ms: 5000       # интервал снапшотов/флаша, минимум 1000
  batch_size: 500         # запечатать батч при таком числе сэмплов
  max_cpu_percent: 90     # CPU-гейт; 0 отключает его (Linux)
  max_pending: 24         # мягкий предел — предупреждение, батчи не выбрасываются

Один флаг — это вся история: during_run: true. Остальные ручки идут с разумными дефолтами. Учтите: стриминг работает для нативного движка — прогоны совместимости --k6, --jmeter и --locust по-прежнему дают нормализованную k6-образную сводку в конце, но не стримят (пока).

Чем это отличается от чистых k6 и JMeter

Честности ради: живые метрики возможны в обоих инструментах — вопрос в том, что вам придётся построить и поддерживать, чтобы их получить.

k6 OSSJMeterperfscale
Живые метрики из коробки✗ (сводка в конце)✗ (сводка в конце)✓ один YAML-флаг
Для живого просмотра нужныk6 Cloud или xk6-расширение + InfluxDB/PrometheusBackend Listener + InfluxDB/Graphite + Grafanaничего
Политика при перегрузке генераторана вашем пайплайне выводана вашем конфиге listener'австроена: пауза POST'ов при 90% CPU, очередь снапшотов, ноль потерь в прогоне
Телеметрия железа на той же шкале✗✗утилизация/VRAM/температура/мощность GPU, мощность ANE/CPU/пакета на Apple Silicon
Раскладка распределённого прогона по машинамручные тегиручные тегилейбл machine_id на каждом сэмпле

Философская разница — в среднем ряду: в perfscale защита измерения от измеряющей системы — задача движка, а не плагина, который вы обязаны правильно настроить под давлением обстоятельств.

Попробуйте

perfscale run -f test.yaml -c config.yaml

Полный справочник — в docs/core/metrics.md и YAML reference. Движок открыт: логика load-shedding живёт в crates/perfscale-core/src/report/stream.rs — можете проверить наши утверждения по коду, а в демо-тенанте на perfscale.su уже подключён рабочий пример.

Комментарии

Ответить в Bluesky