Відтворення реального трафіку: від логів до k6-сценаріїв
При навантажувальному тестуванні типова помилка — використовувати рівномірну подачу запитів з фіксованим числом віртуальних користувачів. У реальності трафік має піки (вранці та ввечері), різні типи користувачів (мобільні браузери, API-клієнти), випадкові паузи та розподіл 80/20. Наприклад, 80% запитів припадають на 20% сторінок. Синтетичні тести часто пропускають проблеми з кешуванням, сесійним станом та конкурентністю.
Ми пропонуємо підхід, заснований на аналізі реального трафіку з логів Nginx або Google Analytics, і генерації сценаріїв для k6, які точно відтворюють поведінку справжніх користувачів. Реалістичне тестування виявляє в три рази більше вузьких місць, ніж рівномірне. Замовники економлять у середньому 40 000–80 000 грн на налагодженні завдяки ранньому виявленню проблем. Зв'яжіться з нами для обговорення вашого проєкту.
Як працює аналіз логів?
# Вилучити паттерни з nginx access log
import re
from collections import Counter, defaultdict
import json
def analyze_access_log(log_file: str):
pattern = re.compile(
r'(?P<ip>\S+) .+ \[(?P<time>[^\]]+)\] '
r'"(?P<method>\w+) (?P<path>[^"]+) HTTP/\d+" '
r'(?P<status>\d+) (?P<bytes>\d+)'
)
endpoint_counts = Counter()
method_counts = Counter()
hourly_traffic = defaultdict(int)
with open(log_file) as f:
for line in f:
m = pattern.match(line)
if not m:
continue
# Нормалізувати path (прибрати ID)
path = re.sub(r'/\d+', '/{id}', m.group('path').split('?')[0])
endpoint_counts[f"{m.group('method')} {path}"] += 1
method_counts[m.group('method')] += 1
# Погодинний розподіл
hour = m.group('time').split(':')[1]
hourly_traffic[hour] += 1
total = sum(endpoint_counts.values())
print("=== Top Endpoints (% of traffic) ===")
for endpoint, count in endpoint_counts.most_common(20):
pct = count / total * 100
print(f" {pct:.1f}% {endpoint}")
print("\n=== Hourly Distribution ===")
for hour in sorted(hourly_traffic):
bar = '█' * (hourly_traffic[hour] // 100)
print(f" {hour}:00 {bar} {hourly_traffic[hour]}")
# Експорт для k6 сценарію
weights = {ep: round(cnt/total, 3) for ep, cnt in endpoint_counts.most_common(20)}
return weights
Отримані ваги експортуються в JSON і використовуються для генерації сценаріїв k6. Аналіз access-логів дозволяє виділити 20% ендпоінтів, що генерують 80% трафіку, згідно із законом Парето.
Обмеження рівномірного трафіку
Рівномірне навантаження не створює ефекту "натовпу": коли 1000 користувачів одночасно переходять на один товар після публікації в соцмережах. Воно не перевіряє сесійне кешування, блокування БД при конкурентному записі або деградацію під постійними піками. Реалістична симуляція з розподілом Pareto (80/20) та сесійною поведінкою відтворює такі сценарії, виявляючи вузькі місця до деплою в прод.
Як побудувати сценарій на основі логів?
На основі вилучених ваг ми генеруємо сценарії k6. Для кожного типу користувача створюється окремий executor з різною інтенсивністю. Наприклад, 40% трафіку — анонімні браузери, 50% — авторизовані користувачі, 10% — API-клієнти. Сценарії включають випадкові паузи, розгалуження та ймовірнісні переходи.
// tests/realistic/user-journey.js
import http from 'k6/http'
import { check, sleep } from 'k6'
import { SharedArray } from 'k6/data'
import { randomItem, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'
// Завантажити тестові дані з CSV
const users = new SharedArray('users', function() {
return open('./data/test-users.csv').split('\n')
.slice(1)
.map(row => {
const [email, token, userId] = row.split(',')
return { email, token, userId }
})
})
const searchTerms = new SharedArray('searches', function() {
return open('./data/popular-searches.txt').split('\n').filter(Boolean)
})
export const options = {
scenarios: {
// Анонімні браузери (40% трафіку)
anonymous_browse: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5m', target: 40 },
{ duration: '30m', target: 40 },
{ duration: '5m', target: 0 }
],
exec: 'anonymousBrowse'
},
// Авторизовані користувачі (50% трафіку)
logged_in_users: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '5m', target: 50 },
{ duration: '30m', target: 50 },
{ duration: '5m', target: 0 }
],
exec: 'loggedInJourney'
},
// API-клієнти (10% трафіку)
api_clients: {
executor: 'constant-arrival-rate',
rate: 10,
timeUnit: '1s',
duration: '40m',
preAllocatedVUs: 20,
exec: 'apiClient'
}
},
thresholds: {
http_req_duration: ['p(95)<800'],
http_req_failed: ['rate<0.01'],
}
}
const BASE = __ENV.BASE_URL || 'https://staging.example.com'
// Сценарій: анонімний браузер
export function anonymousBrowse() {
// Лендінг → каталог → товар → вихід
http.get(`${BASE}/`)
sleep(randomIntBetween(1, 4))
const category = randomItem(['electronics', 'clothing', 'books', 'sports'])
http.get(`${BASE}/api/products?category=${category}&limit=20`)
sleep(randomIntBetween(2, 8))
// 30% ідуть одразу, 70% дивляться товар
if (Math.random() > 0.3) {
const productId = randomIntBetween(1, 500)
http.get(`${BASE}/api/products/${productId}`)
sleep(randomIntBetween(3, 15))
}
// 20% роблять пошук
if (Math.random() < 0.2) {
const term = randomItem(searchTerms)
http.get(`${BASE}/api/search?q=${encodeURIComponent(term)}`)
sleep(randomIntBetween(1, 5))
}
}
// Сценарій: авторизований користувач
export function loggedInJourney() {
const user = randomItem(users)
const headers = {
'Authorization': `Bearer ${user.token}`,
'Content-Type': 'application/json'
}
// Профіль
http.get(`${BASE}/api/me`, { headers })
sleep(randomIntBetween(1, 3))
// Перегляд товарів
for (let i = 0; i < randomIntBetween(2, 8); i++) {
const productId = randomIntBetween(1, 500)
http.get(`${BASE}/api/products/${productId}`, { headers })
sleep(randomIntBetween(2, 10))
}
// 40% додають в кошик
if (Math.random() < 0.4) {
http.post(`${BASE}/api/cart/items`, JSON.stringify({
productId: randomIntBetween(1, 500),
quantity: randomIntBetween(1, 3)
}), { headers })
sleep(randomIntBetween(1, 3))
// 60% з тих, хто додав — оформлюють замовлення
if (Math.random() < 0.6) {
http.get(`${BASE}/api/cart`, { headers })
sleep(randomIntBetween(2, 5))
const checkout = http.post(`${BASE}/api/orders`, JSON.stringify({
paymentMethod: 'saved_card',
shippingAddressId: 1
}), { headers })
check(checkout, { 'order created': (r) => r.status === 201 })
}
}
}
// Сценарій: API-клієнт (інтеграція)
export function apiClient() {
const apiKey = __ENV.API_KEY
const headers = {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
}
// Синхронізація продуктів
const r = http.get(`${BASE}/api/v1/products?since=${Date.now() - 3600000}`,
{ headers })
check(r, { 'api: 200': (r) => r.status === 200 })
}
Реалістичний сценарій дає в 1,5 рази точніше моделювання порівняно з рівномірним.
Розподіл Pareto в k6
Реальний трафік: 20% сторінок отримують 80% трафіку. В k6 це моделюється функцією, що генерує ID за степеневим законом:
// Генератор Pareto-розподілу для ID
function paretoId(maxId, shape = 1.5) {
const u = Math.random()
return Math.ceil(maxId * Math.pow(1 - u, 1 / shape))
}
// Використання
const productId = paretoId(10000) // переважно ID 1-200, рідко ID 9000+
Порівняння синтетичного та реалістичного тестування
| Характеристика | Синтетичне тестування | Реалістичне тестування |
|---|---|---|
| Тип трафіку | Рівномірний, заданий вручну | Відтворює реальні паттерни (піки, сесії) |
| Поведінка користувачів | Однакові сценарії для всіх VU | Різні сценарії (аноніми, авторизовані, API) |
| Шлях користувача | Лінійний (головна → товар → кошик) | Розгалужений з ймовірнісними переходами |
| Виявлення вузьких місць | Тільки пропускна здатність | Кешування, повільні ендпоінти, конкурентність |
| Час підготовки | Години | Дні (потрібен аналіз логів) |
Процес роботи
- Аналіз логів: збір access-логів Nginx або даних Google Analytics, вилучення паттернів (ендпоінти, статуси, погодинний розподіл).
- Проєктування сценаріїв: визначення типів користувачів (анонімні, авторизовані, API), побудова ймовірнісних моделей поведінки.
- Реалізація на k6: написання JavaScript-сценаріїв з executors, випадковими паузами та розгалуженнями.
- Тестовий прогін: запуск тесту на staging-оточенні, збір метрик (LCP, CLS, TTFB, помилки).
- Аналіз та звіт: виявлення вузьких місць, порівняння з baseline, рекомендації з оптимізації.
| Етап | Тривалість | Результат |
|---|---|---|
| Аналіз логів | 0.5–1 день | JSON-профіль ендпоінтів та розподілів |
| Проєктування сценаріїв | 0.5–1 день | Ймовірнісні моделі для кожного типу користувача |
| Реалізація на k6 | 1–2 дні | Робочі k6-скрипти з executors |
| Тестовий прогін | 1 день | Метрики, графіки, порогові значення |
| Звіт та рекомендації | 0.5 дня | Документ з аналізом та планом оптимізації |
Приклад профілю навантаження по годинах
Трафік розподілений нерівномірно: пік о 10-11 ранку та 18-19 вечора. В інший час — спад. Ми задаємо ваги для кожної години та генеруємо stages для k6, щоб навантаження відповідало реальному денному циклу.Результати та терміни
- Документація сценарію з описом поведінки кожного типу користувачів.
- Конфігурації k6 (options, thresholds, порогові значення).
- Звіт з результатами тестування: графіки навантаження, процентилі часу відповіді, помилки.
- Рекомендації з оптимізації продуктивності (індекси БД, кешування, асинхронні черги).
- Підтримка при першому запуску та інтерпретації результатів. Ми гарантуємо, що всі сценарії перевірені на наших тестових стендах.
Середня економія на налагодженні — 40 000–80 000 грн завдяки ранньому виявленню вузьких місць. Вартість розробки сценарію — від 30 000 до 50 000 грн.
Розробка реалістичного сценарію навантажувального тесту на основі аналізу реального трафіку займає від 2 до 5 робочих днів. Вартість розраховується індивідуально після ознайомлення з вашими даними.
Замовте реалістичний сценарій навантажувального тестування під ключ. Отримайте консультацію інженерів.







