diff --git a/docs/k6-report-2026-05-28.md b/docs/k6-report-2026-05-28.md new file mode 100644 index 0000000..a24c7a6 --- /dev/null +++ b/docs/k6-report-2026-05-28.md @@ -0,0 +1,120 @@ +# Отчет по нагрузочному тестированию k6 + +Дата отчета: 2026-05-28 + +## Объект тестирования + +- Стенд: `https://universe.zetcraft.ru` +- Скрипт: [`frontend/scripts/loadtest-endpoints.js`](../frontend/scripts/loadtest-endpoints.js) +- Endpoint'ы: + - `GET /api/v1/courses` + - `GET /api/v1/lectures` + - `GET /api/v1/users/me/stats` + +## Профиль нагрузки + +Тест запускался в 3 параллельных сценариях: + +- `courses_list` +- `lectures_list` +- `user_stats` + +Для каждого сценария использовалось `30 VU`, итого максимум `90 VU`. +Длительность активной нагрузки каждого сценария: `15s`. +С учетом `gracefulStop` максимальная длительность выполнения составила `45s`. + +## Оборудование и сеть + +Тест запускался с машины со следующей конфигурацией: + +- CPU: AMD Ryzen 7 8845HS, `10` потоков использовалось для нагрузки. +- RAM: DDR5 5600, `10 GB` доступно. +- Накопитель: NVMe SSD. +- Сеть: `1 Gbit/s`. + +## Критерии прохождения + +- `checks: rate > 0.95` +- `http_req_duration: p(95) < 1500ms` +- `http_req_failed: rate < 0.01` + +## Прогон 1: без паузы между итерациями + +Команда запуска: + +```bash +BASE_URL="https://universe.zetcraft.ru" VUS=30 DURATION="15s" PAUSE_SECONDS=0 k6 run ./frontend/scripts/loadtest-endpoints.js +``` + +### Итоги + +| Метрика | Значение | +| --- | ---: | +| Статус threshold'ов | пройдено | +| Успешность checks | 100.00% | +| Ошибки HTTP | 0.00% | +| Всего HTTP-запросов | 3508 | +| RPS | 77.95 req/s | +| `http_req_duration` avg | 769.41ms | +| `http_req_duration` med | 38.67ms | +| `http_req_duration` p(90) | 66.61ms | +| `http_req_duration` p(95) | 93.63ms | +| `http_req_duration` max | 36.14s | +| Всего итераций | 3508 | +| Прерванные итерации | 19 | +| Получено данных | 47 MB | +| Отправлено данных | 2.1 MB | + +Проверки: + +- `status is 200`: успешно. +- `body is not empty`: успешно. + +## Прогон 2: пауза 1 секунда между итерациями + +Команда запуска: + +```bash +BASE_URL="https://universe.zetcraft.ru" VUS=30 DURATION="15s" PAUSE_SECONDS=1 k6 run ./frontend/scripts/loadtest-endpoints.js +``` + +### Итоги + +| Метрика | Значение | +| --- | ---: | +| Статус threshold'ов | пройдено | +| Успешность checks | 100.00% | +| Ошибки HTTP | 0.00% | +| Всего HTTP-запросов | 895 | +| RPS | 19.89 req/s | +| `http_req_duration` avg | 336.11ms | +| `http_req_duration` med | 11.77ms | +| `http_req_duration` p(90) | 32.01ms | +| `http_req_duration` p(95) | 42.19ms | +| `http_req_duration` max | 35.9s | +| Всего итераций | 895 | +| Прерванные итерации | 43 | +| Получено данных | 12 MB | +| Отправлено данных | 675 kB | + +Проверки: + +- `status is 200`: успешно. +- `body is not empty`: успешно. + +## Сравнение прогонов + +| Параметр | Без паузы | Пауза 1s | +| --- | ---: | ---: | +| HTTP-запросов | 3508 | 895 | +| RPS | 77.95 req/s | 19.89 req/s | +| Ошибки HTTP | 0.00% | 0.00% | +| Checks | 100.00% | 100.00% | +| p(95) | 93.63ms | 42.19ms | +| Максимальная задержка | 36.14s | 35.9s | + +## Вывод + +Оба прогона успешно прошли заданные threshold'ы: ошибок HTTP не зафиксировано, все проверки ответов успешны, `p(95)` существенно ниже порога `1500ms`. + +При запуске без паузы стенд обработал около `77.95 req/s`, при паузе `1s` - около `19.89 req/s`. Во всех прогонах наблюдались единичные длинные запросы до `35-36s`, при этом они не повлияли на прохождение p95-порога. Это стоит учитывать при дальнейшем анализе хвостовых задержек. diff --git a/docs/load-testing-k6.md b/docs/load-testing-k6.md new file mode 100644 index 0000000..bbd5bb5 --- /dev/null +++ b/docs/load-testing-k6.md @@ -0,0 +1,94 @@ +# Базовый нагрузочный тест (k6) для 3 крупных GET endpoint'ов + +## Цель теста + +Проверить, что при небольшой параллельной нагрузке API: + +- отвечает без ошибок; +- сохраняет приемлемую задержку на «тяжелых» чтениях; +- не падает на endpoint пользовательской статистики. + +Тест рассчитан на новичка: один скрипт, простые пороги, быстрый запуск. + +## Какие endpoint используются + +В тест включены: + +1. `GET /api/v1/courses` — крупный список данных. +2. `GET /api/v1/lectures` — крупный список данных. +3. `GET /api/v1/users/me/stats` — endpoint с информацией о пользователе. + +## Файл теста + +- [loadtest-endpoints.js](../frontend/scripts/loadtest-endpoints.js) + +## Предусловия перед запуском + +1. Запущен API (локально или на тестовом стенде). +2. Если endpoint'ы требуют авторизацию — есть валидный JWT токен. +3. Установлен k6. + +## Запуск + +Без параметров (локальный API по умолчанию `http://localhost:5019`): + +```bash +k6 run ./frontend/scripts/loadtest-endpoints.js +``` + +С параметрами окружения: + +```bash +export TOKEN="" +BASE_URL="http://localhost:5019" VUS=15 DURATION="2m" PAUSE_SECONDS=0 k6 run ./frontend/scripts/loadtest-endpoints.js +``` + +## Что именно делает тест + +Скрипт запускает **3 параллельных сценария**: + +- `courses_list` +- `lectures_list` +- `user_stats` + +Параметры каждого сценария: + +- executor: `constant-vus` +- нагрузка: `10 VU` +- длительность: `2m` +- пауза между итерациями: `sleep(0.5)` + +Итого базовый запуск создает до `30 VU` одновременно: по `10 VU` на каждый из 3 сценариев. + +Переменные окружения: + +- `VUS` — количество VU на каждый сценарий, по умолчанию `10`. +- `DURATION` — длительность каждого сценария, по умолчанию `2m`. +- `PAUSE_SECONDS` — пауза между итерациями, по умолчанию `0.5`. + +На каждом запросе проверяется: + +- статус ответа `200`; +- тело ответа не пустое. + +## Пороговые значения (pass/fail) + +- `http_req_failed: rate < 0.01` — ошибок менее 1%. +- `http_req_duration: p(95) < 1500` — 95% запросов быстрее 1.5с. +- `checks: rate > 0.95` — минимум 95% проверок успешны. + +Если любой threshold не выполнен, k6 завершит запуск как failed. + +## Как интерпретировать результат + +После прогона посмотрите в summary: + +1. `http_req_failed` — если выше 1%, есть проблема со стабильностью. +2. `http_req_duration p(95)` — если выше 1500ms, есть деградация по задержке. +3. `checks` — если ниже 95%, часть ответов не прошла базовую валидацию. + +Минимальный формальный вывод для отчета: + +- «Проведен базовый нагрузочный прогон k6 (3 endpoint'а, 10 VU на сценарий, 5 минут).» +- «Критерии: ошибки < 1%, p95 < 1500ms, checks > 95%.» +- «Статус: пройдено / не пройдено по итогам summary.» diff --git a/frontend/scripts/loadtest-endpoints.js b/frontend/scripts/loadtest-endpoints.js new file mode 100644 index 0000000..5dc0216 --- /dev/null +++ b/frontend/scripts/loadtest-endpoints.js @@ -0,0 +1,66 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:5019'; +const TOKEN = __ENV.TOKEN || ''; +const VUS = Number(__ENV.VUS || 10); +const DURATION = __ENV.DURATION || '2m'; +const PAUSE_SECONDS = Number(__ENV.PAUSE_SECONDS || 0.5); + +const headers = TOKEN + ? { Authorization: `Bearer ${TOKEN}` } + : {}; + +export const options = { + scenarios: { + courses_list: { + executor: 'constant-vus', + vus: VUS, + duration: DURATION, + exec: 'coursesList', + }, + lectures_list: { + executor: 'constant-vus', + vus: VUS, + duration: DURATION, + exec: 'lecturesList', + }, + user_stats: { + executor: 'constant-vus', + vus: VUS, + duration: DURATION, + exec: 'userStats', + }, + }, + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<1500'], + checks: ['rate>0.95'], + }, +}; + +function request(path, tag) { + const res = http.get(`${BASE_URL}${path}`, { + headers, + tags: { endpoint: tag }, + }); + + check(res, { + 'status is 200': (r) => r.status === 200, + 'body is not empty': (r) => (r.body || '').length > 0, + }); + + sleep(PAUSE_SECONDS); +} + +export function coursesList() { + request('/api/v1/courses?page=1&pageSize=50', 'courses_list'); +} + +export function lecturesList() { + request('/api/v1/lectures?page=1&pageSize=50', 'lectures_list'); +} + +export function userStats() { + request('/api/v1/users/me/stats', 'user_stats'); +}