Всех категорически приветствую, любители больших количеств девяток и ровных линий на графиках задержки!

На этой неделе у нас богатый выпуск: долгожданный WebRTC, фикс для FIX (да, каламбур засчитан) и полностью новый API reference. Но сначала — традиционная история с прошлыми выпусками.

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

Perfscale OSS

SDK библиотек: WIT 0.2.0 и Go‑пример

Помните Go‑шероховатости из прошлых выпусков? Теперь мы поправили это (насколько это возможно): в SDK появился пример на Go (TinyGo 0.42, Go 1.25+) [github], а сам интерфейс обновился до WIT 0.2.0 — настройки библиотеки теперь приезжают прямо в Ctx (контекст).

Важный момент для авторов библиотек: capabilities теперь обязательны. Сценарий без явного capabilities: (даже пустого []) падает на валидации — раньше такой сценарий молча работал, что приводило к сюрпризам. Отпишитесь, стало ли лучше или нет. Возможно, есть библиотеки, где capabilities нет но сценариев таких мы не увидели. Лучше упасть на lint, чем в проде (на мой личный взгляд).

Документация метрик стала честнее

Переработали все разделы Metrics в документации: каждая метрика теперь — это отдельный пункт с пояснением, когда, где и как она собирается. Отдельно расписали failed‑метрики: как из 0/1-сэмплов получается rate, и почему у фоновых сэмплеров его нет (нет «вызова», который мог бы упасть).

Perfscale platform

WebRTC

Да, случилось. Нагрузочное тестирование WebRTC через настоящий медиа‑путь стал доступным: каждый VU выполняет рукопожатия и гоняет тесты на готовых соединениях.

Q: Почему это важная фича? Ведь это peer‑to‑peer звонки

A: Нагрузка ложится больше на инфрастурктуру соединения пользователей. Т.е. до соединения peer‑to‑peer сервер должен «знать» клиентов, уметь проходить NAT/Firewall и после этого должен уметь их соединять с TUN/STUN сервером, и не забываем про close\disconnect пользователей. Тут много нюансов и подводных камней, поэтому давайте разберем то, что происходит внутри жизненного цикла звонка на самом простом сценарии:

  1. Создаются пары через shared variables под капотом (1000 VUS = 500 одновременных звонков)

  2. Подключение — стадии ice и dtls. Обе стороны применяют SDP друг друга и проходят стек: ICE — проверка кандидатов. DTLS‑SRTP рукопожатие

  3. Медиа — стадии publish и subscribe. Используем треки (audio + video 320×240@15fps)

  4. стадия hold. Звонок просто живёт в фоне какое‑то время (задается в конфиге)

  5. Stats + close. Собираем статистику и закрываем соединение

Всё это уже в агенте (perfscaled) 0.4.0 и доступно для всех с версиями pro и enterprise.

Кстати, самый простой сценарий(тест) описанный здесь выглядит так:

# file: test.yaml
name: webrtc-p2p-call
steps:
  - name: p2p call
    use: pro/webrtc-call@v1
      with:
        # ── Стадия 1: pairing (рандеву через shared variables) ──
        pairing:
         driver: redis            # memory — если все VU в одном процессе;
    # redis — когда 1000 VU размазаны по агентам
         key: webrtc-room-1       # namespace рандеву: <key>:offer|answer:<pair_id>
         strategy: adjacent       # vu 2k-1 звонит vu 2k → 1000 VU = 500 пар
    # strategy: custom + pair_id/role — если нужно своё правило пар

    # ── Стадии 2–3: connect (ICE + DTLS-SRTP) и медиа (publish/subscribe) ──
           media: bidirectional       # обе стороны и шлют, и принимают
           ice_servers: []            # пусто = host-кандидаты только (один контур);
                                      # убрать ключ — дефолтный STUN Google;
                                      # или свой TURN для честной NAT-нагрузки
           tracks:                    # дефолт и есть Opus + VP8 320x240@15,
             - kind: audio            # но для читаемости — явно:
               codec: opus
               source: synthetic
               bitrate: 64kbps
             - kind: video
               codec: vp8             # vp8/h264 = встроенный ассет 320x240@15fps;
               source: synthetic      # codec: av1 = честное кодирование rav1e
               bitrate: 800kbps       # в resolution/bitrate заявленного размера
               resolution: 320x240
           # ── Стадия 4: hold — звонок живёт, медиа течёт, getStats-сэмплер тикает ──
           hold: 30s
           # ── Стадия 5 (stats + close) выполняется сама, параметры не нужны ──
           timeout: 10000             # дедлайн рандеву + сигналинга

