lenec ru

← все посты

Нагрузочное тестирование REST API на k6: первый рабочий сценарий

15K

Нагрузочное тестирование 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.

Чек-лист первой нагрузки

  1. Поставить k6, написать smoke на 1–2 эндпоинта на 30 секунд.
  2. Поставить thresholds на error rate и p95 latency.
  3. Завести данные: 100+ тестовых аккаунтов, разные параметры запросов.
  4. Описать реалистичный сценарий: цепочка запросов с sleep между ними.
  5. Прогнать ramp-test до ожидаемой пиковой нагрузки.
  6. Подключить Prometheus + Grafana для сбора метрик.
  7. Зафиксировать baseline и гонять regress на каждом значимом релизе.

Нагрузочное — не «прокликивание для красивых цифр». Это инструмент, чтобы понимать, как сервис ведёт себя под нагрузкой, и фиксировать деградации. Без k6 в CI или хотя бы регулярного ручного прогона любая оптимизация перформанса делается на ощупь.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.