Концепции
WebSocket
Нагрузочное тестирование WebSocket — сессии, стримы и ассерты сообщений
Обзор
perfscale нагружает WebSocket-endpoint'ы: открывает соединения, генерирует потоки сообщений из шаблонов, ждёт подходящие ответы и измеряет как handshake, так и прикладной round-trip сообщения — рядом с вашими HTTP/TCP/UDP-метриками.
WebSocket — часть open-source-движка: actions std/ws* доступны на любом
плане.
Два стиля, свободно совместимых:
- One-shot-сессия (
std/ws@v1) — connect, обмен, close в одном шаге. Вся сессия — один сэмпл латенси, как FIX-сессия. - Живое соединение (
std/ws-connect@v1и компания) — соединение живёт между шагами внутри итерации и адресуется по id, который вернул connect. Позволяет чередовать WS с HTTP: подписка по WS, триггер по REST, ассерт push-сообщения.
One-shot-сессия
steps:
- name: подписка и ожидание первого трейда
uses: std/ws@v1
with:
url: wss://stream.example.com/feed
messages:
- send: '{"op":"subscribe","channel":"trades","id":"sub-${seq}"}'
until_json: { type: trade }
check:
message_matches: { type: trade }
| Параметр | По умолчанию | Описание |
|---|---|---|
url | — | Цель ws:// или wss:// (обязателен) |
headers | — | Дополнительные заголовки handshake (auth-токены и т.п.) |
subprotocols | — | Предлагаемые Sec-WebSocket-Protocol — список или одна строка (например graphql-ws) |
skipTLSVerify | false | Принять любой сертификат — только self-signed staging |
connection | — | Объект-профиль, задающий значения по умолчанию для любого поля |
messages | [] | Что отправлять: строка-шаблон или { send, repeat, interval_ms, until_contains, until_json } |
timeout | 10000 | Миллисекунды на всю сессию |
Запись с правилом until_* дожидается подходящего ответа перед следующей — и
даёт один сэмпл message RTT (см. Метрики).
Вывод:
{ "connected": true, "sent": 1, "received": 2, "messages": ["…"], "body": "…",
"subprotocol": "graphql-ws", "duration_ms": 41.5,
"metrics": { "ws_msgs_sent": 1, "ws_msgs_received": 2, "ws_msg_rtt": [8.1] } }
Шаг падает при ошибках handshake/транспорта или на записи, чьё правило
until_* не совпало вовремя, — и всё равно отчитывается обо всём, что успело
пройти до этого момента, плюс поле error с именем сбойной записи
(message[i]: …). Ошибка handshake даёт
{ "connected": false, "error": "…", "duration_ms": … } и идёт в
http_req_failed.
timeout — инлайн-параметр конкретного шага: timeout, записанный внутри
профиля connection, игнорируется.
Живое соединение
# config.yaml — 25 конкурентных VU, у каждого своё соединение на итерацию
vus: 25
duration: 5m
# test.yaml
steps:
- name: открыть фид
uses: std/ws-connect@v1
with: { url: "wss://stream.example.com/feed" }
outputs: feed
- name: подписаться
uses: std/ws-send@v1
with:
id: "${{ feed.id }}"
send: '{"op":"subscribe","id":"sub-${seq}"}'
- name: дождаться подтверждения
uses: std/ws-recv@v1
with:
id: "${{ feed.id }}"
until_json: { type: subscribed }
outputs: got
- name: закрыть
uses: std/ws-close@v1
with: { id: "${{ feed.id }}" }
std/ws-connect@v1— открывает соединение, возвращает{ id, subprotocol, … }.std/ws-send@v1— шлётsend(текст, токены${…}раскрываются на каждую отправку) илиsend_base64(бинарный);repeat+interval_msдают поток из N сообщений по одному шаблону.std/ws-recv@v1— читает до условия остановки:until_contains(подстрока),until_json(JSON-subset-совпадение) или простоcount. Не дождались заtimeout— шаг падает.std/ws-ping@v1— транспортный ping→pong; RTT вduration_msшага.std/ws-close@v1— корректное закрытие с Close-handshake.
Соединение не переживает свою итерацию: всё, что сценарий оставил открытым,
сбрасывается в конце итерации (жёстко — для аккуратного закрытия используйте
ws-close).
Идентификаторы соединений
- Id выдаются per-VU (
ws-1,ws-2, …) и действительны только внутри текущей итерации этого VU — id не переходит ни в следующую итерацию, ни в другой VU. std/ws-connect@v1в блокеbefore:конфига бесполезен: контекст инициализации (и его сокеты) исчезает до старта VU.- Обращение к закрытому или неизвестному id роняет шаг с ошибкой "unknown connection id".
std/ws-connect@v1
Принимает те же параметры цели, что и one-shot-сессия (url, headers,
subprotocols, skipTLSVerify, connection); timeout здесь ограничивает
только handshake (по умолчанию 10000 мс). Вывод:
{ "id": "ws-1", "connected": true, "subprotocol": "graphql-ws", "duration_ms": 3.1 }
subprotocol — согласованный сервером протокол либо null, если сервер не
выбрал ни одного. Handshake идёт в http_req_duration; неудачный handshake
идёт в http_req_failed, а вывод имеет вид
{ "connected": false, "error": "…", "duration_ms": … }.
std/ws-send@v1
| Параметр | По умолчанию | Описание |
|---|---|---|
id | — | Id соединения от std/ws-connect@v1 (обязателен) |
send | одно из send/send_base64 | Текстовый payload; токены ${…} раскрываются на каждую отправку |
send_base64 | одно из send/send_base64 | Бинарный payload (несовместим с send) |
repeat | 1 | Сколько сообщений выпустить из одного шаблона |
interval_ms | 0 | Пауза между повторными отправками, мс |
timeout | 10000 | На весь цикл отправки, мс |
Вывод: { "sent": N, "bytes": B, "duration_ms": …, "metrics": { "ws_msgs_sent": N } }.
Ошибка транспорта роняет шаг и соединение; ошибка параметров (одновременно
send и send_base64) оставляет его рабочим.
std/ws-recv@v1
| Параметр | По умолчанию | Описание |
|---|---|---|
id | — | Id соединения (обязателен) |
until_contains | — | Стоп, когда сообщение содержит подстроку (несовместимо с until_json) |
until_json | — | Стоп, когда сообщение совпадает с объектом по JSON-subset — поля шаблона должны быть равны, лишние поля игнорируются |
count | 1 | Без правила until_*: стоп после N сообщений с данными |
timeout | 10000 | Дедлайн условия остановки, мс |
Вывод:
{ "messages": ["…"], "body": "…", "count": 2, "matched": true, "duration_ms": 8.4,
"metrics": { "ws_msgs_received": 2, "ws_msg_rtt": [7.9] } }
Текстовые фреймы приходят строками, бинарные — base64-строками; body —
склеенная переводом строки текстовая форма, поэтому работает
check: { body_contains: … }. matched показывает, достигнуто ли условие
остановки, а все прочитанные по пути сообщения остаются в messages при
любом исходе. Если пир закрыл соединение или транспорт умер раньше, шаг
падает, в вывод добавляется поле error, а соединение сбрасывается; обычный
таймаут тоже роняет шаг, но соединение остаётся рабочим. Ping/pong-фреймы,
пришедшие во время чтения, игнорируются как шум транспорта.
std/ws-ping@v1
Вывод: { "pong": true, "duration_ms": 0.4 }. Принимает id и timeout
(по умолчанию 10000 мс). RTT не агрегируется ни в одну гистограмму — при
нужде ограничьте его через check: { duration_ms_lt: … }. Сообщения с
данными, пришедшие во время ожидания pong, буферизуются для следующего
std/ws-recv@v1. Отсутствие pong за timeout (или закрытое соединение)
роняет шаг и соединение.
std/ws-close@v1
Отправляет Close-фрейм и ждёт подтверждение пира. Принимает id, code (по
умолчанию 1000, нормальное закрытие), reason (по умолчанию пусто) — оба
уходят в Close-фрейме — и timeout. Вывод: { "closed": true, "duration_ms": … } —
возвращается, даже если пир не подтвердил за timeout: сокет в любом случае
исчезает, а id освобождается.
Динамические сообщения
В текстовых payload'ах работают одинарные токены ${…}, раскрываемые на
каждую отправку — в отличие от ${{ … }}, который резолвится один раз до
запуска action:
| Токен | Раскрывается в |
|---|---|
${seq} | Монотонный счётчик, уникален на сообщение (продолжает считать между отправками одного соединения) |
${uuid} | Случайный 32-hex id |
${now} / ${now_ms} / ${now_iso} | UTC-время (FIX-формат / unix ms / RFC 3339) |
${rand(a,b)} / ${randf(a,b[,dp])} | Случайное целое / дробное в [a, b] |
${choice(x|y|z)} | Случайный выбор |
Неизвестные токены остаются как есть. Payload'ы send_base64 декодируются
один раз и уходят без изменений — раскрытия токенов нет.
- uses: std/ws-send@v1
with:
id: "${{ feed.id }}"
send: '{"op":"order","id":"ord-${seq}","px":${randf(1.05,1.15,5)}}'
repeat: 100
interval_ms: 50
Ассерты сообщений
Шаги приёма отдают список messages; std/check@v1 проверяет его с
квантором any (достаточно одного совпавшего сообщения — в стримах есть
heartbeat'ы и посторонние события):
check:
message_contains: "trade" # какое-то сообщение содержит подстроку
message_matches: { type: trade } # какое-то сообщение совпадает по JSON-subset
messages_count_gte: 5 # пришло не меньше 5 сообщений
Для детерминированных обменов адресуйте сообщение по индексу:
check: { on: got.messages.0, message_matches: { type: welcome } }.
Метрики
- Handshake и one-shot-сессии попадают в общую гистограмму латенси
(
http_req_duration) — перцентили сравнимы с HTTP/TCP/UDP. ws_msg_rtt— прикладной RTT сообщения: от отправки до первого ответа, совпавшего с вашим until-правилом. На дашборде теста — p50 / p95 / max.ws_msgs_sent/ws_msgs_received— скорость обмена сообщениями.- Ожидание server-push-потока намеренно не считается латенси — оно испортило бы общие перцентили.
Страница метрик распознаёт WebSocket-прогон автоматически и переключается на WS-набор плиток (throughput, message RTT, перцентили handshake).
Ограничения
Входящие протокольные лимиты идут из дефолтов WebSocket-библиотеки: сообщения
до 64 МиБ, отдельные фреймы до 16 МиБ — большее входящее сообщение убивает
соединение (а значит, и читающий его шаг). Значения таймаутов, счётчики
repeat и список messages встроенных ограничений не имеют.