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

Интерпретация результатов и интеграция с наблюдаемостью

40 минут

Быстрый разбор вывода k6

  • http_req_duration: смотрим p95/p99. Рост без роста CPU сервиса → ищем зависимые системы (БД, внешние API).
  • http_req_waiting растет, http_req_connecting стабильна → проблема на сервере/БД, не сеть.
  • http_req_sending/receiving растут, а CPU сервиса низкий → уперлись в генератор или сеть между k6 и сервисом.
  • dropped_iterations > 0 → генератор не справляется, выводы о сервисе недостоверны.

Пример walk-through (ShopStack, checkout)

  1. Запуск k6 run checkout.js --out influxdb=http://localhost:8086/k6.
  2. Открываем дашборд Grafana:
    • p95 flow=checkout вырос с 520ms до 760ms.
    • CPU app 35%, CPU Postgres 85%, latency БД рост на p99.
    • http_req_waiting повторяет график latency БД.
  3. Гипотеза: bottleneck — БД (N+1 в запросах).
  4. Решение: релиз стоп, на оптимизацию запросов. Бюджет ошибки не сожжен, но SLO по латентности нарушен.

Подписывайте графики: «Рост http_req_waiting без роста CPU → ищем БД/внешние API». Такие подписи помогают бригаде реагировать одинаково.

Интеграция с наблюдаемостью

Триангуляция: начинаем с метрик k6 в Grafana, переходим к метрикам сервисов, затем к логам (request_id), и наконец к трейсам (trace_id) для детального разбора. Все три сигнала должны быть связаны через общие ID.

  • Prometheus: --out prometheus-remote-write=http://prom:9090/api/v1/write или скрейп endpoint k6. Метки flow, endpoint берутся из тегов.
  • InfluxDB + Grafana: классический стек, готовые дашборды k6.
  • Traces (Jaeger/Tempo, OTEL): кореллируйте trace_id из сервиса с метками k6. Добавьте traceparent в заголовок запросов:
import http from "k6/http";
import exec from "k6/execution";
import { Trend } from "k6/metrics";
 
const endToEndLatency = new Trend("e2e_latency");
 
export function checkoutFlow() {
  const traceId = exec.vu.iterationInTest.toString(16).padStart(16, "0");
  const res = http.post(
    `${__ENV.BASE_URL}/api/checkout`,
    { payment: "test" },
    { headers: { traceparent: `00-${traceId}-0000000000000000-01` } }
  );
  endToEndLatency.add(res.timings.duration, res.tags);
}
  • Логи: кореллируйте по request_id/trace_id. На дашборде держите ссылку в Kibana/Loki на конкретный интервал теста.

Решение Go/No-Go

  • Применяем заранее описанный критерий: SLO + сравнение с baseline.
  • Фиксируем вывод в отчете: «SLO по p95 нарушено, bottleneck — БД, генератор здоров, предлагаю откатить фичу X или увеличить пул соединений до 80».
  • Если метрики в норме, но деградация +10% к baseline — пишем задачу, релиз по решению владельца (вписывается в error budget?).

Практический воркшоп (цепочка метрик → логов → трейсов)

  1. Запускаем тест checkout, пишем в Prom/Influx + включаем traceparent для корреляции.
  2. На дашборде видим: p99 http_req_duration{flow=checkout} = 1200ms, p50 ок. http_req_waiting растет → проблема на сервере/БД.
  3. В Grafana через лейблы (endpoint="/api/orders") находим конкретный эндпоинт с пиком.
  4. В Loki: logs{service="orders", status="500"}slow query detected с request_id.
  5. В Tempo/Jaeger: по trace_id/request_id видим span БД 80% времени, запрос без индекса.
  6. Вывод: добавить индекс на orders, пересмотреть query. После фикса повторяем тест → p99 возвращается к baseline.

Держите ссылки из k6 summary → Grafana → лог/трейс в одном отчете. Это экономит часы расследований и превращает нагрузочное тестирование в понятный процесс.

Охота на «медведя» (пример расследования)

  1. Запускаем тест с --out prometheus-remote-write=http://prom:9090/api/v1/write и включаем traceparent.
  2. В Grafana: p99 flow=checkout вырос, http_req_failed = 3%, http_req_waiting повторяет рост latency БД.
  3. В Loki: logs{job="k6", flow="checkout", status="500"} → видим 500 от users-service, request_id известен.
  4. В Tempo/Jaeger: трейс по request_id → span БД 1.5s, родительский span checkout 1.8s, запрос без индекса.
  5. В Grafana дашборде users-service: скачок CPU, GC паузы до 200ms, рост connection pool wait time.
  6. Решение: добавить индекс/батч, увеличить pool. Повторный прогон → метрики возвращаются к baseline, сохраняем новый baseline.

