Приветствую, любители и професионалы нагрузки! Сразу переходим к прошлым выпускам и далее по тексту:

Прошлые выпуски

Главная новость цикла — в v0.30.0 реализован RFC 009: теперь можно написать свой шаг. Не функцию‑генератор значения, а полноценный use:‑шаг со своей логикой, проверками и метриками. Вокруг этого — Node.js‑шаги как первая библиотека на новом механизме, Faker, gzip, нагрузка с нескольких IP и отдельные Docker‑образы под каждый движок. И, конечно, фейлы, поехали

Perfscale OSS

RFC 009: свои шаги на WASM

До сих пор у пользователя с нестандартной задачей было три пути: втиснуть её в std/tcp и генераторы токенов, просить PR в движок или ждать pro‑модуль. Проприетарный протокол поверх TCP, подпись всего запроса целиком, «посчитай → проверь → запиши метрику» — всё это упиралось в то, что расширять можно было только значения (${mylib.fn(...)}), но не шаги.

RFC 009 закрывает эту дыру. Тот же WASM‑компонент, который раньше экспортировал функции‑генераторы, теперь может экспортировать шаги, и они вызываются из use: как lib/<alias>/<step>:

libraries:
  - use: ./libs/acme-signer.wasm
    capabilities: [clock]
    as: sig

steps:
  - name: login
    use: lib/sig/login          # шаг `login` из компонента
    outputs: auth
    with:
      user: "trader-${{ vu }}"
      password: ${{ env.DESK_PASSWORD }}
  - name: verify
    use: std/check@v1
    with:
      on: auth.session

Да, я знаю, что lib/ префикс ужасный. Мы пока тестируем с префиксом, чтобы отделять «std» от «не std»

Для тех, кому нужно запустить внешний процесс, появилась новая capability process — одноразовый запуск через хост с ограничением вывода, таймаутом и зачисткой дерева процессов. Это сознательно «доверенный» режим, поэтому включается тройным согласием: грант в YAML, allow_library_capabilities: true и allow_process_actions: true. Без любого из трёх компонент просто не загрузится.

Node.js‑шаги — первая библиотека на RFC 009

Первым применением process стала библиотека Perfscale/nodejs и с ней три шага:

Шаг

Что делает

lib/nodejs/install

Скачивает официальный релиз с nodejs.org, сверяет SHA-256 с SHASUMS256.txt до распаковки, кладёт в локальный кэш

lib/nodejs/exec

Запускает inline‑скрипт (node -e) или файл нужной версией node

lib/nodejs/install-exec

Ставит (или берёт из кэша) и сразу запускает

# config.yaml — двойное согласие на process + установка node один раз
allow_library_capabilities: true
allow_process_actions: true
libraries:
  - use: ./nodejs.wasm
    capabilities: [process]
before:
  - name: node
    use: lib/nodejs/install
    with:
      node-version: 24.16.0
      install-dir: .perfscale-tools/nodejs
    outputs: setup
# test.yaml
steps:
  - name: inline-script
    use: lib/nodejs/exec
    with:
      node-path: "${{ config.setup.node-path }}"
      script: console.log(`hello from node ${process.version}`)
    check:
      message_contains: hello from node v24.16.0

Важная информация: количество RPS у вас упадет при использовании nodejs, это стоит понимать, поскольку nodejs не вытащит то количество RPS, которое может выдавать perfscale. Используйте внешние библиотеки на свой страх и риск

Никакого shell: каждый запуск — прямой argv с явным окружением, переменные хоста не наследуются. Пока Linux x86_64 и aarch64. Честное предупреждение: это шаги для оркестрации — подготовить фикстуры, сгенерировать данные, разовые проверки — а не горячий путь на тысячи RPS, каждый вызов — настоящий процесс.

Следом идёт Python по той же модели — официальный standalone CPython, проверка checksum, изолированная установка и те же три шага. Репозиторий Perfscale/python уже создан, библиотека в работе.

Faker

Perfscale/faker — фейковые данные для payload’ов: email, адреса, тестовые платёжные данные.

libraries:
  - use: ./faker.wasm
    capabilities: []
steps:
  - use: std/http@v1
    with:
      method: POST
      url: https://api.example.com/signup
      body:
        email: "${faker.internet("email")}"
        city: "${faker.address("city")}"

Faker подключается и через официальный GitHub Action — тот теперь умеет тянуть кастомные библиотеки из GitHub Releases.

Типизированные inputs: ‑i и ‑e

Один и тот же сценарий гоняют на разных окружениях, с разным числом пользователей и опциями. Раньше это решалось правкой YAML или зоопарком переменных окружения. Теперь конфиг объявляет типизированные параметры прогона, шаги читают их как ${{ inputs.<name> }}, а значения задаются при запуске:

# config.yaml
inputs:
  users: number                                    # обязательный, без default
  host: # host
    type: string
    default: localhost
  verbose: { type: bool, default: false }          # принимает 0/1/false/true
  env: { type: choice, options: [staging, prod], default: staging } # как в github actions
  pacing_ms: { type: number, min: 1, max: 10000 }  # границы для number
# test.yaml
steps:
  - use: std/http@v1
    with:
      url: "https://${{ inputs.host }}/api/orders?env=${{ inputs.env }}"

