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%. Дополнительно можно вводить свои метрики на уровне бизнес-действий.

Нагрузочные профили: 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-протокол бинарный — заведите проверку корректности фреймов на уровне бизнес-логики (счетчик валидных сообщений, таймингов).
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 или используйте совместимый конвертер в пайплайне.

Быстрые 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 обязательно согласуйте профиль нагрузки и запретите тупые «шипы» вплоть до блокировок.
Типичные ошибки и чек-лист
- Отсутствие порогов. Тесты не проваливаются, даже если все плохо. Добавьте
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 в каждую сборку, отводите место для длинных прогонов, держите метрики и тэги в порядке — и у вас будет надежный контур контроля производительности, который растет вместе с проектом.


