Перейти к содержимому
Поиск
Разработка

k6: нагрузочное тестирование API — мастерская и 8 практических опытов

Проверяем API под нагрузкой с k6: восемь настоящих опытов, p95, ошибки, модели пользователей и пороги CI. Мастерская, конструктор test.js и полный стенд для Windows, Linux и Docker Compose.

Александр Некрасов
k6 — проверяем нагрузкой: p95, ошибки и сценарии API

k6 на практике: нагрузка на API, checks, thresholds, p95 и модели пользователей. Восемь настоящих записанных опытов, конструктор теста и исходники для Windows, Linux и Docker Compose.

k6: проверить, как API ведёт себя под нагрузкой

Сервис может отлично отвечать на один запрос разработчика и заметно замедляться, когда пользователи открывают отчёт одновременно. Обычная ручная проверка этого не показывает. Нужен управляемый поток запросов, понятные условия успеха и результат, который можно сохранить и повторить после изменения приложения.

k6 позволяет описать такой опыт JavaScript-кодом: какие запросы выполнять, сколько виртуальных пользователей запускать, как наращивать нагрузку и какие значения считать приемлемыми. Сам сценарий исполняет отдельный runtime k6. Он измеряет запросы и проверки, оценивает пороги и завершает процесс с кодом, который может использовать CI.

В статье есть восемь практических опытов на собственном учебном Node.js API. На странице можно изучить настоящие результаты, изменить критерии оценки и скачать свой test.js. В исходниках находятся API, сценарии, полные записи и Docker Compose. Разберём запуск в Windows, Linux и применение на VPS, не ограничиваясь единственной командой «постучать по адресу».

Где применять: релизы, отчёты, интеграции и очереди

Первый сценарий — проверка регрессии перед релизом. Вы знаете важный маршрут и ожидаемый объём запросов, запускаете одинаковый тест на двух версиях и сравниваете задержку, ошибки и пропущенные итерации. Это полезно после изменения SQL-запроса, кеширования, сериализации или внешней интеграции. Сравнение должно использовать одинаковые данные и сопоставимое окружение.

Второй сценарий — поиск границы, при которой конкретная операция перестаёт укладываться в требования. Например, формирование отчёта может занимать больше времени, чем просмотр карточки. Если смешать всё в одну статистику, быстрые маршруты скроют проблему. Теги и отдельные thresholds помогают оценивать операции по смыслу.

Третий сценарий — проверка поведения при росте и снижении нагрузки: появляются ли ошибки, увеличивается ли очередь, восстанавливается ли задержка. Здесь результат k6 нужно сопоставить с серверными метриками. Сам генератор показывает наблюдаемое поведение запроса, но не знает, виноваты ли база, CPU, лимит соединений или внешний сервис. Для этих причин потребуется наблюдение внутри приложения.

k6, функциональные тесты и мониторинг решают разные задачи

Функциональный тест отвечает на вопрос, работает ли конкретный пользовательский путь. Нагрузочный опыт добавляет управляемую конкурентность и измерения. Мониторинг постоянно собирает показатели уже работающей системы. Эти инструменты дополняют друг друга, поэтому хороший тест не заканчивается зелёной строкой в терминале: у него есть контекст сервера и понятная цель.

В наших примерах используется HTTP API k6. Запрос к HTML-странице таким способом не запускает весь JavaScript интерфейса и не проверяет расположение кнопок. У k6 есть отдельные возможности браузерного тестирования, но в данной мастерской они не используются. Для проверки реальной страницы и пользовательских действий посмотрите наш материал о Playwright.

А для объяснения серверных показателей откройте мастерскую Prometheus. Во время теста полезно видеть загрузку CPU, память, ошибки, задержки и размер очереди. Если генератор сам упёрся в лимит своей машины, это тоже необходимо заметить, иначе вывод о мощности сервиса окажется неверным.

Мини-мастерская: записи настоящих запусков и конструктор теста

