Нагрузочное тестирование REST API на k6: первый рабочий сценарий
Нагрузочное тестирование REST API на старте всегда страшнее, чем оказывается на практике. В голове ассоциация с JMeter и часами на конфигурацию, на деле — k6 ставится за две минуты, первый сценарий пишется за десять. Главная задача не «как написать тест», а «что именно мерить и какие выводы делать», и здесь чаще всего ломаются.
За последние пять лет я писала нагрузку на трёх проектах. Покажу, как собрать первый рабочий сценарий на k6, какие метрики брать всерьёз, и что не работает.
Почему k6, а не JMeter
Короткий ответ: код вместо XML. Сценарии на JavaScript читаются и ревьюятся как обычный код. CI запускает их одной командой. Метрики выгружаются в Prometheus или InfluxDB напрямую. JMeter — мощный инструмент, но его UI и формат XML мешают командной работе.
Установка:
# Linux
sudo apt install k6
# macOS
brew install k6
# или скачать бинарник с grafana.com/k6
Один бинарник, без зависимостей. Запускается одной командой, не требует JVM.
Первый сценарий
Самый простой кейс — нагрузка на один эндпоинт фиксированным числом виртуальных пользователей:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '30s',
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};
export default function () {
const response = http.get('https://api.staging.example.com/products');
check(response, {
'status is 200': (r) => r.status === 200,
'has products': (r) => r.json('items').length > 0,
});
sleep(1);
}
Запуск:
k6 run script.js
На выходе — табличка с метриками: количество запросов, средняя длительность, p95, p99, error rate. Если threshold не выполнен — k6 завершается с ненулевым кодом, и CI падает. Это можно блокировать на pre-merge.
Профили нагрузки
Прогон с фиксированным числом VU — это smoke. На реальных сценариях нужны профили. K6 поддерживает «stages»:
export const options = {
stages: [
{ duration: '2m', target: 50 }, // ramp up до 50 VU
{ duration: '5m', target: 50 }, // держим 50 VU
{ duration: '2m', target: 200 }, // ramp up до 200 VU
{ duration: '5m', target: 200 }, // держим 200 VU
{ duration: '2m', target: 0 }, // ramp down
],
};
Этот сценарий называется ramp-test или load-test. Показывает, как сервис ведёт себя под растущей нагрузкой и где ломается. На графике p95 latency обычно виден «локоть» — точка, где начинается рост.
Stress-test
Цель — найти точку отказа. Жмём, пока не упадёт:
export const options = {
stages: [
{ duration: '1m', target: 100 },
{ duration: '1m', target: 500 },
{ duration: '1m', target: 1000 },
{ duration: '1m', target: 2000 },
{ duration: '1m', target: 4000 },
{ duration: '5m', target: 4000 },
],
thresholds: {
http_req_failed: ['rate<0.1'],
},
};
На stress-test обычно гоняют не на проде. На staging с теми же ресурсами, что прод, или на production-clone. Никогда не на самом проде в рабочее время.
Spike-test
Резкий пик нагрузки — имитация Хабр-эффекта или флешмоба:
export const options = {
stages: [
{ duration: '10s', target: 100 },
{ duration: '10s', target: 1000 }, // резкий пик
{ duration: '30s', target: 1000 },
{ duration: '10s', target: 100 },
{ duration: '1m', target: 100 },
],
};
Здесь цель — посмотреть на восстановление. Часто сервис переживает пик, но не возвращается к нормальной latency после него. Это уже поведение пулов соединений, GC, кэшей — много всего.
Реалистичный сценарий
Один эндпоинт ничего не показывает. Реальный пользователь делает цепочку запросов: логин, открытие списка, открытие конкретной записи, действие. Это пишется как функция:
import http from 'k6/http';
import { check, sleep, group } from 'k6';
const BASE = 'https://api.staging.example.com';
export const options = {
scenarios: {
catalog_browsing: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '1m', target: 0 },
],
},
},
};
export function setup() {
const credentials = JSON.parse(open('./users.json'));
return credentials;
}
export default function (users) {
const user = users[Math.floor(Math.random() * users.length)];
group('login', () => {
const r = http.post(`${BASE}/auth/login`, JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' },
});
check(r, { 'login 200': (resp) => resp.status === 200 });
});
sleep(1);
group('browse catalog', () => {
const list = http.get(`${BASE}/catalog?page=1`);
check(list, { 'catalog 200': (r) => r.status === 200 });
const items = list.json('items');
if (items && items.length > 0) {
const sku = items[0].sku;
const detail = http.get(`${BASE}/catalog/${sku}`);
check(detail, { 'detail 200': (r) => r.status === 200 });
}
});
sleep(2);
}
Файл users.json содержит заранее подготовленные тестовые аккаунты. Один аккаунт — гарантированный bottleneck (один user-id, одна сессия, lock на чём-то), не повторяй эту ошибку.
Какие метрики смотреть
K6 выдаёт десяток метрик. Из них реально использую четыре.
- http_req_duration p95 / p99. Не средняя — она прячет хвост. На p95 видно, как живут «реальные» пользователи.
- http_req_failed (error rate). Доля запросов, которые упали по сети, по таймауту или вернули >=400. Должно быть <1% на load-test.
- http_reqs RPS. Сколько запросов в секунду в среднем. Помогает прикинуть пропускную способность.
- iteration_duration. Сколько занимает один полный сценарий (всю цепочку). Если средний пользователь делает 5 запросов и iteration занимает 5 секунд — каждый запрос ~1 сек, что часто слишком.
Среднее по latency — почти бесполезная метрика. Показывает «среднюю температуру по больнице». 50% запросов могут лежать вблизи 100ms, а 5% — около 5 секунд, средняя получится приемлемой, а часть пользователей в это время бесится.
Куда складывать метрики
K6 умеет писать в Prometheus, InfluxDB, Datadog, New Relic. Самое простое — Prometheus + Grafana:
k6 run \
--out experimental-prometheus-rw=http://prometheus:9090/api/v1/write \
script.js
В Grafana есть готовый дашборд K6 (id 19665), на нём сразу видно RPS, p95, error rate в реальном времени. Без этого приходится смотреть в выводе консоли — на длинных прогонах неудобно.
Что не зашло
- Гонять нагрузку с одной ноутбучной машины. Локальная сеть, лимиты дескрипторов, неровный CPU — результаты ползут. Запускай k6 в облаке (k6 Cloud, GitHub Actions с большим раннером, отдельная VM).
- Тестировать на одних и тех же данных. Кэш сервиса прогревается, тесты показывают идеальную latency. Используй пул разных аккаунтов и параметров запросов, имитируя реальное распределение.
- Стресс-тест на проде в рабочее время. Один раз положили чужой LDAP, потому что наш сервис через него ходил. Только на изолированных стендах.
- Сравнивать прогоны без фиксации окружения. «Стало хуже на 30%» — а железо тестового стенда было перегружено другим релизом. Метрики стенда (CPU, RAM, network) фиксируй вместе с метриками k6.
Чек-лист первой нагрузки
- Поставить k6, написать smoke на 1–2 эндпоинта на 30 секунд.
- Поставить thresholds на error rate и p95 latency.
- Завести данные: 100+ тестовых аккаунтов, разные параметры запросов.
- Описать реалистичный сценарий: цепочка запросов с sleep между ними.
- Прогнать ramp-test до ожидаемой пиковой нагрузки.
- Подключить Prometheus + Grafana для сбора метрик.
- Зафиксировать baseline и гонять regress на каждом значимом релизе.
Нагрузочное — не «прокликивание для красивых цифр». Это инструмент, чтобы понимать, как сервис ведёт себя под нагрузкой, и фиксировать деградации. Без k6 в CI или хотя бы регулярного ручного прогона любая оптимизация перформанса делается на ощупь.