Как запустить load-test¶
Пошаговый рецепт для нагрузочного тестирования backend-сервиса. Делается
перед go-live, после крупных изменений и регулярно для SLO-регрессии.
Reference observability — ../conventions/observability.md.
Profile running сервиса — profile-service.md.
Содержание¶
- Зачем
- Инструмент — k6
- Установка локально
- Простой сценарий — HTTP
- Запуск
- Интерпретация результата
- Сценарии для разных workload'ов
- Что мерить в Prometheus
- Нагрузка Kafka consumer
- Baseline SLA
- Где запускать
- Red flags во время теста
- Chaos + load
- Anti-patterns
- Связанные разделы
Зачем¶
Load-test нужен в трёх случаях:
- Перед go-live нового сервиса. Без нагрузочного теста production-
ready чеклист не закрывается (см.
../checklists/production-ready.md). - После крупных изменений. Новый hot path, новая интеграция с downstream, поменяли DB-схему — проверь, что p95/p99 не ухудшились.
- Регулярно, для SLO-регрессии. Weekly load-test на staging, результаты складываются в Grafana. Если p95 вырос на 20% за неделю — разбираемся, что изменилось.
Load-test без baseline бесполезен. Цифры «1000 RPS, p95 150ms» осмысленны только если ты знаешь, что «на прошлой неделе было 1200 RPS, p95 100ms».
Инструмент — k6¶
Стандарт — grafana/k6. Причины выбора:
- JavaScript для сценариев — читабельнее, чем YAML или кастомные DSL.
- Хорошая интеграция с Grafana / Prometheus (результаты стримятся в remote-write endpoint).
- k8s-operator для cluster-нагрузок, если нужно давить с нескольких нод.
- Активно поддерживается, bug-fix'ы быстрые.
Альтернативы (JMeter, Gatling, vegeta) — не используем. Если появилась причина, почему k6 не подходит для конкретного кейса — обсуждение в команде, issue в docs-репо, тогда добавим вариант сюда.
Установка локально¶
brew install k6 # macOS
winget install k6 # Windows
# Linux — через repo, см. k6.io/docs/get-started/installation
Проверка:
Простой сценарий — HTTP¶
Создай файл load-tests/review-create.js в сервис-репо:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // ramp up
{ duration: '5m', target: 100 }, // sustained
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<200', 'p(99)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.post(
`${__ENV.BASE_URL}/v1/reviews`,
JSON.stringify({ place_id: 42, rating: 5, text: 'test' }),
{
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${__ENV.TOKEN}`,
},
},
);
check(res, { 'status 201': (r) => r.status === 201 });
sleep(1);
}
Разберёмся по кусочкам:
stages— профиль нагрузки: 2 минуты ramp'уемся до 100 VU (virtual users), 5 минут держим 100 VU, 1 минута ramp-down до 0. Итого 8 минут. Короткий тест для smoke-проверки.thresholds— автоматические проверки. Если p95 > 200ms или p99 > 500ms, k6 завершится с ненулевым exit code — CI падает.http_req_failed: ['rate<0.01']— error-rate меньше 1%.__ENV.BASE_URL,__ENV.TOKEN— передаются через env при запуске, не хардкодятся в скрипте.sleep(1)— think-time между iteration'ами, 1 секунда.
Запуск¶
Для CI (self-hosted runner) — тот же вызов, дополнительно с
--out experimental-prometheus-rw=http://prometheus-pushgateway:9090/api/v1/write
для стрима метрик в Prometheus.
Полезные флаги:
--vus 50 --duration 30s— override stages для быстрой ad-hoc проверки.--summary-export=summary.json— выгрузка результатов в JSON для пост-обработки.--http-debug=full— логирование каждого HTTP-вызова (только для отладки сценария; не для нагрузки).
Интерпретация результата¶
Типовой output в конце прогона:
http_req_duration.........: avg=42.3ms p(95)=120ms p(99)=250ms
http_reqs.................: 15000 500/s
iterations................: 14850
http_req_failed...........: 0.34%
Что смотреть:
p(95) < 200ms— хорошо, threshold пройден.http_req_failed > 1%— плохо. Ищи причину: таймауты (timeouts в downstream), 5xx от backend'а, rate-limit Gateway'ем. Смотри логи сервиса за период теста.500/s— throughput. Сравни с expected (если одна реплика должна держать 1000 RPS, а у тебя 500 — узнай, почему).p(99) - p(95)разница большая (например, 250ms vs 120ms) — tail latency, обычно от GC pauses, медленного SQL или замедлившегося downstream'а.
Grafana-dashboard для k6 — отдельная страница: показывает RPS по времени, p50/p95/p99, error rate, и это синхронизировано с backend- метриками (CPU, RAM, DB pool usage) по времени. Так видно, что началось раньше — spike RPS или spike CPU.
Сценарии для разных workload'ов¶
Write-heavy (POST)¶
Осторожно с:
- DB locks.
FOR UPDATE/ advisory locks под высоким write load могут сериализовать запросы и обрушить latency. - FK-constraints и триггеры. Вставка в таблицу с 10 FK и CHECK-triggers — каждый INSERT дорогой.
- Outbox-row. Каждый write генерирует outbox-row → нагрузка на
forwarder → проверь
outbox_forwarder_lag_seconds.
Read-heavy (GET)¶
export default function () {
const id = Math.floor(Math.random() * 10000);
http.get(`${__ENV.BASE_URL}/v1/places/${id}/details`);
sleep(0.5);
}
Смотри:
- Cache hit ratio в Prometheus — если hit rate 99%, load почти не доходит до БД. Если 50% — кэш плохо работает, проверь TTL и invalidation policy.
- Uniform distribution id'ов — если все запросы на один
id=1, измеряешь только кэш. Если хочешь real-world — используй zipf-distribution (90% запросов на 10% популярных id).
Mixed (80/20 read/write)¶
import { group } from 'k6';
export const options = {
scenarios: {
reads: {
executor: 'constant-vus', vus: 80, duration: '5m',
exec: 'readScenario',
},
writes: {
executor: 'constant-vus', vus: 20, duration: '5m',
exec: 'writeScenario',
},
},
};
export function readScenario() { /* ... */ }
export function writeScenario() { /* ... */ }
Два сценария стартуют одновременно, нагрузка ближе к prod-паттерну.
Что мерить в Prometheus¶
k6 показывает только client-side timing. Serverside-проблемы смотри в Prometheus:
| Метрика | Что значит |
|---|---|
http_request_duration_seconds per-endpoint |
p95/p99 на backend (без net latency между k6 и сервисом) |
process_cpu_seconds_total rate |
CPU usage подов под нагрузкой |
process_resident_memory_bytes |
RAM, растёт ли со временем (memory leak) |
pgx_pool_idle_connections / pgx_pool_total_connections |
DB pool usage; near-limit = bottleneck |
outbox_forwarder_publish_duration_seconds |
Kafka publish latency |
outbox_forwarder_lag_seconds |
outbox lag — растёт = forwarder не успевает |
redis_command_duration_seconds |
Redis latency |
http_requests_total{status=~"5.."} rate |
server-side error rate |
Наблюдение: если client видит p95=200ms, а server видит p95=50ms — 150ms это network + client-side overhead. Норма для internet-staging; аномально для in-cluster.
Настройка дешбордов — ../conventions/observability.md
и add-metric-and-alert.md.
Нагрузка Kafka consumer¶
Для consumer-сайда k6-HTTP не подходит напрямую. Сценарий:
- Отдельный publisher-скрипт наливает N сообщений в Kafka topic
(Go-утилита в
cmd/load-producer/main.goилиkcat). - Consumer обрабатывает, замеряем:
- throughput (messages/sec — из consumer log'ов или метрики
messages_processed_total). - lag (Kafka consumer lag через
kafka_consumer_lagиз Prometheus JMX exporter). - p95 handler duration — метрика handler'а.
Цель: throughput > input rate с запасом (input 500 msg/s, consumer держит 2000 msg/s → запас 4×).
Если lag растёт монотонно при стабильном input rate — consumer не справляется (либо медленный handler, либо забита downstream- зависимость, либо pool connection'ов переполнен).
Baseline SLA¶
Первый load-test на staging — это baseline. Дальше каждый тест сравнивается с ним.
Метрики, которые фиксируются в baseline:
- p95 latency per endpoint (в Grafana-annotation).
- Throughput per pod (RPS одного pod'а при target CPU ~70%).
- Resource usage per RPS — CPU% и RAM MB на 100 RPS. Это цифра для capacity planning.
Правило: любой PR с регрессией >10% по baseline требует обоснования. В PR-description — почему регрессия приемлема, или как чинить.
Где запускать¶
| Окружение | Use-case |
|---|---|
| Local (один pod в docker-compose) | Smoke-test: проверить, что скрипт не сломан, endpoint отвечает ожидаемо. 1–2 минуты, 10 VU. |
| Staging (полный stack в кластере) | Основной load-test. Реальный Postgres, Kafka, Redis. Реплики, ресурсы как в проде или уменьшенные пропорционально. |
| CI (self-hosted runner или k6 cloud) | Pre-merge для critical paths. Короткий сценарий (1–2 мин), проверка threshold'ов. Если не проходит — PR блокируется. |
| Prod | Только read-only сценарии, очень осторожно, с предупреждением команды SRE. Write на прод load-тест'ом — запрещено. |
Red flags во время теста¶
- Растущий response time при постоянной нагрузке. p95 начал 100ms, через 5 минут — 300ms, RPS не менялся. Признак resource exhaustion: memory leak, pool fullness, connection leak. Снять heap-profile, смотреть в pprof.
- Stepwise увеличение 5xx. Error rate прыжком с 0.5% до 30% — cascading failure: один downstream упал, timeout'ы поехали на upstream, handler'ы забились.
- Spike в p99 без роста p50. Tail-latency от GC pauses или
slow query. Смотри
go_gc_duration_secondsиpg_stat_statementsза окно теста. - Kafka consumer lag растёт монотонно. throughput < input rate. Нужно либо увеличить параллельность handler'ов, либо оптимизировать handler, либо добавить реплики.
Если видишь red flag — останови тест и разберись, не продолжай «чтобы добить до конца». Дальше результат уже искажён (cascading effects).
Chaos + load¶
Во время sustained load — осознанно ломай что-нибудь:
- Kill одного pod'а consumer'а / api'а → проверь, что трафик перераспределился. Цель: ≤ 30 секунд на восстановление стабильного state'а (зависит от конфигурации readiness probe и rebalance timings).
- Drop Redis на 30 секунд → fail-open работает? (см.
../patterns/idempotent-consumer.md). - DB connection spike:
PGHBA-level заблокировать 50% connection'ов →pgxpoolretry работает, сервис деградирует изящно, не падает в panic.
Chaos-тест раз в релиз или раз в квартал — это зрелость системы. Сервис, который падал при убийстве одного pod'а, не production-ready.
Anti-patterns¶
Load-test первый раз на проде¶
Load на прод без предыдущего тестирования на staging = высокий риск инцидента. Порядок: local → staging → (если нужно и согласовано) prod read-only.
Load-test без baseline¶
«RPS 1000, p95 200ms» — числа в вакууме. Без сравнения с прошлым тестом непонятно, плохо это или хорошо. Первый тест → baseline, все последующие → сравнение.
Игнорирование Prometheus-метрик¶
k6 показал p95=50ms, команда обрадовалась. А в Prometheus за тот же
период pgx_pool_idle=0 постоянно — pool на пределе, следующий тест
уже упрётся. Клиентская сторона всегда оптимистичнее серверной.
Запуск с локальной машины на prod-endpoint¶
Net latency до staging/prod из дома — 50–200ms. Твои p95 — это в основном твой интернет, не backend. Run from in-cluster или close-by runner.
Не предупредить SRE перед load на staging¶
Staging alert'ы срабатывают на load-test (CPU spike, error rate) → SRE разбирается, «что сломалось», тратит время. Всегда в чат
ops: «в X часов запускаю load-test на staging, duration 15 минут».¶
Load-test с одним id'ом¶
Все запросы в одну cache-key. Real-world hit rate ≈ 0% или 100%, не измеряет distribution. Randomize id, используй Zipf или uniform, зависит от сценария.
Включил load, ушёл пить кофе¶
k6 может нагружать downstream, который не твой — внешний FCM, SMS-gateway, S3. Нагрузка на чужие сервисы — это инцидент с их стороны и счёт с твоей. Sanity-check сценария на 1 RPS перед ramp-up'ом.
Связанные разделы¶
../checklists/production-ready.md— load-test обязателен в чеклисте go-live.../conventions/observability.md— Prometheus-метрики, SLO, dashboard'ы.read-logs.md— LogQL для корреляции логов с периодом теста.read-traces.md— Tempo trace для tail-latency investigation.profile-service.md— pprof для CPU/memory profile во время нагрузки.add-metric-and-alert.md— завести метрики, которые load-test потом будет замерять.