Выберите опыт и сначала посмотрите его исходный результат: число запросов и итераций, p95, исторический код завершения и исходные пороги. Записи получены настоящим k6 на собственном локальном API, дата, платформа и версия показаны в интерфейсе. Столбики min, median, p95 и max сравнивают агрегаты одного прогона; это не временная шкала нагрузки.

Затем измените критерии: допустимую задержку, долю HTTP-ошибок и процент успешных checks. Мастерская пересчитает оценку уже сохранённых данных. Исторический exit code останется прежним: он относится к исходному тесту, а не к новым значениям полей. Такая разница особенно хорошо видна в опыте checks, где проверка провалена, но процесс завершился успешно.

Ниже можно выбрать модель нагрузки и скачать новый test.js. Конструктор не обращается к внешнему адресу и не запускает запросы с сайта. Полученный сценарий выполняется локально против учебного API из архива. Для новой нагрузки нужен новый настоящий запуск: изменение количества VU в форме не преобразует старые измерения в результат другого теста.

Что находится в стенде и какие версии закреплены

Проект использует k6 2.3.0 и Node.js 24 LTS для учебного API и сборки интерфейса. Бинарник k6 устанавливается отдельно, а helper проверяет его SHA-256 по официальному файлу контрольных сумм. В архиве нет случайного исполняемого файла или зависимости от глобального npm-пакета с похожим названием. Версия и происхождение записываются после установки.

Скрипт scripts/run.js содержит восемь режимов CASE. API отвечает на health, формирует быстрый или медленный отчёт, возвращает управляемые ошибки и поддерживает короткую цепочку создания заявки. Токен специально учебный, а записи существуют только в памяти процесса. Это маленький стенд для изучения механизма, а не готовая система авторизации или хранения заказов.

Официальные изменения версии можно посмотреть в релизе k6 2.3.0. Для других платформ используйте инструкцию установки k6. Наш загрузчик поддерживает Windows и Linux x64; ARM и остальные архитектуры требуют соответствующего официального дистрибутива.

Windows: сначала открыть готовую мастерскую

Распакуйте проект, откройте PowerShell в его каталоге и проверьте Node.js. Команда npm ci устанавливает версии из lock-файла, build собирает локальный интерфейс, test запускает проверки. npm start поднимает статический сервер на loopback и выводит адрес. По умолчанию это http://127.0.0.1:8300/. Браузер открывает сохранённую запись, а не запускает нагрузочный тест.

Готовая папка public уже присутствует в архиве. Если вам нужен только просмотр, её можно обслужить другим обычным локальным HTTP-сервером. File:// не подходит: интерфейс получает JSON через fetch. Сервер, запущенный в текущем терминале, останавливается Ctrl+C. Если порт занят, используйте свободный порт через переменную PORT перед npm start.

Этот первый шаг позволяет разобраться с критериями и подготовить сценарий без установки k6. Затем переходите к реальному запуску ниже. Не путайте две роли Node.js: он обслуживает учебный API и собирает сайт, но не исполняет k6/http. Команда node scripts/run.js не является заменой k6 run.

node --version
npm.cmd ci --ignore-scripts
npm.cmd run build
npm.cmd test
npm.cmd start

Windows: настоящий k6 и свой локальный API

Выполните setup:k6: helper загрузит официальный архив нужной версии, сверит контрольную сумму и распакует бинарник в tools/bin. После этого откройте два терминала. В первом запустите npm.cmd run api. Во втором выполните k6 с режимом smoke и отдельным именем summary. Учебный API слушает только 127.0.0.1:8320.

Посмотрите LASTEXITCODE сразу после процесса. Для smoke ожидается успех, а для budget — намеренный провал критериев. Если запуск не состоялся из-за занятого порта или отсутствия бинарника, это другая причина, которую нельзя выдавать за результат измерения. Подробности выводятся в терминал и сохранённый артефакт.