Ну а конфигурация соответственно, так:

# file: config.yaml
vus: 1000                      # → 500 одновременных звонков
duration: 10m

# TURN, если надо грузить relays, а не host:
# webrtc:
#   ice_servers:
#     - urls: ["turn:turn.example.com:3478"]
#       username: ${TURN_USER}
#       credential: ${TURN_PASS}
# кол-во соединений и настраивается это на стороне сервера
#   max_peer_connections: 1200   

# Общие переменные для теста между агентами:
shared_variables:
  driver: redis
  redis_url: ${REDIS_URL}

# SLO-гейты: rate — «как часто падаем», stage-счётчики — «где именно»
thresholds:
  webrtc_setup_ms: ["p(95)<2000"]
  webrtc_setup_ms_failed: ["rate<0.05"]
  webrtc_call_errors_ice: ["count==0"]
  webrtc_call_errors_dtls: ["count==0"]
  webrtc_call_errors_signaling: ["count<10"]   # >0 при нечётном VU — это норма

Пару заметок к этому сценарию:

  • hold: 30s — это время жизни одного звонка внутри итерации. За duration: 10m каждая пара перезвонит ~20 раз: суммарно ~10k звонков при стабильных 500 одновременных. Хочешь «длинные звонки без перезвонов» — ставь hold ≈ duration.

  • Нечётный vus оставит старшего VU без пары — он будет офферить и падать по таймауту со счётчиком webrtc_call_errors_signaling. Это by design(пары то нет), поэтому в thresholds выше стоит count<10, а не ==0.

  • Стоимость на железе примерно такое: 500 пар = 1000 DTLS‑стеков + 2000 RTP‑потоков. VP8 почти бесплатен по CPU; но если переключить видео на codec: av1 то надо закладывать ~1 ядро на каждый кодируемый трек, то есть ~1000 ядер на эту конфигурацию (это очень много, поэтому аккуратно с av1). Для 1000 VU реалистичнее VP8 либо меньше VU с AV1.

Фикс fix

Неловкая история: pro/fix* шаги были реализованы, но… не подключены в агент perfscaled. Классический «код есть, а register() вызвать забыли». Теперь FIX запечён в агент 0.4.0 вместе с остальными pro‑семействами, плюс появился новый e2e тест, чтобы такое больше не повторилось.

Новый API reference

Controlplane теперь сам генерирует OpenAPI, а страница /docs/api‑reference — интерактивная документация со Swagger UI и Try‑it‑out, спецификацию можно скачать. Задокументировали 402 Payment Required, поскольку апи можно пользоваться только в случае оплаты и убрали localhost из примеров — везде честные URL. И да, доки наконец отвечают на языке интерфейса: i18n добрался и до справочника.

По мелочи

Валидация сценария теперь происходит при сохранении теста — ошибка прилетает сразу с 422, а не посреди прогона на агенте (а то у одного клиента был такой баг). Починили тёмную тему в доках. Scalar(тот, кто рисует по swagger интерактивную доку) перебивал фон своим глобальным стилем и из‑за этого ночью было больно смотреть.

Фейлы недели

Не обошлось без классики, но на этот раз фейлил GitHub. Вечером понедельника (05 октября 2026 года) hosted‑раннеры Actions перестали выдаваться: деплой‑джобы умирали ровно через 15 минут, не добравшись до первого шага, с честным «job was not acquired by Runner». Наши конфиги были ни при чём — лечилось терпением и rerun’ами. Если ваши пайплайны в тот вечер тоже мистически висели — вы не одни.

Ну а на этом на этой неделе всё! Традиционно желаю не болеть в холодные дни, ведь отопление уже должны были дать; а вашим сервисам желаю держать пять девяток (99,999%), даже когда GitHub болеет.

Ссылки

*Facebook и кампания Meta** — нежелательные организации на территории РФ