В прошлых частях я писал про то, когда нагрузка нужна, и как поставить k6. Теперь — про самое интересное: как собрать скрипт, который имитирует реального пользователя, а не просто бьёт в один эндпоинт.
Мои первые скрипты были ужасны. Один запрос в цикле, хардкод ID, никакой связности. Я получал зелёные метрики и думал, что всё ок. Потом оказалось, что реальные пользователи ходят по 5-7 страницам, авторизуются, кладут товары в корзину, и в этом потоке приложение падает совсем в другом месте.
Здесь я покажу, как я сейчас собираю сценарии: от простого к сложному, с данными между шагами и с разумным разделением кода.
Простой сценарий: авторизация + действие
Самый распространённый паттерн: пользователь логинится, получает токен, делает что-то полезное. Вот как это выглядит в k6:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10,
duration: '1m',
};
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export default function () {
// Шаг 1: Логин
const loginRes = http.post(`${BASE_URL}/auth/login`, JSON.stringify({
email: 'test@example.com',
password: 'testpass123',
}), {
headers: { 'Content-Type': 'application/json' },
});
check(loginRes, {
'login status is 200': (r) => r.status === 200,
'login returns token': (r) => r.json('token') !== undefined,
});
const token = loginRes.json('token');
sleep(1);
// Шаг 2: Получить профиль
const profileRes = http.get(`${BASE_URL}/profile`, {
headers: { Authorization: `Bearer ${token}` },
});
check(profileRes, {
'profile status is 200': (r) => r.status === 200,
});
sleep(2);
// Шаг 3: Создать заказ
const orderRes = http.post(`${BASE_URL}/orders`, JSON.stringify({
items: [{ id: 1, qty: 2 }],
}), {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
});
check(orderRes, {
'order created': (r) => r.status === 201,
'order has id': (r) => r.json('id') !== undefined,
});
sleep(1);
}Что здесь важно:
- Каждый шаг проверяется отдельно. Если логин упал, я сразу вижу это в метриках.
- Данные передаются между шагами: токен из ответа используется в следующих запросах.
sleep()между шагами — это не пауза «для галочки», а имитация реального пользователя. Без него получается не нагрузочный тест, а DDoS собственного сервиса.
Разделение кода: не пишите всё в один файл
Когда сценарий разрастается, я разбиваю его на части:
// config.js
export const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000';
export const DEFAULT_HEADERS = {
'Content-Type': 'application/json',
};
// auth.js
import http from 'k6/http';
import { check } from 'k6';
import { BASE_URL, DEFAULT_HEADERS } from './config.js';
export function login(email, password) {
const res = http.post(`${BASE_URL}/auth/login`, JSON.stringify({ email, password }), {
headers: DEFAULT_HEADERS,
});
check(res, {
'login ok': (r) => r.status === 200,
});
return res.json('token');
}
// orders.js
import http from 'k6/http';
import { check } from 'k6';
import { BASE_URL, DEFAULT_HEADERS } from './config.js';
export function createOrder(token, items) {
const res = http.post(`${BASE_URL}/orders`, JSON.stringify({ items }), {
headers: { ...DEFAULT_HEADERS, Authorization: `Bearer ${token}` },
});
check(res, {
'order created': (r) => r.status === 201,
});
return res.json('id');
}
// main.js
import { sleep } from 'k6';
import { login } from './auth.js';
import { createOrder } from './orders.js';
export default function () {
const token = login('test@example.com', 'testpass123');
sleep(1);
createOrder(token, [{ id: 1, qty: 2 }]);
sleep(2);
}Так проще поддерживать: логика авторизации в одном месте, заказы — в другом. Если API меняется, правлю в одном файле, а не ищу по всем скриптам.
Параметризация данных: не хардкодьте ID
Моя типичная ошибка — использовать один и тот же ID товара для всех VU. В реальности разные пользователи смотрят разные товары. в k6 есть встроенный механизм под это:
// data.json
[
{ "id": 1, "name": "Product A" },
{ "id": 2, "name": "Product B" },
{ "id": 3, "name": "Product C" }
]
// script.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { SharedArray } from 'k6/data';
import { BASE_URL } from './config.js';
const products = new SharedArray('products', function () {
return JSON.parse(open('./data.json'));
});
export default function () {
const product = products[Math.floor(Math.random() * products.length)];
const res = http.get(`${BASE_URL}/products/${product.id}`);
check(res, { 'product loaded': (r) => r.status === 200 });
sleep(1);
}
SharedArrayважен: он загружает данные один раз и раздаёт всем VU. Обычныйopen()внутриdefault functionбудет читать файл на каждую итерацию — и быстро станет bottleneck.
Scenarios: когда один поток — мало
Реальные пользователи ведут себя по-разному. Кто-то быстро листает каталог, кто-то долго сидит в корзине, кто-то вообще только читает статьи. В k6 это моделируется через scenarios:
// script.js — login и createOrder берём из примера с разделением кода выше
import http from 'k6/http';
import { sleep } from 'k6';
import { login } from './auth.js';
import { createOrder } from './orders.js';
import { BASE_URL } from './config.js';
export const options = {
scenarios: {
browsers: {
executor: 'constant-vus',
vus: 50,
duration: '5m',
exec: 'browseCatalog',
},
buyers: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 20 },
{ duration: '3m', target: 20 },
{ duration: '1m', target: 0 },
],
exec: 'buyFlow',
},
},
};
export function browseCatalog() {
http.get(`${BASE_URL}/products`);
sleep(Math.random() * 3 + 1);
}
export function buyFlow() {
const token = login('buyer@example.com', 'pass');
createOrder(token, [{ id: 1, qty: 1 }]);
sleep(2);
}Здесь два независимых потока: «браузеры» постоянно листают каталог, «покупатели» заходят волнами и делают заказы. Метрики собираются отдельно для каждого сценария.
Что я проверяю перед «боевым» прогоном
- Скрипт проходит дымовой прогон без ошибок.
- Все checks зелёные.
- Данные не заканчиваются на 100-й итерации.
- Токены не протухают раньше, чем закончится сценарий.
- Логи приложения не показывают необработанных ошибок.
К следующей части
Со сценариями разобрались. Но метрики в терминале — это только половина дела. В следующей части я покажу, как складывать результаты в InfluxDB и смотреть на них в Grafana, чтобы видеть динамику, а не только итоговые цифры.
Discussion
No comments yet - start the thread.