Для проверки всего пути есть npm.cmd run verify: команда сама поднимает API, выполняет настоящий smoke и завершает свой сервер. Перед ней остановите вручную запущенный API, чтобы порт был свободен. Capture также запускает свой стенд, но обновляет записи восьми опытов; выполняйте его осознанно, когда хотите получить новый набор результатов.

npm.cmd run setup:k6
# Терминал 1:
npm.cmd run api
# Терминал 2:
& .\tools\bin\k6-v2.3.0-windows-amd64\k6.exe run -e CASE=smoke -e SUMMARY=artifacts/my-smoke.json scripts/run.js
$LASTEXITCODE

Linux: установка, проверка и отдельные результаты

На Linux логика такая же: Node.js запускает учебный API, отдельный бинарник k6 — сценарий. После установки зависимостей helper скачает Linux x64-архив и проверит его контрольную сумму. Для другой архитектуры установите подходящий официальный пакет и используйте его путь. Не копируйте Windows exe на сервер в надежде, что Compose или Node его заменят.

При ручном запуске откройте API в одном терминале, а k6 в другом. Выбирайте отдельное имя SUMMARY для своего опыта. Это сохраняет исходные учебные записи и позволяет сравнить два результата. В Linux код завершения находится в специальном параметре оболочки; сохраните его сразу, пока следующая команда не заменила значение.

Для первых опытов достаточно loopback. Когда переносите тест на отдельный VPS-генератор, меняются сеть, ресурсы и границы доступа. Учебный скрипт по умолчанию допускает только известный локальный API: адаптируйте адрес и эту проверку явно под собственный стенд. Ниже разберём, что ещё нужно учитывать при таком переносе.

npm ci --ignore-scripts
npm run setup:k6
npm run build
npm test
npm run verify
# При отдельно запущенном npm run api:
./tools/bin/k6-v2.3.0-linux-amd64/k6 run -e CASE=smoke -e SUMMARY=artifacts/my-smoke.json scripts/run.js
echo $?

Docker Compose: тот же опыт в Linux-контейнерах

Compose связывает два сервиса: собственный Node.js API и официальный образ k6. Генератор обращается к api:8320 внутри сети, а на хосте API опубликован только на loopback. Скрипты монтируются для чтения, artifacts — для сохранения summary. Так сценарий можно воспроизвести и в Windows с Docker Desktop, и на Linux.

Сначала запустите API, затем одноразовый контейнер k6. Режим CASE можно передать переменной окружения. После опыта сохраните результат и остановите стенд. Повторный Compose-запуск записывает один и тот же compose-summary.json, поэтому для длительного сравнения переименовывайте артефакты осмысленно или задавайте отдельную структуру в своём pipeline.

В Linux учитывайте владельца файлов на смонтированном каталоге. Учебный контейнер пишет от root; для рабочего CI используйте подходящий UID/GID или отдельный шаг передачи артефакта. Не исправляйте это правами 777 на весь проект. Подробности сетей, volumes и жизненного цикла сервисов разобраны в нашей статье Docker Compose для PHP и Node.js.

docker compose up -d api
docker compose --profile test run --rm k6
# Linux: другой учебный опыт
CASE=tags docker compose --profile test run --rm k6
docker compose down

Опыт 1. Smoke: маленький тест перед большой нагрузкой

Smoke начинается с одного виртуального пользователя и трёх итераций. Мы проверяем, что API доступен, возвращает ожидаемый статус и содержит нужное значение JSON. Такой короткий опыт не измеряет производительность под серьёзной нагрузкой, зато быстро выявляет неверный адрес, неподготовленные данные и сломанный контракт ответа.

Если smoke не проходит, увеличение VU обычно только добавляет шум. Сначала убедитесь, что запрос корректен сам по себе: заголовки, метод, токен, тело и ожидаемая структура ответа. Потом добавляйте конкурентность. Этот порядок экономит время, потому что ошибка сценария и перегрузка сервиса требуют разных действий.

