Нагрузка — k6 + smoke

k6 — инструмент нагрузочного тестирования HTTP-, WebSocket- и других сервисов. Сценарии пишутся на JavaScript, запускаются из CLI и могут быть частью CI/CD.

Нагрузочный тест показывает, как система ведёт себя при заданной интенсивности запросов: меняются ли latency и error rate, где возникает деградация и хватает ли ресурсов приложения, базы данных и инфраструктуры.

Тестируйте только собственные или явно разрешённые окружения. До запуска согласуйте профиль нагрузки, длительность, тестовые данные, критерии остановки и ответственных.


Содержание


Основные понятия 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.js
const 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);
}

Основные элементы:

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-тест — короткая проверка, которая отвечает, можно ли продолжать деплой или дальнейшее тестирование.

Он должен:

Пример:

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% проверок должны пройти.

Перцентили

Среднее значение может скрывать медленные запросы, поэтому для 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_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

Рекомендуемое разделение:

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.json

URL, токены и другие секреты храните в защищённых переменных 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-теста.


Подготовка данных и безопасность

До запуска определите:

Статические данные можно загружать через 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' });

Практические рекомендации


Чек-лист

До запуска

Во время запуска

После запуска


Шпаргалка

# Версия
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 и ошибок с ресурсами системы помогает найти причину деградации.