OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

k6 для HTTP и WebSocket: thresholds, сценарии и интеграция в CI/CD

Разбираем, как писать сценарии k6 для HTTP и WebSocket, выбирать профиль нагрузки (VUs vs arrival-rate), задавать строгие thresholds и теги, собирать отчеты и встраивать прогоны в CI/CD. Дадим шаблоны, рекомендации по стабильности, типичные ошибки и чек‑лист для быстрого запуска на проекте.
k6 для HTTP и WebSocket: thresholds, сценарии и интеграция в CI/CD

k6 стал де-факто стандартом легковесного нагрузочного тестирования для веб-проектов: простой JavaScript-DSL, хорошие метрики из коробки, гибкие thresholds и удобная интеграция в CI/CD. В этой статье соберу практику по HTTP и WebSocket, типам сценариев, провальным и успешным порогам, а также покажу, как подружить k6 с пайплайнами, артефактами и отчетами.

Зачем k6 для HTTP и WebSocket в CI/CD

Нагрузка — это не только редкий «перед релизом». Выигрывает тот, кто запускает небольшие, но регулярные проверки на каждом изменении. k6 идеально пригоден для этого: маленький footprint, скрипты на JS, пороги, по которым можно завалить сборку, и сценарии, покрывающие как REST/HTTP, так и real-time через WebSocket.

При грамотной постановке k6 тесты становятся «охранной сигнализацией» производительности. Из CI/CD они гарантируют, что p95 латентность не выросла, error-rate не пополз вверх, а чаты/уведомления по WS не деградировали. Более тяжелые прогоны можно запускать по расписанию (nightly) или перед крупными релизами.

Базовая структура скрипта k6

Скрипт k6 — обычный модуль ES. Вы импортируете http, объявляете options (пороговые значения, сценарии), пишете default или именованные функции под сценарии и используете check для функциональной валидации ответов.

import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<500', 'avg<300']
  },
  vus: 10,
  duration: '1m'
};

export default function () {
  const res = http.get('https://example.test/api/health');
  check(res, {
    'status is 200': r => r.status === 200,
    'has flag': r => r.json('ok') === true
  });
  sleep(1);
}

Такой smoke-сценарий легко запускать на каждом PR. Он быстро отлавливает регрессии и соблюдает базовые SLA (ошибок меньше 1%, p95 не выше 500 мс).

Ключевые метрики из коробки

k6 публикует полезные метрики без дополнительной настройки:

  • http_req_duration — полная длительность запроса (включая DNS, TLS, TTFB, body).
  • http_req_failed — доля неуспешных запросов (rate).
  • checks — доля успешных проверок check.
  • iterations — количество итераций VU.

В большинстве проектов достаточно держать p95 по http_req_duration внутри SLO и следить, чтобы http_req_failed оставался меньше 1%. Дополнительно можно вводить свои метрики на уровне бизнес-действий.

Пороговые метрики k6 во время прогона в терминале

Нагрузочные профили: VUs vs arrival-rate

В k6 есть два базовых способа описывать нагрузку:

  • Пул VU — фиксированное число виртуальных пользователей, каждый выполняет сценарий в цикле (vus + duration, ramping-vus).
  • Поток RPS — фиксированная скорость поступления запросов/итераций (constant-arrival-rate, ramping-arrival-rate), что лучше для API с требованием к стабильной RPS.

Например, если нужно проверить API на 300 RPS с контролируемым разогревом и удержанием, используйте ramping-arrival-rate. Для пользовательских сценариев с «мысленным временем» и паузами — ramping-vus.

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    api_rate: {
      executor: 'ramping-arrival-rate',
      startRate: 50,
      timeUnit: '1s',
      preAllocatedVUs: 50,
      maxVUs: 200,
      stages: [
        { target: 100, duration: '1m' },
        { target: 300, duration: '2m' },
        { target: 0, duration: '30s' }
      ]
    },
    ui_vus: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [
        { target: 30, duration: '1m' },
        { target: 30, duration: '2m' },
        { target: 0, duration: '30s' }
      ]
    }
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<600']
  }
};

export function api_rate () {
  const r = http.get('https://example.test/api/items');
  check(r, { '200': x => x.status === 200 });
}

export function ui_vus () {
  const r = http.get('https://example.test/');
  check(r, { '200': x => x.status === 200 });
}

Два сценария можно запускать параллельно, каждый — со своим профилем нагрузки и логикой.

WebSocket: корректная модель общения и метрики