Настройка вывода в Prometheus (remote write)

k6 run checkout.js \
  --out prometheus-remote-write=http://prometheus:9090/api/v1/write \
  --summary-export summary.json

Добавляйте теги release, region, flow, чтобы быстро сравнивать релизы и регионы в Grafana.

Для продвинутого разбора держите готовый дашборд с разрезами по endpoint/flow, ссылку на лог-квери в Loki и поиск в Tempo по trace_id — так расследование не превращается в хаос.

Troubleshooting: типичные проблемы анализа

Причина: Проблема может быть в генераторе нагрузки, а не в сервисе.

Как проверить:

  • dropped_iterations > 0 — генератор не справляется, результаты недостоверны
  • http_req_connecting растет — проблемы с сетью между k6 и сервисом
  • http_req_sending/receiving высокие — упираемся в bandwidth генератора
  • CPU генератора > 85% — недостаточно ресурсов для k6

Решение:

# Проверьте метрики генератора в Grafana
- CPU/RAM k6 pod/instance
- Network throughput
- dropped_iterations метрика

# Если генератор под нагрузкой:

- Увеличьте ресурсы (CPU/RAM)
- Распределите нагрузку (k6-operator с parallelism)
- Уменьшите сложность сценария

Золотое правило: Всегда проверяйте здоровье генератора ПЕРЕД выводами о сервисе.

Причина: Сервис ждет ответа от зависимостей (БД, внешние API, очереди).

Порядок расследования:

  1. Проверьте БД:
    • Латентность запросов (slow query log)
    • CPU/RAM базы данных
    • Connection pool exhaustion
    • Lock contention, deadlocks
  2. Проверьте внешние API:
    • Латентность upstream сервисов
    • Rate limiting (429 ответы)
    • Circuit breaker открыт?
  3. Проверьте кэш:
    • Cache miss rate (Redis/Memcached)
    • Evictions, memory pressure
  4. Используйте трейсы:
    • Откройте Tempo/Jaeger по trace_id
    • Найдите span с максимальным временем
    • Это и есть bottleneck

Пример:

# В Grafana видим:
http_req_waiting: p95 = 1.2s (было 300ms)
CPU service: 25% (норма)

# Идём в Tempo по trace_id → видим:

- checkout span: 1.2s
- payment_api span: 1.1s ← BOTTLENECK
- db span: 50ms

# Вывод: внешний payment API тормозит

# Решение: увеличить timeout, добавить retry, кэшировать

Симптомы проблемы в БД:

  • http_req_waiting растет пропорционально росту нагрузки
  • CPU сервиса низкий (<50%), но latency высокая
  • В трейсах span БД занимает >70% времени запроса
  • Метрики БД показывают проблемы (CPU, slow queries, locks)

Что проверить в БД:

-- PostgreSQL
SELECT * FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY mean_exec_time DESC LIMIT 10;

-- Проверить connection pool
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';

-- Проверить locks
SELECT * FROM pg_locks WHERE NOT granted;

Метрики БД в Grafana:

  • pg_stat_database_tup_fetched — сколько строк читаем
  • pg_stat_database_deadlocks — deadlock'и
  • pg_stat_activity_count — активных соединений
  • pg_slow_queries — медленные запросы

Типичные решения:

  • Добавить индексы на часто используемые колонки
  • Оптимизировать N+1 queries (использовать JOIN)
  • Увеличить connection pool
  • Включить query cache
  • Вертикальное масштабирование БД (CPU/RAM/IOPS)

Причина 1: k6 не отправляет метрики

# Проверьте вывод k6 при запуске
k6 run test.js --out prometheus-remote-write=http://prom:9090/api/v1/write

# Должно быть:

INFO[0000] output: prometheus-remote-write

# Если ошибка "dial tcp: connection refused":

- Проверьте URL Prometheus
- Проверьте сетевую доступность (ping, telnet)
- Проверьте Prometheus remote-write endpoint включен

Причина 2: Неправильные labels/фильтры в Grafana

# В Grafana query проверьте:
http_req_duration{flow="checkout"} # Есть ли такой тег?

# Посмотрите все метрики k6:

{**name**=~"http_req.*"}

# Посмотрите все labels:

{job="k6"}

