Skip to main content
Back to course
k6: нагрузочное тестирование как система
7 / 1741%

Реалистичные пользовательские потоки и миксы

25 минут

Откуда брать проценты

  • Логи/аналитика: посмотрите распределение событий за типичный час пика. Пример 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.

Что пошло не так:

  1. 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% остальное
  2. 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 минут
  3. Потери:

    • 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. Команда тестировала "среднюю температуру по больнице".

Что сделали после инцидента:

  1. Проанализировали реальный трафик за премьеры (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)
  2. Создали 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
    }
  3. Обнаружили новые 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
  4. Повторный 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-тестов.

Реалистичные пользовательские потоки и миксы — k6: нагрузочное тестирование как система — Potapov.me