Для real-time проверок k6 поддерживает WebSocket. Типовой кейс: коннект, авторизация, подписка, обмен сообщениями и контроль времени ответа на ping/pong или прикладные события. Важно явно задавать таймауты, проверять события и считать собственные метрики, если стандартных недостаточно. Для wss:// позаботьтесь о валидном TLS: используйте актуальные SSL-сертификаты и обратитесь к материалу по настройке TLS и HSTS.

import ws from 'k6/ws';
import { check, sleep } from 'k6';
import { Trend, Rate, Counter } from 'k6/metrics';

const wsLatency = new Trend('ws_latency_ms');
const wsOk = new Rate('ws_ok');
const wsMsgs = new Counter('ws_msgs');

export const options = {
  thresholds: {
    ws_ok: ['rate>0.99'],
    ws_latency_ms: ['p(95)<3000'],
    'checks{proto:ws}': ['rate>0.99']
  },
  vus: 10,
  duration: '1m'
};

export default function () {
  const url = 'wss://example.test/ws/chat';

  const res = ws.connect(url, { tags: { proto: 'ws', endpoint: '/ws/chat' } }, function (socket) {
    const t0 = Date.now();

    socket.on('open', function () {
      socket.send(JSON.stringify({ type: 'auth', token: 'test-token' }));
      socket.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
    });

    socket.on('message', function (data) {
      wsMsgs.add(1);
      try {
        const msg = JSON.parse(data);
        if (msg.type === 'pong') {
          wsLatency.add(Date.now() - t0);
          wsOk.add(true);
        }
      } catch (e) {
        wsOk.add(false);
      }
    });

    socket.setTimeout(function () {
      socket.close();
    }, 5000);
  });

  check(res, { 'WS status is 101': r => r && r.status === 101 }, { proto: 'ws' });
  sleep(1);
}

Здесь мы:

  • Создаем кастомные метрики для латентности WS-ответа, доли успешных обменов и числа сообщений.
  • Маркируем метрики тэгами (proto, endpoint) — так удобнее писать точечные thresholds и фильтровать в отчетах.
  • Проверяем код апгрейда (101) через check.

Тонкости WebSocket-тестов

Не забывайте о таймаутах и чистом закрытии сокета, иначе сценарий может «зависнуть» и исказить метрики. Для бекенда с бэкоффом и ретраями важно симулировать реальную паузу между попытками. Если WS-протокол бинарный — заведите проверку корректности фреймов на уровне бизнес-логики (счетчик валидных сообщений, таймингов).

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Thresholds: от smoke до регрессии SLA

thresholds — сердце «защитных» тестов. Они превращают нагрузку в строгий «гейт» CI: любое нарушение порога приводит к неуспеху job. Пороги можно навешивать на:

  • Встроенные метрики (http_req_duration, http_req_failed, checks).
  • Кастомные (Trend, Rate, Counter, Gauge).
  • Фильтрацию по тэгам, например только для endpoint:/api/cart.
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend } from 'k6/metrics';

const addToCart = new Trend('biz_add_to_cart_ms');

export const options = {
  thresholds: {
    http_req_failed: ['rate<0.005'],
    'http_req_duration{type:api}': ['p(95)<400'],
    biz_add_to_cart_ms: ['p(90)<300', 'max<2000'],
    'checks{critical:true}': ['rate>0.999']
  }
};

export default function () {
  const r = http.post('https://example.test/api/cart', JSON.stringify({ sku: 'ABC', qty: 1 }), {
    headers: { 'Content-Type': 'application/json' },
    tags: { type: 'api', endpoint: '/api/cart' }
  });
  addToCart.add(r.timings.duration);
  check(r, { 'added': x => x.status === 200 }, { critical: 'true' });
  sleep(1);
}

Смысл в том, что вы выражаете SLO на языке порогов: «p95 < 400 мс, ошибок < 0.5%, критические чек-пойнты проходят 99.9%». Такие гейты отлично живут в CI и не требуют «ручной» оценки.

Кастомные метрики и тэги

Как только сценарий начинает описывать бизнес-действия (оформление заказа, поиск, авторизация), полезно завести отдельные Trend по шагам и рассчитывать thresholds именно на них. Тэги помогут агрегировать метрики в отчетах и отделять API, UI и WS-каналы.

Организация тестовых наборов

