Интерпретация результатов и интеграция с наблюдаемостью
Быстрый разбор вывода 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)
- Запуск
k6 run checkout.js --out influxdb=http://localhost:8086/k6. - Открываем дашборд Grafana:
- p95
flow=checkoutвырос с 520ms до 760ms. - CPU app 35%, CPU Postgres 85%, latency БД рост на p99.
http_req_waitingповторяет график latency БД.
- p95
- Гипотеза: bottleneck — БД (N+1 в запросах).
- Решение: релиз стоп, на оптимизацию запросов. Бюджет ошибки не сожжен, но 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?).
Практический воркшоп (цепочка метрик → логов → трейсов)
- Запускаем тест checkout, пишем в Prom/Influx + включаем
traceparentдля корреляции. - На дашборде видим: p99
http_req_duration{flow=checkout}= 1200ms, p50 ок.http_req_waitingрастет → проблема на сервере/БД. - В Grafana через лейблы (
endpoint="/api/orders") находим конкретный эндпоинт с пиком. - В Loki:
logs{service="orders", status="500"}→slow query detectedсrequest_id. - В Tempo/Jaeger: по
trace_id/request_idвидим span БД 80% времени, запрос без индекса. - Вывод: добавить индекс на
orders, пересмотреть query. После фикса повторяем тест → p99 возвращается к baseline.
Держите ссылки из k6 summary → Grafana → лог/трейс в одном отчете. Это экономит часы расследований и превращает нагрузочное тестирование в понятный процесс.
Охота на «медведя» (пример расследования)
- Запускаем тест с
--out prometheus-remote-write=http://prom:9090/api/v1/writeи включаемtraceparent. - В Grafana: p99
flow=checkoutвырос,http_req_failed= 3%,http_req_waitingповторяет рост latency БД. - В Loki:
logs{job="k6", flow="checkout", status="500"}→ видим 500 отusers-service, request_id известен. - В Tempo/Jaeger: трейс по
request_id→ span БД 1.5s, родительский span checkout 1.8s, запрос без индекса. - В Grafana дашборде
users-service: скачок CPU, GC паузы до 200ms, рост connection pool wait time. - Решение: добавить индекс/батч, увеличить 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, очереди).
Порядок расследования:
- Проверьте БД:
- Латентность запросов (slow query log)
- CPU/RAM базы данных
- Connection pool exhaustion
- Lock contention, deadlocks
- Проверьте внешние API:
- Латентность upstream сервисов
- Rate limiting (429 ответы)
- Circuit breaker открыт?
- Проверьте кэш:
- Cache miss rate (Redis/Memcached)
- Evictions, memory pressure
- Используйте трейсы:
- Откройте 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Проблема: Некорректное сравнение даёт ложные выводы о регрессии.
Правильный подход:
- Одинаковые условия:
- Один и тот же стенд (не dev vs stage)
- Одинаковая версия данных (не пустая БД vs prod-like)
- Одинаковое время суток (нагрузка на shared ресурсы)
- Одинаковый сценарий и параметры k6
- Используйте 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 - Сравнивайте перцентили, а не средние:
- ❌ Неправильно: avg = 150ms vs 180ms (+20%)
- ✅ Правильно: p95 = 300ms vs 450ms (+50%)
- Причина: средние скрывают выбросы
- Учитывайте вариацию:
- Разница <5% — скорее всего шум
- Разница 5-10% — потенциальная проблема, повторите тест
- Разница >10% — вероятная регрессия, расследуйте
- Прогревайте систему (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.
Что произошло:
-
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)
- ✅ p95
-
Триангуляция через 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 baselineactive_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 (через
traceparentheader) -
Открыли 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()
- k6 summary показал: рост latency только на
-
Решение (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; } -
Повторный stress-тест после отката:
- ✅ p99
/api/cart/*: 310ms (baseline восстановлен) - ✅ Postgres
waiting_connections: max 12/50 - ✅ Go-decision: релиз в production
- ✅ p99
Результат 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):
-
Оптимизировали dynamic discounts:
- Batch query вместо N+1 → latency 45ms (было 1.2s)
- Redis cache для hot products (80% cache hit rate)
- Добавили
EXPLAIN ANALYZEв PR checklist для DB queries
-
Обязательный pre-release stress test:
- Любой PR, касающийся cart/checkout → mandatory stress test
- Thresholds привязаны к baseline:
p99 < baseline * 1.15(max 15% regression) - CI блокирует merge, если thresholds не прошли
-
Улучшили observability:
- Автоматическая корреляция: k6 → Prometheus → Jaeger (по
request_id) - Grafana dashboard "Performance Regression" для быстрого анализа
- Alerts на connection pool exhaustion (warning at 70%, critical at 90%)
- Автоматическая корреляция: k6 → Prometheus → Jaeger (по
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:
- k6 обнаруживает проблему (p99 latency spike)
- Grafana показывает симптом (DB connection pool exhausted)
- Slow query log показывает запрос (N+1 query)
- Jaeger показывает execution path (какой span медленный)
- Code review → исправление → повторный тест
✅ Чек-лист завершения урока
После этого урока вы должны уметь:
Чтение результатов k6:
- Интерпретировать
http_req_waitingvshttp_req_connectingдля локализации проблемы - Определять bottleneck по метрикам:
dropped_iterations, CPU генератора, network bandwidth - Знать, когда проблема в генераторе, а когда в сервисе
Интеграция с observability:
- Настроить вывод метрик в Prometheus (
--out prometheus-remote-write) - Добавить
traceparentheader для корреляции с трейсами - Связать метрики 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.