Причина 3: Time range не совпадает

  • Тест запустили в 14:00, а в Grafana выбран range "Last 5 minutes" в 14:10
  • Решение: Выберите правильный временной интервал

Причина 4: Prometheus не скрейпит метрики

# Проверьте в Prometheus UI (http://prom:9090/targets)

- Статус k6 target должен быть UP
- Last scrape должен быть недавно

# Если DOWN:

- Проверьте prometheus.yml scrape_configs
- Проверьте Service/Pod в Kubernetes
- Проверьте network policy

Проблема: Некорректное сравнение даёт ложные выводы о регрессии.

Правильный подход:

  1. Одинаковые условия:
    • Один и тот же стенд (не dev vs stage)
    • Одинаковая версия данных (не пустая БД vs prod-like)
    • Одинаковое время суток (нагрузка на shared ресурсы)
    • Одинаковый сценарий и параметры k6
  2. Используйте baseline:
    # Сохраните baseline после успешного теста
    k6 run test.js --summary-export baseline.json
    
    # При следующем тесте сравните
    
    k6 run test.js --summary-export current.json
    node compare.js baseline.json current.json
  3. Сравнивайте перцентили, а не средние:
    • ❌ Неправильно: avg = 150ms vs 180ms (+20%)
    • ✅ Правильно: p95 = 300ms vs 450ms (+50%)
    • Причина: средние скрывают выбросы
  4. Учитывайте вариацию:
    • Разница <5% — скорее всего шум
    • Разница 5-10% — потенциальная проблема, повторите тест
    • Разница >10% — вероятная регрессия, расследуйте
  5. Прогревайте систему (warmup):
    • Первые 2-3 минуты не учитывайте в результатах
    • JIT, кэши, connection pool должны прогреться

Автоматическое сравнение в CI:

# В GitLab CI / GitHub Actions

- Прогоните тест
- Сохраните summary.json в артефакты
- Сравните с baseline из предыдущего успешного запуска
- Если p95 вырос >10% → fail pipeline

Причина 1: Неправильный формат traceparent

// ❌ Неправильно
const traceId = "12345";
headers: { traceparent: traceId }

// ✅ Правильно (W3C Trace Context format)
const traceId = exec.vu.iterationInTest.toString(16).padStart(32, "0");
const spanId = "0000000000000000";
headers: {
traceparent: `00-${traceId}-${spanId}-01`
}

// Формат: version-trace_id-span_id-flags
// 00 = version
// trace_id = 32 hex символа
// span_id = 16 hex символов
// 01 = sampled flag

Причина 2: Сервис не пробрасывает trace_id

  • Проверьте, что сервис поддерживает W3C Trace Context
  • Проверьте middleware для distributed tracing (OpenTelemetry SDK)
  • Посмотрите логи сервиса — появляется ли trace_id?

Причина 3: Sampling rate слишком низкий

// В OpenTelemetry настройках сервиса
samplingRate: 1.0 // 100% для load testing
// В проде обычно 0.01-0.1

Причина 4: Трейсы не доходят до Tempo/Jaeger

  • Проверьте OTEL Collector logs
  • Проверьте network между сервисом и Tempo
  • Проверьте OTEL_EXPORTER_OTLP_ENDPOINT в сервисе

Как отладить:

// 1. Выведите trace_id в логи k6
console.log("Trace ID:", traceId);

// 2. Найдите этот trace_id в логах сервиса
grep "trace_id=<YOUR_ID>" service.log

// 3. Если есть в логах, но нет в Tempo:
// - Проблема в экспорте трейсов
// 4. Если нет в логах:
// - Проблема в propagation trace context

Практика: что сделать до следующего урока

1.Настройте отправку метрик в Prometheus

  • Если у вас есть Prometheus — запустите k6 с --out prometheus-remote-write=http://localhost:9090/api/v1/write
  • Если нет Prometheus — используйте Grafana Cloud (бесплатный tier) или локальный Docker: docker run -p 9090:9090 prom/prometheus
  • Добавьте в тест теги: { test_id: 'my-test', endpoint: '/api/users' }
  • Откройте Prometheus UI (localhost:9090) и выполните query: http_req_duration{test_id='my-test'}
  • Убедитесь, что метрики появляются

2.Добавьте traceparent для корреляции с APM

  • В вашем k6-сценарии добавьте генерацию trace_id из exec.vu.iterationInTest
  • Добавьте header traceparent в формате W3C Trace Context
  • Если у вас есть Jaeger/Tempo — настройте сервис для приема трейсов
  • Запустите тест и найдите trace_id в логах сервиса
  • Откройте Jaeger UI и найдите трейс по trace_id — убедитесь, что span'ы связаны