В мастерской откройте первый опыт и сравните число итераций с HTTP-запросами. Здесь они совпадают, потому что одна итерация делает один запрос. В цепочке flow одна итерация выполняет три запроса, а в budget — два. Не переносите равенство из простого примера на любой пользовательский путь.

import http from 'k6/http';
import { check } from 'k6';
export const options = {
  scenarios: { smoke: { executor: 'shared-iterations', vus: 1, iterations: 3 } },
  thresholds: { checks: ['rate==1'] }
};
export default function () {
  const r = http.get('http://127.0.0.1:8320/health');
  check(r, { 'healthy JSON': x => x.status === 200 && x.json('ok') === true });
}

Опыт 2. Check не равен неуспешному завершению

Во втором опыте сервер отвечает правильно, но одно условие проверки намеренно неверно: сценарий ищет отсутствующее поле. Половина checks проходит, половина — нет. При этом thresholds отсутствуют, поэтому процесс завершается кодом 0. Это один из самых полезных первых опытов: он показывает, почему зелёный шаг CI не всегда означает выполненные бизнес-условия.

Check фиксирует логическое утверждение о результате. Threshold задаёт критерий приемки агрегированной метрики. Если вы хотите, чтобы любая неудачная проверка завершала тест неуспешно, задайте соответствующий threshold для checks. При этом не обязательно прерывать опыт немедленно: можно собрать весь результат и оценить его в конце.

В интерфейсе измените минимальный процент успешных checks. Новая оценка может стать положительной, но исторический exit code останется 0. Запись не переписывается. Поведение checks отдельно описано в официальной документации; критерии завершения — в разделе thresholds.

export const options = {
  thresholds: {
    checks: ['rate==1']
  }
};
// Без этого условия failed check сам по себе не делает процесс failed.

Опыт 3. P95, ошибки и бюджет задержки

В режиме budget каждая итерация делает медленный запрос и запрос к профилю ошибок. Медленный маршрут специально задерживает ответ примерно на 240 мс. Ошибочный профиль возвращает 503 для каждого третьего такого обращения. Условия исходного теста строже поведения стенда, поэтому провал порогов здесь ожидаем и полезен для разбора.

Посмотрите отдельно p95, долю HTTP-ошибок и checks. Это разные вопросы: насколько долго отвечал сервис, сколько ответов не считались ожидаемыми и какие утверждения сценария не выполнились. Увеличение порога задержки не исправляет ошибки статуса, а разрешение ошибок не делает ответ быстрее. Конструктор позволяет увидеть это без повторного запуска нагрузки.

Число 150 мс в учебном сценарии выбрано для демонстрации. Оно не является универсальным нормативом для любого API. В своём проекте определяйте бюджет по смыслу операции и пользовательскому пути, учитывайте сеть и зависимости. Новые критерии нужно применять к репрезентативному запуску, а не выдавать несколько секунд локальной записи за подтверждение SLO.

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<150'],
    http_req_failed: ['rate<0.01'],
    checks: ['rate==1']
  }
};

Как читать p95, median и max без неверных выводов

Median описывает середину выборки, p95 — значение на уровне девяносто пятого процентиля, max — наибольшую наблюдавшуюся длительность. Они отвечают на разные вопросы. Быстрая медиана может соседствовать с неприятным медленным хвостом, а единичный максимум не объясняет, насколько часто проблема повторяется.

В нашей мастерской показаны агрегаты итогового summary. Из них нельзя восстановить точную последовательность запросов или график задержки по времени. Также нельзя получить корректный общий p95, просто усреднив p95 двух запусков. Если нужны объединённые распределения или временной ряд, сохраняйте подходящие исходные метрики и используйте соответствующий способ агрегации.

Метрика http_req_duration включает отправку запроса, ожидание и получение ответа, но не все первоначальные этапы соединения. Отдельно существуют показатели блокировки, подключения и TLS. Определения приведены в справочнике встроенных метрик. Маленькая выборка в smoke даёт только ограниченное наблюдение; для вывода о стабильности нужны достаточный объём и повторяемый сценарий.

