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