Типовая структура репозитория:

  • tests/smoke/*.js — быстрые проверки для каждого PR.
  • tests/regression/*.js — более тяжелые сценарии для nightly.
  • tests/data/*.json — подготовленные фикстуры.
  • env.json — хосты, токены и таймауты по окружениям (dev/stage/prod).

Храните секреты вне Git (переменные CI, Vault). Разделяйте тестовые и продакшен-данные, используйте идемпотентные операции или «песочницы» в API, чтобы не засорять реальные базы.

Интеграция в CI/CD

k6 завершает процесс с ненулевым кодом, если нарушены пороги — этого достаточно, чтобы job помечался как failed. Самый простой путь — запускать через готовый контейнер.

GitHub Actions: быстрый старт

name: k6-tests
on: [push, pull_request]
jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Run k6 (smoke)
        run: |
          docker run --rm -v "$PWD":/work -w /work grafana/k6 run tests/smoke/http_smoke.js

Для стабильности закрепляйте версии контейнера, прокидывайте переменные окружения с токенами и параметрами сценария, а артефакты (файлы отчета) сохраняйте в job.

GitLab CI: job с образом k6

stages:
  - test

k6_smoke:
  stage: test
  image: grafana/k6:latest
  script:
    - k6 run tests/smoke/http_smoke.js
  artifacts:
    when: always
    paths:
      - reports/

Тяжелые сценарии можно вынести в отдельный job по расписанию и, например, запускать из изолированного runner, чтобы не влиять на короткие CI-циклы приложения.

Отчеты и артефакты: summary JSON, текст, CSV

Через handleSummary легко сделать машинно-читаемые отчеты, которые потом можно прикреплять к job как артефакты или отдавать во внешние системы.

import http from 'k6/http';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.4/index.js';

export function handleSummary(data) {
  return {
    'reports/summary.json': JSON.stringify(data, null, 2),
    'reports/summary.txt': textSummary(data, { indent: ' ', enableColors: false })
  };
}

Текстовая сводка удобно читается прямо в интерфейсе CI, а JSON можно разбирать для дашбордов. Если нужен JUnit, реализуйте простую конвертацию метрик в XML в handleSummary или используйте совместимый конвертер в пайплайне.

Job в GitLab CI запускает k6 и сохраняет отчеты

Быстрые vs длительные тесты: как встроить в циклы

Рекомендация по слоям:

  • PR/commit — smoke: 10–30 VU, 30–60 секунд, проверка ключевых API и WS handshake.
  • Nightly — 5–15 минут, профили ramping-arrival-rate, проверка p95/p99 и устойчивости.
  • Периодические нагрузки — ситуативно долгие прогревы (soak) на отдельной среде.

Такой расклад минимизирует время обратной связи в PR, но позволяет вовремя ловить деградации производительности.

Тестовые данные и повторяемость

Идемпотентность и детерминированность — две опоры воспроизводимых тестов. Используйте:

  • Фиксированные сиды генераторов случайностей, если генерируете данные.
  • Выделенные тестовые учетные записи и корзины.
  • API «очистки» или TTL для временных сущностей.
  • Контроль окружений: dev/stage для нагрузки, prod — только в исключительных согласованных случаях.

Простое правило: каждый прогон должен оставлять систему в предсказуемом состоянии, иначе сравнение метрик теряет смысл.

Масштабирование прогонов: инфраструктура и сетевые нюансы

k6 способен генерировать ощутимую нагрузку даже на одной машине. Чтобы не «растаптывать» ваш CI-агент, вынесите тяжелые прогоны на отдельные раннеры или виртуальные серверы. Хорошая практика — запускать прогоны с ближайшей к стенду площадки, например с облачного VDS, чтобы снизить сетевую латентность и стабилизировать p95.

Следите за пропускной способностью канала и латентностью до тестируемого бэкенда: значения p95 чувствительны к сети так же, как к коду.

Не забывайте про этап прогрева (warm-up) — кэши, JIT и пулы соединений требуют нескольких десятков секунд стабильного трафика перед замерами SLA. Для API с rate limit обязательно согласуйте профиль нагрузки и запретите тупые «шипы» вплоть до блокировок.

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Типичные ошибки и чек-лист

  • Отсутствие порогов. Тесты не проваливаются, даже если все плохо. Добавьте thresholds как go/stop-критерий.
  • Смешение smoke и тяжелых тестов. Медленные job тормозят каждую сборку. Разведите по слоям.
  • Нет тэгов на метриках. Нельзя отделить API от WS, разные эндпоинты мешаются в кучу. Используйте tags.
  • Генерация нагрузки с узкого канала. Падение p95 из-за сети, а не сервиса. Перенесите раннер ближе к целевому стенду.
  • Нет фиксации версий. Разные версии k6/скриптов дают разные метрики. Пинтуйте образ/бинарники.
  • Неправильный профиль. vus для RPS-сервисов и наоборот. Выбирайте arrival-rate, если тестируете RPS.
  • Забытые таймауты WS. Висит сокет — висит job. Ставьте setTimeout и условия завершения.

Минимальный стартовый шаблон

import http from 'k6/http';
import ws from 'k6/ws';
import { check, sleep } from 'k6';
import { Trend, Rate } from 'k6/metrics';

const stepLogin = new Trend('step_login_ms');
const stepWs = new Trend('step_ws_latency_ms');
const okRate = new Rate('ok_rate');

export const options = {
  scenarios: {
    http_smoke: {
      executor: 'ramping-vus',
      startVUs: 0,
      stages: [ { target: 10, duration: '30s' }, { target: 0, duration: '10s' } ],
      exec: 'http_smoke'
    },
    ws_smoke: {
      executor: 'constant-vus',
      vus: 5,
      duration: '45s',
      exec: 'ws_smoke'
    }
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<600'],
    step_login_ms: ['p(90)<300'],
    step_ws_latency_ms: ['p(95)<2000'],
    ok_rate: ['rate>0.99']
  }
};

export function http_smoke () {
  const r = http.post('https://example.test/api/login', JSON.stringify({ u: 'u', p: 'p' }), {
    headers: { 'Content-Type': 'application/json' },
    tags: { type: 'api', endpoint: '/api/login' }
  });
  stepLogin.add(r.timings.duration);
  check(r, { '200': x => x.status === 200, 'has token': x => !!x.json('token') });
  sleep(1);
}

export function ws_smoke () {
  const res = ws.connect('wss://example.test/ws', {}, function (socket) {
    const t0 = Date.now();
    socket.on('open', function () {
      socket.send(JSON.stringify({ type: 'ping', ts: t0 }));
    });
    socket.on('message', function (msg) {
      try {
        const data = JSON.parse(msg);
        if (data.type === 'pong') {
          stepWs.add(Date.now() - t0);
          okRate.add(true);
          socket.close();
        }
      } catch (e) {
        okRate.add(false);
      }
    });
    socket.setTimeout(function () { socket.close(); }, 3000);
  });
  check(res, { '101': x => x && x.status === 101 }, { proto: 'ws' });
}

Рекомендации по эксплуатации

Для устойчивых оценок производительности придерживайтесь простых правил: фиксируйте версию k6 и скриптов, запускайте тяжелые прогоны из стабильной инфраструктуры рядом с тестируемой средой, отделяйте PR-smoke от регрессионной нагрузки, храните отчеты как артефакты, а пороги отражайте реальные SLO. И главное — не бойтесь дробить сценарии: лучше несколько небольших и понятных тестов, чем один «комбайн», в котором трудно найти источник деградации.

Работаете над снижением TTFB и ускорением отдачи статики? Обратите внимание на прием с HTTP 103 Early Hints — он помогает оптимизировать критический путь загрузки и может улучшить результаты ваших нагрузочных проверок.

Итоги

k6 дает понятный путь: JS-сценарии для HTTP и WS, выразительные thresholds как CI-гейты, профили нагрузки под RPS и пользовательские потоки, и отчеты, пригодные для автоматического анализа. Встраивайте smoke в каждую сборку, отводите место для длинных прогонов, держите метрики и тэги в порядке — и у вас будет надежный контур контроля производительности, который растет вместе с проектом.

Поделиться статьей

Вам будет интересно

DMARC: rua, p=quarantine и p=reject — как включить без потери доставляемости OpenAI Статья написана AI (GPT 5)

DMARC: rua, p=quarantine и p=reject — как включить без потери доставляемости

Пошаговое руководство для админов: что такое DMARC и агрегированные отчёты rua, как правильно оформить rua=mailto, собрать XML-отч ...
Floating IP для Nginx: keepalived VRRP, healthcheck и быстрый failover OpenAI Статья написана AI (GPT 5)

Floating IP для Nginx: keepalived VRRP, healthcheck и быстрый failover

Пошагово строим отказоустойчивый фронтенд на двух серверах Nginx с общим floating IP. Настраиваем keepalived (VRRP), HTTP health c ...
SYNPROXY в nftables: защита VDS от TCP SYN flood пошагово OpenAI Статья написана AI (GPT 5)

SYNPROXY в nftables: защита VDS от TCP SYN flood пошагово

SYN flood забивает TCP-очереди и conntrack на VDS, съедая CPU. SYNPROXY в nftables отсекает мусор до TCP-стека. Разбираем принцип, ...