Опыт 4. Плавный рост и снижение числа пользователей

Ramping VUs меняет число виртуальных пользователей по этапам. В учебном опыте сначала происходит небольшой рост, затем короткое плато и снижение. Такой профиль помогает понять устройство сценария и поведение остановки. Для реального исследования длительности этапов выбираются по времени прогрева, характеру операции и необходимому объёму измерений.

Во время плато полезно смотреть не только клиентскую задержку, но и серверные показатели. Например, CPU может оставаться умеренным, а очередь обращений к базе — расти. После снижения нагрузки проверьте восстановление: продолжающийся рост памяти или невосстановившаяся очередь требуют отдельного объяснения. Один итоговый summary не показывает всю эту динамику.

Параметр gracefulStop даёт итерациям время завершиться после окончания запланированной нагрузки. Если операция длится дольше, часть работы может быть прервана. Поэтому ожидаемое число итераций зависит от конфигурации и длительности сценария. Начинайте с небольшой нагрузки и понятного предела, затем увеличивайте её шагами, наблюдая обе стороны.

export const options = {
  scenarios: { main: {
    executor: 'ramping-vus', startVUs: 1,
    stages: [
      { duration: '2s', target: 3 },
      { duration: '3s', target: 3 },
      { duration: '2s', target: 0 }
    ],
    gracefulStop: '2s'
  } }
};

Опыт 5. Постоянные VU: скорость зависит от итерации

Constant VUs удерживает заданное число виртуальных пользователей. Каждый последовательно выполняет итерации своего кода. Если ответ становится медленнее, пользователь дольше ждёт и успевает начать меньше новых итераций. Поэтому два VU не означают два запроса в секунду, а десять VU не гарантируют заданный RPS.

В нашем опыте одна итерация содержит запрос и паузу 0,1 секунды. Измеренная интенсивность зависит от суммы этих затрат и небольших накладных расходов. Сравните число запросов с длительностью прогона в настоящем summary, затем измените параметры конструктора и прочитайте сгенерированный код. Новый результат появится только после выполнения нового файла.

Эта модель полезна, когда вы хотите описать ограниченное число одновременно активных пользовательских циклов. При этом пауза должна иметь смысл: например, время между действиями. Не подбирайте sleep только ради красивого итогового RPS, если предметная задача требует иной модели поступления запросов. Для заданного темпа начала итераций существует следующий executor.

export const options = {
  scenarios: { main: {
    executor: 'constant-vus', vus: 2, duration: '4s'
  } }
};
// В default: запрос, затем sleep(0.1).

Опыт 6. Arrival rate: частота начала итераций

Constant arrival rate планирует новые итерации независимо от того, как быстро завершились предыдущие. В учебном режиме задаётся восемь стартов в секунду, а заранее выделенные VU выполняют работу. Если запросы замедляются, для поддержания темпа потребуется больше одновременно занятых исполнителей, пока не достигнут установленный предел.

Здесь одна итерация делает один запрос, поэтому сравнивать частоту с запросами удобно. В другом сценарии итерация может содержать вход, создание и чтение объекта: тогда заданный arrival rate относится к цепочкам, а не к каждому HTTP-вызову. Смотрите реальный код и метрику http_reqs, а не угадывайте нагрузку по одному параметру.

Конструктор не добавляет sleep для искусственного регулирования arrival rate: расписанием управляет executor. Внутри настоящего пользовательского пути ожидание может быть частью бизнес-сценария, но это отдельное решение. Параметры и ограничения описаны в документации constant-arrival-rate.

export const options = {
  scenarios: { main: {
    executor: 'constant-arrival-rate', rate: 8, timeUnit: '1s',
    duration: '4s', preAllocatedVUs: 2, maxVUs: 4
  } }
};

Dropped iterations: нагрузка могла не состояться