3.Проведите расследование bottleneck

  • Запустите load-тест и дождитесь роста http_req_waiting
  • В Grafana найдите endpoint с максимальным p95
  • Проверьте метрики сервиса: CPU, память, БД latency
  • Если CPU низкий, но latency высокая → ищите в БД (connection pool, slow queries)
  • Используйте трейсы: найдите span с максимальным временем — это и есть bottleneck
  • Запишите вывод: где узкое место и как его исправить?

📊 Реальный кейс: Black Friday 2023 — спасение за 2 дня до пика

Контекст: E-commerce модной одежды (18M посетителей/месяц), за 2 дня до Black Friday деплоят обновление корзины с новой фичей dynamic discounts.

Проблема: После деплоя на pre-prod p99 latency /api/cart/* выросла с 300ms до 1.2s.

Что произошло:

  1. k6 stress-тест обнаружил регрессию:

    k6 run --out prometheus-remote-write=http://grafana:9090/api/v1/push \
           --tag env=pre-prod \
           black-friday-stress.js

    Результаты:

    • ✅ p95 /checkout: 420ms (SLO: <500ms)
    • ✅ p95 /catalog: 180ms
    • 🔴 p99 /api/cart/add: 1.2s (SLO: <600ms, было 300ms)
    • 🔴 p99 /api/cart/calculate-total: 1.8s (SLO: <800ms, было 450ms)
    • Error rate: 0.12% (в пределах SLO, но выше baseline)
  2. Триангуляция через k6 + Grafana + Jaeger (2 часа анализа):

    Шаг 1: k6 metrics → определили endpoint

    • k6 summary показал: рост latency только на /api/cart/*
    • Остальные endpoints (catalog, checkout, search) без изменений
    • Вывод: проблема локальна в cart service

    Шаг 2: Grafana (Postgres metrics) → нашли симптом

    • Dashboard "Postgres Performance":
      • waiting_connections: достиг максимума 50/50 (pool exhausted!)
      • slow_queries (>500ms): +240% vs baseline
      • active_queries: медиана 18 (было 4)
    • Вывод: БД не справляется, connection pool исчерпывается

    Шаг 3: Slow query log → нашли запрос

    -- Slow query (1.2s avg):
    SELECT * FROM cart_items ci
    JOIN products p ON ci.product_id = p.id
    JOIN discounts d ON p.category_id = d.category_id
    WHERE ci.user_id = $1
    AND d.valid_from <= NOW()
    AND d.valid_to >= NOW()
    ORDER BY ci.created_at DESC;
    • Запрос выполняется для каждой позиции в корзине (N+1 query!)
    • Новая фича: dynamic discounts проверяет скидки для каждого товара отдельно
    • Средняя корзина: 8 товаров → 8 запросов → 8 × 150ms = 1.2s

    Шаг 4: Jaeger traces → подтвердили root cause

    • Trace ID из k6 (через traceparent header)

    • Открыли trace в Jaeger:

      POST /api/cart/calculate-total (1.8s total)
        ├─ cart_service.calculateTotal (1.7s)
        │  ├─ db.getCartItems (120ms) ✅
        │  └─ FOR EACH item (8 iterations):
        │     └─ discount_service.getActiveDiscount (180ms) 🔴 N+1!
        └─ payment_service.reserveFunds (80ms) ✅
    • Вывод: N+1 query в новом коде discount_service.getActiveDiscount()

  3. Решение (18 hours to Black Friday):

    Вариант A (откат): Удалить dynamic discounts фичу

    • ✅ Безопасно: вернемся к baseline
    • ❌ Потеряем конкурентное преимущество (маркетинг обещал dynamic discounts)

    Вариант B (оптимизация): Исправить N+1 query

    • Batch query: загружать все discounts за 1 запрос
    • Redis cache: hot discounts (TTL 5 min)
    • Риск: изменения за 18 часов до пика

    Решение команды: Откатили фичу, но запланировали оптимизацию после BFCM

    Код-ревью после отката:

    // БЫЛО (N+1):
    async function calculateTotal(cartItems) {
      let total = 0;
      for (const item of cartItems) {
        const discount = await getActiveDiscount(item.productId); // N queries!
        total += item.price * (1 - discount.percentage);
      }
      return total;
    }
     
    // СТАЛО (batch query):
    async function calculateTotal(cartItems) {
      const productIds = cartItems.map((i) => i.productId);
      const discounts = await getActiveDiscountsBatch(productIds); // 1 query!
      let total = 0;
      for (const item of cartItems) {
        const discount = discounts[item.productId] || { percentage: 0 };
        total += item.price * (1 - discount.percentage);
      }
      return total;
    }
  4. Повторный stress-тест после отката:

    • ✅ p99 /api/cart/*: 310ms (baseline восстановлен)
    • ✅ Postgres waiting_connections: max 12/50
    • Go-decision: релиз в production

Результат Black Friday:

  • Peak traffic: 14.2K RPS (прогноз: 12K)
  • p99 /checkout: 480ms (SLO: <600ms)
  • Error rate: 0.09%
  • Zero инцидентов, revenue: $12.8M за 24h (record)

Что предотвратил k6:

  • Потенциальный downtime: при 14K RPS connection pool исчерпался бы за 3 минуты
  • Прогноз убытков: 2-4 часа partial outage × $533K/hour = $1-2M потерь
  • Reputation damage: trending в социальных сетях, потеря клиентов

ROI анализа:

  • Стоимость: 12 часов работы (4 инженера × 3 часа) = $6K
  • Избежали: $1-2M прямых потерь + reputation damage
  • ROI: 167-333x

Что сделали после BFCM (post-mortem):

  1. Оптимизировали dynamic discounts:

    • Batch query вместо N+1 → latency 45ms (было 1.2s)
    • Redis cache для hot products (80% cache hit rate)
    • Добавили EXPLAIN ANALYZE в PR checklist для DB queries
  2. Обязательный pre-release stress test:

    • Любой PR, касающийся cart/checkout → mandatory stress test
    • Thresholds привязаны к baseline: p99 < baseline * 1.15 (max 15% regression)
    • CI блокирует merge, если thresholds не прошли
  3. Улучшили observability:

    • Автоматическая корреляция: k6 → Prometheus → Jaeger (по request_id)
    • Grafana dashboard "Performance Regression" для быстрого анализа
    • Alerts на connection pool exhaustion (warning at 70%, critical at 90%)

War story: CTO на all-hands: "k6 спас нам Black Friday. Мы чуть не выкатили код, который убил бы production. Теперь правило: любое изменение в критичных сервисах проходит stress-тест с полной observability stack".

Урок: Интеграция k6 с observability (Grafana + Jaeger + logs) — это не опция, а обязательное требование для production-grade load testing. Триангуляция k6 metrics → service metrics → DB metrics → traces позволяет находить root cause за минуты, а не дни.

Обязательный flow:

  1. k6 обнаруживает проблему (p99 latency spike)
  2. Grafana показывает симптом (DB connection pool exhausted)
  3. Slow query log показывает запрос (N+1 query)
  4. Jaeger показывает execution path (какой span медленный)
  5. Code review → исправление → повторный тест

✅ Чек-лист завершения урока

После этого урока вы должны уметь:

Чтение результатов k6:

  • Интерпретировать http_req_waiting vs http_req_connecting для локализации проблемы
  • Определять bottleneck по метрикам: dropped_iterations, CPU генератора, network bandwidth
  • Знать, когда проблема в генераторе, а когда в сервисе

Интеграция с observability:

  • Настроить вывод метрик в Prometheus (--out prometheus-remote-write)
  • Добавить traceparent header для корреляции с трейсами
  • Связать метрики k6, логи (request_id), трейсы (trace_id) в единую картину

Триангуляция проблем:

  • Начинать с метрик k6 → метрики сервисов → логи → трейсы
  • Использовать Grafana для быстрой фильтрации по flow/endpoint
  • Находить bottleneck span в Tempo/Jaeger по trace_id

Процесс Go/No-Go:

  • Применять заранее описанные критерии: SLO + baseline
  • Документировать вывод: метрики, bottleneck, рекомендация
  • Понимать, когда релиз Go / Review / No-Go

Практическое задание:

  • Запустите тест с отправкой в Prometheus/InfluxDB
  • Добавьте traceparent и найдите trace в Tempo/Jaeger
  • Проведите расследование: k6 метрики → сервис → БД → вывод
  • Создайте отчет Go/No-Go с ссылками на дашборд/трейсы

Если чек-лист пройден — переходите к уроку 12: встроим k6 в CI/CD с автоматическим Go/No-Go.

Интерпретация результатов и интеграция с наблюдаемостью — k6: нагрузочное тестирование как система — Potapov.me