Нагрузка — k6 + smoke
k6 — инструмент нагрузочного тестирования HTTP-, WebSocket- и других сервисов. Сценарии пишутся на JavaScript, запускаются из CLI и могут быть частью CI/CD.
Нагрузочный тест показывает, как система ведёт себя при заданной интенсивности запросов: меняются ли latency и error rate, где возникает деградация и хватает ли ресурсов приложения, базы данных и инфраструктуры.
Тестируйте только собственные или явно разрешённые окружения. До запуска согласуйте профиль нагрузки, длительность, тестовые данные, критерии остановки и ответственных.
Содержание
- Основные понятия k6
- Установка и запуск
- Сценарий k6
- Виды нагрузочных тестов
- Smoke-тесты
- Thresholds и критерии успеха
- Анализ результатов
- Интеграция в CI
- Практические рекомендации
- Чек-лист
- Шпаргалка
Основные понятия k6
| Понятие | Значение |
|---|---|
| VU | Виртуальный пользователь, выполняющий сценарий в цикле |
| Iteration | Одно выполнение функции сценария одним VU |
| Request | HTTP-запрос из сценария |
| Check | Проверка ответа без автоматического завершения теста |
| Threshold | Формальное условие успешности теста |
| Stage | Этап изменения количества VU |
| Scenario | Отдельный профиль выполнения или пользовательский поток |
Встроенные метрики
| Метрика | Назначение |
|---|---|
http_reqs |
Количество HTTP-запросов |
http_req_duration |
Полное время запроса |
http_req_failed |
Доля неуспешных запросов |
http_req_waiting |
Время ожидания ответа от сервера |
http_req_connecting |
Время установки соединения |
http_req_blocked |
Время ожидания доступного соединения |
vus |
Текущее количество VU |
iterations |
Количество итераций |
checks |
Результаты проверок |
Теги
Теги позволяют разделять метрики по endpoint'ам и сценариям. Используйте стабильные имена, особенно если URL содержит динамический идентификатор:
http.get(`${BASE_URL}/api/products`, {
tags: { name: 'GET /api/products' },
});Установка и запуск
Проверка установки:
k6 versionЗапуск сценария:
k6 run script.jsПараметры можно передать через переменные окружения:
BASE_URL=https://staging.example.com k6 run script.jsconst BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';Сохранение результата:
k6 run --out json=results.json script.jsДля сравнения запусков сохраняйте версию приложения, commit, версию k6, профиль нагрузки, окружение, набор данных и версию сценария.
Сценарий k6
Минимальный рабочий пример:
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
export const options = {
vus: 1,
duration: '10s',
};
export default function () {
const response = http.get(`${BASE_URL}/health`, {
timeout: '5s',
tags: { name: 'GET /health' },
});
check(response, {
'status is 200': (res) => res.status === 200,
'body is not empty': (res) => res.body.length > 0,
});
sleep(1);
}Основные элементы:
options— профиль нагрузки и пороги;default— функция, выполняемая каждым VU;check()— проверки статуса, тела и заголовков;sleep()— пауза, моделирующая поведение пользователя;__ENV— переменные окружения;__VUи__ITER— номер VU и текущей итерации.
POST с JSON
const payload = JSON.stringify({
email: `load-${__VU}-${__ITER}@example.test`,
name: 'Load Test User',
});
const response = http.post(`${BASE_URL}/api/users`, payload, {
headers: { 'Content-Type': 'application/json' },
tags: { name: 'POST /api/users' },
});
check(response, {
'status is 201': (res) => res.status === 201,
'response has id': (res) => Boolean(res.json('id')),
});Секреты передавайте через защищённые переменные CI:
const response = http.get(`${BASE_URL}/api/profile`, {
headers: {
Authorization: `Bearer ${__ENV.API_TOKEN}`,
},
});Жизненный цикл
export function setup() {
return { token: __ENV.API_TOKEN };
}
export default function (data) {
// Основная нагрузка. data содержит результат setup().
}
export function teardown(data) {
// Очистка после теста.
}setup() выполняется один раз, затем его результат передаётся в default, а после завершения запускается teardown().
Виды нагрузочных тестов
Load
Проверяет работу при ожидаемой штатной нагрузке:
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '2m', target: 0 },
],
};Плавный разгон, плато и снижение нагрузки позволяют оценить стабильность обычного режима.
Stress
Постепенно повышает нагрузку выше штатного уровня и помогает найти предел системы:
export const options = {
stages: [
{ duration: '2m', target: 50 },
{ duration: '3m', target: 100 },
{ duration: '3m', target: 200 },
{ duration: '2m', target: 0 },
],
};Заранее определите критерий остановки: массовые ошибки, недоступность зависимостей или опасное влияние на окружение.
Spike
Проверяет реакцию на резкий скачок:
export const options = {
stages: [
{ duration: '30s', target: 10 },
{ duration: '10s', target: 300 },
{ duration: '1m', target: 300 },
{ duration: '30s', target: 10 },
{ duration: '1m', target: 0 },
],
};Такой тест показывает поведение autoscaling, очередей, пулов соединений и rate limiting.
Soak
Долго поддерживает стабильную нагрузку:
export const options = {
stages: [
{ duration: '10m', target: 50 },
{ duration: '2h', target: 50 },
{ duration: '10m', target: 0 },
],
};Soak-тест помогает обнаружить утечки памяти, рост latency, исчерпание соединений, накопление файлов и постепенную деградацию.
| Тип | Цель |
|---|---|
| Smoke | Быстрая проверка доступности и критического пути |
| Load | Работа при ожидаемой нагрузке |
| Stress | Поиск предела и точки деградации |
| Spike | Реакция на резкий пик |
| Soak | Проблемы длительной работы |
Количество VU не равно RPS: фактическая скорость зависит от времени ответа, числа запросов в итерации и пауз. Для фиксированной скорости операций используйте arrival-rate executors:
export const options = {
scenarios: {
api_flow: {
executor: 'constant-arrival-rate',
rate: 10,
timeUnit: '1s',
duration: '2m',
preAllocatedVUs: 10,
maxVUs: 50,
},
},
};Smoke-тесты
Smoke-тест — короткая проверка, которая отвечает, можно ли продолжать деплой или дальнейшее тестирование.
Он должен:
- выполняться быстро;
- проверять health endpoint и один-два критических пути;
- проверять статус, формат ответа и важные поля;
- иметь строгие thresholds;
- завершаться ненулевым кодом при нарушении условий.
Пример:
import http from 'k6/http';
import { check } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://localhost:8080';
export const options = {
vus: 1,
iterations: 1,
thresholds: {
checks: ['rate == 1.0'],
http_req_failed: ['rate == 0'],
http_req_duration: ['p(95) < 1000'],
},
};
export default function () {
const health = http.get(`${BASE_URL}/health`, {
tags: { name: 'GET /health' },
});
check(health, {
'health returns 200': (res) => res.status === 200,
});
const products = http.get(`${BASE_URL}/api/products?limit=1`, {
tags: { name: 'GET /api/products' },
});
check(products, {
'products returns 200': (res) => res.status === 200,
'response is JSON': (res) =>
String(res.headers['Content-Type'] || '').includes('application/json'),
});
}Smoke-тесты запускают после деплоя на staging, после production-деплоя, перед нагрузочным тестом и иногда по расписанию.
Healthcheck проверяет состояние процесса или сервиса, а smoke-тест — внешнее поведение API с точки зрения клиента. Они дополняют друг друга.
Thresholds и критерии успеха
check() фиксирует результат проверки, но сам по себе не обязательно делает запуск неуспешным. Для этого используются thresholds:
export const options = {
thresholds: {
http_req_failed: ['rate < 0.01'],
http_req_duration: ['p(95) < 500', 'p(99) < 1000'],
checks: ['rate > 0.99'],
},
};Здесь допускается менее 1% ошибок, p95 должен быть меньше 500 мс, p99 — меньше 1000 мс, а 99% проверок должны пройти.
Перцентили
p(50)— медиана;p(90)— 90% запросов быстрее этого значения;p(95)— 95% запросов быстрее этого значения;p(99)— хвост редких медленных запросов;max— максимальное наблюдавшееся время.
Среднее значение может скрывать медленные запросы, поэтому для SLO обычно важнее p95 и p99.
Threshold для конкретного endpoint'а:
export const options = {
thresholds: {
'http_req_duration{name:GET /health}': ['p(95) < 200'],
'http_req_duration{name:GET /api/products}': ['p(95) < 700'],
'http_req_failed{name:POST /api/orders}': ['rate < 0.005'],
},
};Пороги задавайте на основе SLO, требований продукта и стабильных предыдущих запусков. Слишком строгие пороги вызывают шумные падения, слишком мягкие не защищают производительность.
Анализ результатов
Нужно анализировать не только код завершения k6, но и профиль поведения системы.
| Показатель | Возможная причина отклонения |
|---|---|
http_req_failed |
Ошибки приложения, сети, тайм-ауты, лимиты |
http_req_duration |
Рост общей задержки |
http_req_waiting |
Медленная обработка сервера или upstream |
http_req_connecting |
Сетевые проблемы или нехватка соединений |
http_req_blocked |
Ожидание свободного соединения |
iterations |
Фактическая скорость сценария |
checks |
Корректность ответов и бизнес-проверок |
Высокая latency при нулевых ошибках означает, что система уже может нарушать SLO. Низкая latency при большом количестве ошибок также не является успехом: сервер может быстро возвращать 4xx или 5xx.
Сопоставляйте результаты k6 с:
- HTTP-статусами и телами ошибок;
- RPS, VU и количеством итераций;
- CPU, памятью, swap и дисковым I/O;
- пулом соединений приложения и БД;
- очередями и garbage collection;
- slow queries и блокировками PostgreSQL;
- логами Loki или другим централизованным логированием;
- метриками Prometheus/Grafana.
Пример результата:
http_req_duration: avg=210ms med=160ms p(90)=380ms p(95)=520ms p(99)=1.2s
http_req_failed: 0.40%
checks: 99.70% ✓Среднее 210 мс выглядит приемлемо, но p99 равен 1,2 секунды. Это указывает на хвост медленных запросов.
При stress-тесте полезно построить зависимость нагрузки от p95/p99, ошибок, CPU, памяти, соединений и времени запросов к БД. Точка деградации часто видна как резкий рост latency или error rate.
Сохранение и сравнение
k6 run --out json=results.json load.jsСравнивайте только сопоставимые запуски: одинаковые профиль, данные, окружение, версию k6, прогрев и thresholds. Один запуск не всегда доказывает регрессию — учитывайте естественную вариативность.
Интеграция в CI
Рекомендуемое разделение:
- smoke — на каждый pull request или merge request;
- короткий load — после деплоя на staging;
- stress и soak — вручную или по расписанию;
- production smoke — после деплоя с ограниченной нагрузкой.
GitHub Actions
name: k6 smoke
on:
pull_request:
workflow_dispatch:
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6
uses: grafana/k6-action@v0.3.1
with:
filename: tests/smoke.js
env:
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
API_TOKEN: ${{ secrets.STAGING_API_TOKEN }}При нарушении thresholds k6 возвращает ненулевой код, поэтому job завершается с ошибкой.
GitHub Actions с артефактом
name: k6 load
on:
workflow_dispatch:
jobs:
load:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run k6
run: |
docker run --rm \
-e BASE_URL="$BASE_URL" \
-v "$PWD:/work" \
grafana/k6 run \
--out json=/work/results.json \
/work/tests/load.js
env:
BASE_URL: ${{ secrets.STAGING_BASE_URL }}
- name: Upload result
if: always()
uses: actions/upload-artifact@v4
with:
name: k6-results
path: results.jsonДля длительных тестов используйте выделенный runner или отдельную нагрузочную инфраструктуру: общий CI-runner может стать узким местом и исказить измерения.
GitLab CI
stages:
- smoke
- load
k6_smoke:
stage: smoke
image:
name: grafana/k6:latest
entrypoint: ['']
script:
- k6 run --out json=results.json tests/smoke.js
variables:
BASE_URL: $STAGING_BASE_URL
artifacts:
when: always
paths:
- results.jsonURL, токены и другие секреты храните в защищённых переменных CI. Production job должна иметь отдельные правила, права и, при необходимости, ручное подтверждение.
Профили через переменную
const PROFILE = __ENV.PROFILE || 'smoke';
const profiles = {
smoke: { vus: 1, duration: '10s' },
load: {
stages: [
{ duration: '1m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '1m', target: 0 },
],
},
};
export const options = profiles[PROFILE];PROFILE=smoke BASE_URL=http://localhost:8080 k6 run script.js
PROFILE=load BASE_URL=https://staging.example.com k6 run script.jsНе допускайте, чтобы случайное значение CI запускало stress-тест вместо smoke-теста.
Подготовка данных и безопасность
До запуска определите:
- какие записи можно создавать и изменять;
- как разделять данные разных VU;
- как очищать созданные объекты;
- не используются ли реальные персональные данные;
- не выполняются ли реальные платные операции;
- не попадут ли токены в логи;
- есть ли аварийный способ остановки.
Статические данные можно загружать через SharedArray:
import { SharedArray } from 'k6/data';
const users = new SharedArray('users', () =>
JSON.parse(open('./users.json'))
);
export default function () {
const user = users[(__VU - 1) % users.length];
// Использование user.
}Параллельные VU не должны без необходимости изменять одну запись: это создаёт гонки и блокировки. Используйте уникальные тестовые идентификаторы.
Каждому запросу задавайте разумный timeout:
http.get(`${BASE_URL}/api/resource`, { timeout: '5s' });Практические рекомендации
- Используйте отдельные smoke, load, stress, spike и soak-сценарии.
- Проверяйте статус, структуру ответа и критические бизнес-условия.
- Давайте каждому endpoint стабильный тег
name. - Не храните секреты в репозитории.
- Не смешивайте подготовку данных с измеряемой операцией без необходимости.
- Для реалистичных пользовательских сценариев моделируйте паузы.
- Для фиксированной скорости операций используйте arrival-rate executors.
- Смотрите p95 и p99, а не только average.
- Анализируйте k6 вместе с метриками сервера, БД и логами.
- Сохраняйте результаты как артефакты CI.
- Длительные тесты запускайте отдельно от быстрых проверок.
- Ограничивайте параллельные тестовые запуски.
- Не запускайте стресс-тест в production без специального согласования и защитных ограничений.
Чек-лист
До запуска
- Подтверждены окружение и URL.
- Согласованы интенсивность и длительность.
- Подготовлены безопасные тестовые данные.
- Проверены токены и права.
- Заданы timeouts и thresholds.
- Включён мониторинг приложения, БД и инфраструктуры.
- Есть способ остановить тест.
Во время запуска
- Нагрузка растёт по ожидаемому профилю.
- Фактический RPS соответствует цели.
- Нет неожиданных 4xx/5xx.
- Не исчерпываются CPU, память, диск и соединения.
- Логи не показывают массовых ошибок.
- Тест не влияет недопустимо на соседние сервисы.
После запуска
- Проверены p50, p95 и p99.
- Проверен процент ошибок и результаты checks.
- Ошибки разобраны по endpoint'ам и статусам.
- Результаты сопоставлены с Prometheus/Grafana и логами.
- Сохранены JSON-результаты и конфигурация.
- Зафиксированы версия приложения и сценария.
- Описаны узкие места и выполнен повторный тест после исправлений.
Шпаргалка
# Версия
k6 version
# Smoke
BASE_URL=http://localhost:8080 k6 run tests/smoke.js
# Переопределить VU и длительность
k6 run --vus 10 --duration 30s tests/load.js
# JSON-результат
k6 run --out json=results.json tests/load.js
# Переменные окружения
BASE_URL=https://staging.example.com \
API_TOKEN='***' \
k6 run tests/load.js
# Запуск в Docker
docker run --rm \
-e BASE_URL=http://host.docker.internal:8080 \
-v "$PWD:/work" \
grafana/k6 run /work/tests/smoke.jsИтог
k6 позволяет описывать нагрузку как код, воспроизводить профили и автоматически проверять SLO через thresholds. Smoke-тесты быстро проверяют доступность и критический путь, load-тесты моделируют штатную нагрузку, stress-тесты ищут предел, spike-тесты проверяют скачки, а soak-тесты выявляют проблемы длительной работы.
Для достоверного вывода k6 необходимо использовать вместе с мониторингом Prometheus/Grafana, централизованными логами и мониторингом PostgreSQL. Число запросов в секунду показывает масштаб нагрузки, но только сопоставление latency и ошибок с ресурсами системы помогает найти причину деградации.