Если executor не смог начать запланированную итерацию, это отдельное событие. Оно не равнозначно ответу сервера с 500: запрос мог вообще не быть отправлен. Причиной бывает нехватка доступных VU или слишком медленная обработка, из-за которой исполнители остаются занятыми. Также необходимо проверять ресурсы самого генератора.

Поэтому отсутствие HTTP-ошибок ещё не доказывает, что нужная нагрузка действительно была приложена. Сопоставьте заданный темп, фактические итерации, запросы и dropped_iterations. Если метрика отсутствует в сохранённом summary, интерфейс показывает «нет метрики», а не придумывает нулевое измерение. Такое различие важно при автоматическом анализе артефактов разных версий и конфигураций.

Не увеличивайте maxVUs бесконечно, пытаясь спрятать проблему. Сначала проверьте длительность итерации и ограничения CPU, памяти, соединений и сети. Затем осмысленно выберите запас исполнителей или измените сценарий. Разбор причин есть в официальной документации dropped iterations.

Опыт 7. Теги маршрутов и отдельные пороги

В одном тесте часто соседствуют быстрые служебные запросы и тяжёлые бизнес-операции. Общая статистика полезна, но может скрыть локальную проблему. В опыте tags запросы health и report получают постоянные метки маршрутов. Для каждого задан отдельный порог, поэтому медленный отчёт виден как самостоятельный неуспех.

Откройте исходные thresholds в мастерской и сравните результаты по route. Так можно понять, какой именно критерий не прошёл. На реальном проекте теги помогают разделять группы операций, сценарии и версии окружения. Используйте небольшой устойчивый набор значений, чтобы отчёт оставался понятным и не разрастался из-за случайных идентификаторов.

Не помещайте в tags токены, почту пользователя и каждый уникальный номер заказа. Для динамического маршрута обычно нужна логическая группа вроде orders, а не бесконечный набор адресов с идентификаторами. Назначение тегов и групп описано в документации k6. В этом примере имена route фиксированы и не содержат личных данных.

http.get(BASE + '/health', { tags: { route: 'health' } });
http.get(BASE + '/report?profile=slow', { tags: { route: 'report' } });

// В options.thresholds:
// 'http_req_duration{route:health}': ['p(95)<100']
// 'http_req_duration{route:report}': ['p(95)<100']

Опыт 8. Вход, создание и чтение заявки

Пользовательский путь редко состоит из одного изолированного запроса. В режиме flow сценарий получает учебный токен, создаёт заявку и читает её по возвращённому id. Проверки подтверждают статус и данные на каждом шаге. Это показывает, как результат одного HTTP-вызова становится входом следующего.

В мастерской токен постоянный и явно обозначен как учебный. Он не защищает production-данные и не является примером полноценной аутентификации. Заказы хранятся в памяти, количество ограничено, при остановке API они исчезают. Такой механизм позволяет повторять опыт без внешней базы, отправки писем и изменения настоящих заказов.

При переносе сценария на свой сервис заранее решите, как создавать и очищать тестовые данные, откуда брать ограниченные учётные записи и какие операции допустимо повторять. Не смешивайте проверку производительности с необратимыми бизнес-действиями. Важно проверить не только 201, но и содержимое созданного объекта: быстрый ответ с неправильными данными не считается полезным успехом.

const login = http.post(BASE + '/login');
const token = login.json('token');
const params = {
  headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + token },
  tags: { route: 'orders' }
};
const created = http.post(BASE + '/orders', JSON.stringify({ title: 'Учебная заявка' }), params);
const got = http.get(BASE + '/orders/' + created.json('id'), params);
check(got, { 'saved title': r => r.status === 200 && r.json('title') === 'Учебная заявка' });

Конструктор test.js: что переносится в новый запуск

Конструктор объединяет условия приёмки и выбранную модель нагрузки. В constant VUs он создаёт нужное количество пользователей и короткую паузу между запросами. В arrival rate задаёт частоту начала итераций и ограниченный пул VU. Адрес остаётся известным локальным API, чтобы скачанный файл можно было воспроизвести в этом же проекте.

