Реалистичные пользовательские потоки и миксы
Откуда брать проценты
- Логи/аналитика: посмотрите распределение событий за типичный час пика. Пример ShopStack (stage ≈ prod):
- 68% — чтение каталога/поиск
- 22% — добавление в корзину
- 8% — checkout
- 2% — админские действия
- Сессии, а не запросы: ищем последовательности действий, а не частоту отдельных эндпоинтов.
- Пиковые окна: берем данные за час пика, а не средний день.
Код: микс сценариев с разным весом
// traffic-mix.js
import http from "k6/http";
import { check, sleep } from "k6";
const base = __ENV.BASE_URL;
export const options = {
scenarios: {
readers: {
executor: "ramping-arrival-rate",
startRate: 50,
timeUnit: "1s",
preAllocatedVUs: 30,
maxVUs: 150,
stages: [
{ target: 200, duration: "5m" },
{ target: 200, duration: "20m" },
{ target: 0, duration: "3m" },
],
tags: { flow: "reader" },
exec: "reader",
},
buyers: {
executor: "ramping-arrival-rate",
startRate: 10,
timeUnit: "1s",
preAllocatedVUs: 40,
maxVUs: 200,
stages: [
{ target: 60, duration: "5m" },
{ target: 60, duration: "20m" },
{ target: 0, duration: "3m" },
],
tags: { flow: "buyer" },
exec: "buyer",
},
admins: {
executor: "constant-arrival-rate",
rate: 5,
timeUnit: "1s",
duration: "10m",
preAllocatedVUs: 10,
maxVUs: 20,
tags: { flow: "admin" },
exec: "admin",
},
},
thresholds: {
"checks{flow:buyer}": ["rate>0.97"],
"http_req_duration{flow:buyer}": ["p(95)<700"],
},
};
export function reader() {
const res = http.get(`${base}/api/catalog?q=shoes`);
check(res, { "catalog 200": (r) => r.status === 200 });
sleep(1 + Math.random()); // think time
}
export function buyer() {
const login = http.post(`${base}/api/login`, { user: "demo", pass: "demo" });
check(login, { "login ok": (r) => r.status === 200 });
const add = http.post(`${base}/api/cart`, { sku: "SKU-1", qty: 1 });
check(add, { "cart ok": (r) => r.status === 200 });
const checkout = http.post(`${base}/api/checkout`, { payment: "test" });
check(checkout, { "checkout ok": (r) => r.status === 200 });
sleep(2);
}
export function admin() {
const res = http.put(`${base}/api/admin/products/SKU-NEW`, { price: 99 });
check(res, { "admin ok": (r) => r.status === 200 });
sleep(3);
}Анти-паттерн: бить /login 10k rps. В реальной жизни логинов на порядок меньше, а чтения каталога — наоборот. Разделяйте трафик по journeys, а не по эндпоинтам.
Think time — часть модели. Используйте sleep() в сценариях, чтобы не
получить нагрузку «машинная пушка», если пользователи в жизни читают страницу
1–3 секунды.
Динамическое моделирование «рабочего дня»
- Стройте кривую нагрузки по логам nginx/ALB за типичный день: утром 0.6×, днем 1.0×, вечером 1.3×. Конвертируйте в
stagesилиramping-arrival-rate. - Учитывайте кампании: блок с резким всплеском + медленный спад («хвост»).
- Разделяйте регионы: latency и CDN кеш дадут разный профиль p95 → теги
regionобязательны. - Проверяйте корреляцию VU ↔ RPS: держите запас по
maxVUsи следите заdropped_iterations, чтобы не «сжечь» сервис синтетической нагрузкой из-за слишком агрессивного ramp.
Конвертация логов в stages (пример)
# parse_logs.py
import json, datetime
from collections import Counter
counts = Counter()
with open("access.log") as f:
for line in f:
ts = line.split()[3].lstrip("[") # [12/Feb/2024:10:00:01
dt = datetime.datetime.strptime(ts, "%d/%b/%Y:%H:%M:%S")
bucket = dt.replace(minute=0, second=0)
counts[bucket] += 1
stages = []
peak = max(counts.values())
for bucket, rps in sorted(counts.items()):
target = int(rps / peak * 200) # нормируем на 200 rps пиковое
stages.append({"duration": "10m", "target": target})
print(json.dumps(stages, default=str, indent=2))Полученные stages вставляем в ramping-arrival-rate, получаем нагрузку, максимально похожую на прод.
Управление тестовыми данными в сценариях
- Префиксуйте данные run-id:
(__ENV.RUN_ID || Date.now()) + '-' + __VU + '-' + __ITER. - Держите пулы тестовых пользователей/sku в
SharedArrayи раздавайте циклически. - Для write-потоков добавляйте teardown/cleanup или отдельный job, который удаляет сущности с меткой
run-id, чтобы параллельные запуски не конфликтовали.
📊 Реальный кейс: Streaming-платформа и "нереалистичный микс"
Контекст: Видео-стриминг платформа (стиль Netflix), готовились к премьере популярного сериала. Прогноз: 500K одновременных зрителей в первый час.
Проблема: Load-тесты показывали "всё ок", но в production случился partial outage.
Что пошло не так:
-
Load-тест моделировал "равномерный микс":
// WRONG: равномерная нагрузка на все endpoints export const options = { scenarios: { uniform_load: { executor: "constant-arrival-rate", rate: 5000, // RPS duration: "30m", }, }, }; export default function () { const endpoints = [ "/api/catalog/browse", "/api/video/play", "/api/user/profile", "/api/search", ]; const randomEndpoint = endpoints[Math.floor(Math.random() * endpoints.length)]; http.get(BASE_URL + randomEndpoint); sleep(1); }- Каждый endpoint получал ~1250 RPS (25% от 5000)
- Реальный трафик: 85%
/video/play, 10%/catalog/browse, 5% остальное
-
Production incident (премьера сериала):
- 🔴 CDN origin overwhelmed:
/video/playполучил 8500 RPS (vs 1250 RPS в тестах) - 🔴 Video transcoding queue заполнилась за 4 минуты (лимит: 10K concurrent jobs)
- 🔴 Postgres connection pool исчерпался (connections взяли
/video/play+ analytics queries) - ⚠️ 38% пользователей получили "Video unavailable" в первые 12 минут
- 🔴 CDN origin overwhelmed:
-
Потери:
- 12 минут partial outage → 190K зрителей не смогли начать просмотр
- Social media outrage: #NetflixDownAgain trending
- Customer churn: 2.8K подписчиков отменили subscription в течение 24 часов
- Revenue loss: ~$85K (subscription refunds + churn)
Root cause: Load-тест не моделировал реальное распределение user journeys. Команда тестировала "среднюю температуру по больнице".
Что сделали после инцидента:
-
Проанализировали реальный трафик за премьеры (last 3 months):
Типичная премьера (first hour): - 82% — video playback (/api/video/play, /cdn/chunks/*) - 12% — catalog browsing (/api/catalog/*, /api/recommendations) - 4% — search/discovery - 2% — user actions (watchlist, ratings) -
Создали realistic traffic model:
// traffic-premiere.js export const options = { scenarios: { video_watchers: { executor: "ramping-arrival-rate", startRate: 500, timeUnit: "1s", preAllocatedVUs: 1000, maxVUs: 5000, stages: [ { target: 4100, duration: "10m" }, // 82% от 5000 RPS { target: 4100, duration: "30m" }, { target: 2000, duration: "10m" }, ], tags: { flow: "playback" }, exec: "playVideo", }, catalog_browsers: { executor: "ramping-arrival-rate", startRate: 60, timeUnit: "1s", preAllocatedVUs: 150, maxVUs: 800, stages: [ { target: 600, duration: "10m" }, // 12% от 5000 RPS { target: 600, duration: "30m" }, { target: 300, duration: "10m" }, ], tags: { flow: "browse" }, exec: "browseCatalog", }, // ... search (4%), user_actions (2%) }, }; export function playVideo() { // Realistic journey: play → heartbeat every 10s → quality switch const videoId = getRandomVideo(); http.post(`${BASE_URL}/api/video/play`, { videoId }); sleep(randomIntBetween(8, 12)); http.post(`${BASE_URL}/api/video/heartbeat`, { videoId }); sleep(randomIntBetween(8, 12)); // ...simulate 10 minutes of watching } -
Обнаружили новые bottlenecks:
- CDN origin требует rate limiting: 5000 req/s → graceful degradation
- Transcoding queue: добавили auto-scaling (max 25K jobs) + priority queue
- Postgres: выделили отдельный connection pool для
/video/play(200 connections) - Analytics queries → вынесли в read replica
-
Повторный stress-тест (realistic mix):
- ✅ Выдержали 5500 RPS (10% запас) с реальным распределением
- ✅ p95
/video/play: 280ms (SLO: <400ms) - ✅ CDN origin: graceful degradation на 6K RPS
- ✅ Transcoding queue: auto-scaling сработал на 18K jobs
Результат следующей премьеры (2 месяца спустя):
- Peak traffic: 6200 RPS (превысили прогноз на 24%)
- p95 latency: 310ms
- Error rate: 0.08%
- Zero инцидентов, highest concurrent viewers в истории платформы (580K)
ROI:
- Стоимость: реалистичный traffic model + CDN/DB оптимизации = $120K
- Избежали: потеря подписчиков ($85K/incident × 4 премьеры/год) + reputation damage
- ROI: 2.8x (только по прямым потерям, не считая репутацию)
War story: После этого инцидента команда ввела правило: "Любой load-тест должен начинаться с анализа production логов за последние 30 дней". Один инженер сказал: "Мы тестировали приложение, которое существовало только в нашем воображении, а не то, которым пользуются люди".
Урок: Реалистичное распределение трафика важнее абсолютных цифр RPS. Тест на 10K RPS с равномерным распределением хуже, чем тест на 5K RPS с реальным профилем пользователей. Всегда: 1. Анализируйте production логи за пиковые окна 2. Моделируйте user journeys, а не отдельные endpoints 3. Используйте weighted scenarios для разных типов пользователей 4. Добавляйте realistic think time между запросами
✅ Чек-лист завершения урока
После этого урока вы должны уметь:
Анализ трафика:
- Извлечь проценты потоков из логов/аналитики (reader 68%, buyer 22%, etc.)
- Понимать разницу между «запросами» и «сессиями/journeys»
- Брать данные за пиковые окна, а не средний день
Моделирование миксов:
- Создать несколько scenarios с разным exec для разных потоков
- Настроить правильное соотношение arrival rates (reader 200 RPS, buyer 60 RPS)
- Добавить tags для разделения потоков:
{ flow: 'reader' }
Think time и реалистичность:
- Добавить
sleep()с случайным временем для реалистичного think time - Понимать, почему нельзя молотить один endpoint без пауз
- Моделировать user journey: login → browse → add to cart → checkout
Работа с данными:
- Использовать
SharedArrayдля тестовых данных (users, products) - Генерировать уникальные ID:
Date.now() + '-' + __VU + '-' + __ITER - Префиксовать данные run-id для cleanup
Практическое задание:
- Проанализируйте логи вашего приложения и выделите 2-3 основных user journey
- Создайте traffic-mix.js с разными scenarios для каждого journey
- Запустите и убедитесь, что RPS распределен согласно реальным пропорциям
Если чек-лист пройден — переходите к уроку 08: научимся писать чистый, модульный код для k6-тестов.