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