Поля проверяются до скачивания. Не принимаются бесконечные значения, неверная модель, дробное число исполнителей и выход за учебные границы длительности и нагрузки. Если настройка неверна, интерфейс показывает объяснение и блокирует экспорт кода. Это защита от случайной ошибки ввода, а не доказательство безопасности любой дальнейшей адаптации сценария.

Порог HTTP-ошибок задаётся как строгое «меньше», поэтому ноль в этом поле не подходит: rate меньше нуля невозможно. Для требования отсутствия ошибок в собственном коде используйте осмысленное условие равенства либо допустимый положительный предел. Успешные checks в конструкторе оцениваются включительно: не менее выбранного процента.

// Пример критериев, которые создаёт конструктор:
export const options = {
  thresholds: {
    http_req_duration: ['p(95)<150'],
    http_req_failed: ['rate<0.01'],
    checks: ['rate>=1']
  }
};

Summary и артефакты: сохранить то, что действительно измерено

HandleSummary получает итоговые агрегаты после выполнения теста. Наш скрипт записывает этот объект в JSON и короткое сообщение в stdout. В artifacts также сохранён текстовый вывод каждого опыта, а recording.json объединяет результаты с версией, платформой, датой и контрольной суммой выполненного сценария. Это позволяет связать цифры с конкретным кодом.

Скачанный из мастерской summary содержит настоящие данные выбранного исходного запуска. Изменение критериев в интерфейсе не переписывает файл и не подменяет исторические thresholds. Для нового теста используйте отдельное имя результата. Один и тот же путь в handleSummary при следующем запуске будет перезаписан — это нужно учитывать в локальной работе и CI.

Итоговый JSON удобен для небольшой проверки, но он не заменяет поток метрик длительного испытания. Для графиков по времени и анализа отдельных фаз выберите подходящий output. Механизм handleSummary и структура результата описаны в официальном руководстве custom summary.

export function handleSummary(data) {
  return {
    'artifacts/my-summary.json': JSON.stringify(data, null, 2),
    stdout: 'Summary saved\n'
  };
}

Ниже — фактическое число HTTP-запросов в приложенной записи. Дата: 2026-11-10T07:04:21.373Z. Короткая длительность выбрана для учебной воспроизводимости, поэтому эти числа не описывают ёмкость вашего сервера.

Записанный опыт HTTP-запросы Exit code
smoke 3 0
checks 3 0
budget 44 99
ramp 131 0
vus 66 0
arrival 33 0
tags 10 99
flow 12 0

CI: сохранить код завершения и не потерять отчёт

В автоматическом pipeline нужен явный критерий результата. Настройте thresholds, запускайте тест против подготовленного окружения и сохраняйте summary даже при провале. Если после k6 выполняется команда публикации артефакта, она не должна случайно заменить исходный код завершения своим успехом. Сохраните код сразу, затем верните его в конце шага.

Разделяйте проблемы инфраструктуры и неуспех критерия. Недоступный API, синтаксическая ошибка сценария и превышение p95 требуют разных сообщений. В наших записанных опытах budget и tags завершились кодом 99 из-за намеренно проваленных thresholds. Checks завершился кодом 0, потому что порогов не было. Не обобщайте один номер на все будущие ошибки инструмента.

Для повторяемости фиксируйте версию приложения, сценария, тестовых данных и окружения. Периодический тест на случайном общем сервере без контроля фоновой нагрузки может давать шум, который трудно интерпретировать. Лучше небольшой воспроизводимый smoke на каждый релиз и отдельный согласованный нагрузочный прогон с наблюдением ресурсов.

set +e
./tools/bin/k6-v2.3.0-linux-amd64/k6 run -e CASE=smoke -e SUMMARY=artifacts/ci-summary.json scripts/run.js
test_exit=$?
set -e
# Сохраните JSON средствами своей CI-системы, в том числе при failure.
exit "$test_exit"

