docs: добавил k6 тестирование
Frontend CI / build-and-check (push) Failing after 19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 10s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 25s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 2s
Frontend CI / build-and-check (push) Failing after 19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 8s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 10s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 25s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 2s
This commit is contained in:
@@ -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-порога. Это стоит учитывать при дальнейшем анализе хвостовых задержек.
|
||||||
@@ -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="<jwt>"
|
||||||
|
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.»
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user