Відтворення реального трафіку: від логів до 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 робочих днів. Вартість розраховується індивідуально після ознайомлення з вашими даними.
Замовте реалістичний сценарій навантажувального тестування під ключ. Отримайте консультацію інженерів.