Практика Linux/VPS: генератор и сервис должны быть наблюдаемы

На VPS сначала определите, что именно проверяется: весь путь через reverse proxy, конкретный внутренний API или операция вместе с базой и внешним сервисом. Эти варианты имеют разные задержки и пределы. Для серьёзного измерения отдельный генератор помогает не смешивать CPU и сеть клиента с ресурсами исследуемого приложения.

Наблюдайте обе машины. На стороне сервиса важны память, CPU, пул соединений, очередь и зависимые компоненты. На стороне генератора — способность поддерживать заданный темп, доступные файловые дескрипторы, сеть и ограничения контейнера. Если процесс находится под CPU quota, число ядер хоста само по себе не описывает доступный ресурс.

Учебный systemd unit в архиве запускает только наш API на loopback с отдельным пользователем и без записи на диск. Путь Node и каталог проекта необходимо заменить своими. Постоянный сервис нужен для воспроизводимого стенда, но сам k6 обычно запускается как ограниченная по времени команда или CI job. Для доступа извне настройте свой защищённый путь, а не открывайте учебный токен как production-аутентификацию.

sudo systemctl daemon-reload
sudo systemctl start k6-training-api
sudo systemctl status k6-training-api --no-pager
curl -fsS http://127.0.0.1:8320/health
# После завершения отдельного учебного стенда:
sudo systemctl stop k6-training-api

Как перенести опыт на свой API

Начните с одного разрешённого маршрута и малого smoke. Подготовьте отдельные тестовые данные и учётную запись с необходимыми правами. Проверьте корректность запроса обычным способом, затем перенесите его в k6 и добавьте проверку содержимого. Только после этого выбирайте модель нагрузки и постепенно увеличивайте объём.

В нашем scripts/run.js адрес BASE_URL дополнительно ограничен известным локальным стендом. Для собственного окружения измените эту проверку явно и храните адрес в конфигурации проекта. Не делайте автоматическую нагрузку на произвольный URL из публичного поля формы. Публичная мастерская намеренно только генерирует файл и разбирает записи.

Секреты передавайте средствами окружения или CI secret storage, не включайте их в репозиторий, tags и опубликованный отчёт. Для операций создания заранее определите очистку и идемпотентность, чтобы повторный прогон не умножал рабочие записи. Согласованный объём и наблюдение за сервисом делают результат полезным и позволяют вовремя остановить опыт, если его цель уже достигнута.

// В собственном сценарии адрес задаётся окружением:
const BASE = __ENV.BASE_URL;
if (!BASE) throw new Error('BASE_URL is required');
// Добавьте allowlist своих тестовых окружений и подходящий контракт запроса.
// Учебный scripts/run.js уже ограничен localhost/api:8320.

Исходники, проверки и границы результатов

Полный проект содержит сценарии, API, генератор интерфейса, настоящие JSON-записи и текстовые логи, Compose и команды установки. Source archive не включает бинарник k6: helper загружает официальный файл и проверяет контрольную сумму. Готовая статическая мастерская работает независимо от запущенного генератора и не требует публичного Node-сервера.

Проверки охватывают границы конструктора, отсутствующие метрики, строгие и включительные сравнения, HTTP-статусы учебного API, формат записи и привязку к сценарию. Отдельные DOM-тесты проверяют смену критериев, повтор после ошибки и защиту от устаревшей загрузки. Это дополняет ручную проверку интерфейса, но не превращает короткий опыт в нагрузочное испытание реального production.

При изменении кода сначала запустите тесты, затем намеренно переснимите запись и изучите результат. Не обновляйте эталон только ради зелёного отчёта. Полезный итог этой статьи — умение сформулировать проверяемое требование, собрать понятный сценарий и связать его результат с конкретным окружением. Дальше расширяйте именно свой пользовательский путь и наблюдайте причины изменений.

npm test
npm run verify
# Только для намеренного обновления всех записей, при свободном8320:
npm run capture
npm run build