Ну и запуск выглядит так:

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-файла
perfscale lint config.yaml -i users=10    # покажет итоговые значения

Приоритет простой: -i в командной строке, затем env‑файл (-e, из него берутся только объявленные ключи, так что общий .env подойдёт), затем default:. Главное — всё проверяется до старта: пропущенный обязательный input, неизвестный ключ, users=ten вместо числа, число вне min/max, значение choice не из списка — это ошибка в perfscale lint и при запуске, а не полунастроенный прогон, который упадёт на десятой минуте.

Ограничение: inputs подставляются в with:/check: шагов, а vus: и duration: пока типизированы статически — профиль нагрузки на основе inputs сделаем следующим релизом.

Нагрузка с нескольких IP

Классическая стена в нагрузке на соединения — ~64 тысячи, и упирается она не в CPU и не в память, а в сам TCP. Соединение определяется четвёркой (IP источника, порт источника, IP назначения, порт назначения): с одного адреса на один эндпоинт меняется только порт источника, а эфемерных портов меньше 64 k. Для сценариев «N тысяч пользователей держат постоянное соединение» это потолок, который обычно обходят кучей машин‑генераторов.

std/http@v1 умеет привязывать исходящие соединения к списку локальных адресов — каждый адрес даёт свои ~64 k портов:

steps:
  - use: std/http@v1
    with:
      url: http://target:8080/api
      pool: dedicated                 # своё постоянное соединение на VU
      source_ips: ["10.0.0.2", "10.0.0.16/28", "fd00::/120"]

gzip в std/http@v1

Сжатые ответы (Content-Encoding: gzip) теперь декодируются прозрачно — достаточно отправить Accept-Encoding: gzip в headers, и распакованное тело уходит в check: и в пути вида ${{ feed.body.posts }} как обычный JSON. Лимит max_response_body_bytes применяется к распакованным данным, так что gzip‑бомба не съест память генератора (ранее мы поймали такой сценарий в рамках тестов с LLM агентами, CVE потенциальный закрыт).

Docker: отдельный образ под каждый движок

Раньше варианты образа были суффиксами тегов одного пакета (perfscale:0.29.0-k6), и на странице пакетов это выглядело как «один образ со всем подряд». Теперь каждый вариант — отдельный пакет:

Нужен только k6 — docker pull ghcr.io/perfscale/perfscale-k6:latest, без JRE и Python в довесок. Старые теги с суффиксом продолжают публиковаться на те же образы — ничего не сломается (надеюсь).

Бенчмарки: SyTester в общих таблицах

В bench появился ещё один конкурент — SyTester от СберТеха. Он встаёт в таблицы throughput, saturation, gRPC и SQL рядом с k6, locust и JMeter. VU‑модели у SyTester нет — он держит фиксированный TPS, поэтому получает свой максимум (10 000 TPS по схеме плана). На локальном прогоне при одинаковых 10 000 RPS на GET /health:

RPS

p99

CPU на запрос

Peak RSS

perfscale (arrival)

9 997

0.57 мс

59 µs

27.5 MiB

SyTester

9 406

9.68 мс

743.5 µs

945.9 MiB

Это локальные цифры (macOS); канонические — в CI‑отчёте bench. Заодно в workflow бенча появился input apps — выбор движков‑конкурентов (k6 jmeter locust sytester), а опечатка в suites или apps теперь роняет прогон сразу, а не молча выкидывает строку из отчёта.

Фейлы недели

  1. arrival выбрасывал 85% итераций. Добавляя SyTester, мы сначала сравнили его с нашей открытой моделью (arrival:) — и проиграли: на 10 000 итераций/с perfscale выдавал 721 RPS и выбрасывал 42 777 итераций при 256 простаивающих воркерах. Причина — канал выдачи итераций глубиной в один слот: планировщик просыпается раз в ~1 мс и раздаёт накопившиеся итерации пачкой, а вторая итерация пачки видела занятый слот и считала, что «все VU заняты». Теперь решение принимает счётчик реально выполняемых итераций. Было 721 RPS, стало ~10 000 с почти нулём отбросов. Если в старых прогонах на высокой интенсивности вы видели много dropped_iterations — перезапустите на v0.29.0+.

  2. Бенчмарк был нечестным. Порты между строками отчёта освобождались только в сюитах db, pubsub и boundary. После gRPC‑строки (подключение на каждый вызов) на порту висело 16 292 сокета в TIME_WAIT — и следующая строка, кем бы она ни была, мерила потолок свободных портов машины, а не движок. Теперь перед каждой строкой бенч ждёт, пока TIME_WAIT опустится ниже 1000.

  3. Кэш CI выедал сам себя. PR‑прогоны сохраняли свои копии Rust‑кэша по 2 ГБ, а релизные сборки — ещё ~2.9 ГБ на тег, которые никто никогда не восстановит. Лимит в 10 ГБ переполнялся, кэш main вытеснялся, и каждый PR компилировался с нуля: шаг тестов шёл 8 минут вместо 4. Теперь кэш пишет только main, а релизы его только читают.

Perfscale platform?

На этой неделе обновлений нет, кроме бампа на последний perfscale oss для синхронизации разработки, следите за обновлениями!

Ссылки