У коллег реклама на 500 тысяч в день. Прод лежал полдня. Минус четверть миллиона на ровном месте. С тех пор нагрузку я гоняю не ради графиков. Ищу точку, где система начинает есть выручку.
Маркетплейсы на Чёрную пятницу и 11.11, госуслуги в первый день QR-пропусков: тестировали. Тестировали не то и не так. Локальный curl отдаёт 50 ms. На 1000 RPS те же эндпоинты отвечают секундами и 500-ми. Девять из десяти таких вещей видно только под нагрузкой.
Задержка в секунду при 1000 RPS: каждую секунду реального времени пользователи ждут суммарно тысячу секунд. Это брошенные корзины, не «неудобный UX».
Локальный curl врёт спокойно. Под нагрузкой врёт дороже.
Два вопроса, не четыре типа тестов
Когда запускаю нагрузку, держу два вопроса.
Насколько система быстрая на целевой нагрузке? Это p95, latency, throughput.
Как и когда она ломается, как деградирует и сколько это стоит? Это пределы в деньгах.
Дальше названия уже служебные. Load: сколько обслужим, не начав терять деньги. Stress: где прогорит дно. Spike: хабр-эффект, при каком RPS падаем. Soak: что стоит час пика, если течёт память.
Почему k6
Инструмент вторичен. Главное, чтобы отчёт отвечал «сколько теряем при этой архитектуре», а не «какой красивый график».
k6 живу в репозитории рядом с кодом, ревьюится, гоняется в CI. Gatling и JMeter тоже годятся, если закрывают те же требования: тест как код, один шаг в пайплайне, пороги, отчёт, который понимает не только автор скрипта.
| Принцип / Инструмент | k6 | Apache JMeter | Gatling |
|---|---|---|---|
| Тест как код | JS/TS | XML/GUI | Scala DSL |
| Интеграция в CI/CD | Из коробки | Громоздко | Отлично |
| Бизнес-метрики | Пороги, кастомные метрики | Есть, но сложнее | Богатые отчёты |
| Кому подходит | DevOps, стартапы | QA, унаследованные проекты | JVM-команды, высокие нагрузки |
JMeter рождался для отдельной QA-команды и кликов в GUI. k6 родился там, где разработчик и инженер одно лицо.
Если не k6: JMeter, когда команда не пишет JS. Gatling, когда команда на JVM и сценарии с состоянием. Locust, если уже Python. Artillery, если хватает YAML и нужны WebSocket.
k6 закрывает большую часть случаев: знакомый JS, CLI, Docker, CI, выгрузка в Grafana Cloud.
Порядок, без которого график бесполезен
Сначала считаю стоимость 1% ошибок и одной секунды latency на ключевом сценарии. Потом выбираю инструмент. Потом два-три денежных потока и гипотезы, где сломается. Потом профиль с плавным ростом и пороги SLA. Потом сверка метрик теста и железа. Потом отчёт: гипотеза, данные, действие, повторный прогон.
Сервис такси, сценарий «поиск → заказ → оплата». Из аналитики: p95 поиска машин выше 3 s, конверсия падает на 15%. Пик 1000 RPS: 150 потерянных запросов в секунду. Средний заказ $20, до заказа доходит 10%. Это около $300 выручки в минуту. Тест показал: геокодер трещит на 800 RPS. Час пика до фикса: около $2400.
Перед прогоном команда пишет, где система сломается первой. «БД упрётся в IOPS на 300 RPS». «Кеш кончится на 500 VU». «Платёжный шлюз начнёт отдавать таймауты». Тест эти фразы либо хоронит, либо подтверждает.
В первом прогоне трогаю два-три потока, которые дают большую часть выручки или пик. Авторизация и дашборд. Поиск, корзина, чекаут. Создание записи и список.
import http from "k6/http";
import { group, check } from "k6";
export default function () {
group("User Journey: Login → Dashboard → Logout", () => {
// 1. Логин
const loginRes = http.post("https://api.example.com/auth/login", {
email: "test@example.com",
password: "password123",
});
check(loginRes, { "login success": (r) => r.status === 200 });
const token = loginRes.json("token");
// 2. Получение дашборда
const headers = { Authorization: `Bearer ${token}` };
const dashRes = http.get("https://api.example.com/dashboard", { headers });
check(dashRes, { "dashboard loaded": (r) => r.status === 200 });
// 3. Логаут
http.post("https://api.example.com/auth/logout", null, { headers });
});
}Десять тысяч VU сразу дают ложные падения. Нарастаю ступенями.
export const options = {
stages: [
{ duration: "1m", target: 50 }, // Прогрев
{ duration: "3m", target: 50 }, // Baseline
{ duration: "2m", target: 150 }, // Рост (пик часы)
{ duration: "5m", target: 150 }, // Пиковая нагрузка
{ duration: "2m", target: 300 }, // Стресс-тест
{ duration: "3m", target: 300 }, // Удержание стресса
{ duration: "2m", target: 0 }, // Плавный спуск
],
};Пороги превращают прогон в проверку SLA.
export const options = {
thresholds: {
// Latency
http_req_duration: [
"p(50)<200", // Медиана < 200ms
"p(95)<500", // 95 перцентиль < 500ms
"p(99)<1000", // 99 перцентиль < 1s
],
// Доступность
http_req_failed: ["rate<0.01"], // < 1% ошибок
// Throughput
http_reqs: ["rate>100"], // Минимум 100 RPS
// Кастомные проверки
"checks{type:auth}": ["rate>0.99"], // 99% успешных логинов
},
};Гоняю в CI, чтобы условия совпадали. Локально только отладка.
После прогона смотрю p50/p95/p99, ошибки по кодам, RPS, сколько VU выдержали. Рядом: CPU и память, запросы в БД, cache hit, сеть.
Если p95 меньше 500 ms, а p99 равен 5 s, хвосты длинные. Ищите медленные запросы в БД или таймауты внешних API.
Какие числа решают
На одном экране Grafana: latency против throughput, треугольник latency / error rate / throughput, насыщение CPU, IO и сети.
p95: SLA для основной массы. p99: совесть. Если p95 200 ms, а p99 2000 ms, есть ядро и хвост. Хвост обычно медленные запросы, блокировки, GC.
1% ошибок на 1000 RPS: 10 неудачных запросов в секунду. За десять минут теста: 6000 ошибок. Это инцидент, не погрешность.
RPS сам по себе ничего не говорит. Смотрю, как ведёт себя latency при росте. Если взлетает, нашли потолок узкого места.
RPS при 90-100% CPU или IOPS: треснет железо, не код.
Apdex без порогов Satisfied / Tolerating / Frustrated: пустое число. Для трейдинга Satisfied ближе к 50 ms, для CMS к 500 ms. Формула: (Satisfied + Tolerating/2) / Total.
CPU выше 80% долго: узкое место. Память растёт линейно: утечка. Пул соединений исчерпан: тюним. Низкий cache hit: стратегия кеша врёт.
Как ищу корень:
- Высокий p99 и пики CPU/IO на БД: slow query log,
EXPLAIN ANALYZE, pt-query-digest. - 5xx без роста CPU: лимиты подключений, пулы, таймауты внешних API.
- Throughput упёрся, CPU низкий: глобальные блокировки, сеть, rate limit.
- RPS растёт, latency скачет, cache hit падает: промахи кеша и TTL.
Отчёт, который сразу становится бэклогом
# Нагрузочный тест: API v2.0
## Цель
Проверить готовность API к увеличению нагрузки в 3x (с 500 до 1500 RPS).
## Сценарий
- User Journey: Login → Get Dashboard → Logout
- Профиль нагрузки: 50 → 150 → 300 VU
- Длительность: 18 минут
## Результаты
### Прошли пороги
- p95 latency: 420ms (порог: < 500ms)
- Error rate: 0.3% (порог: < 1%)
- Throughput: 1200 RPS (ожидалось: 1000 RPS)
### Проблемы
- p99 latency: 3.2s (порог: < 1s)
- CPU на DB: 92% на пике
- Slow queries: `/users` эндпоинт → N+1 запросы
## Узкие места (по приоритету)
1. **Database N+1 queries:** `/users` делает 50+ запросов к БД
- **Действие:** добавить eager loading
- **ETA:** 2 дня
- **Эффект:** p99 latency → < 800ms
2. **CPU на DB:** достигает 92% на 300 VU
- **Действие:** увеличить инстанс (4 → 8 vCPU)
- **ETA:** 1 день
- **Эффект:** запас до 500 VU
3. **Cache hit rate:** только 65% для `/dashboard`
- **Действие:** увеличить TTL кеша с 5 мин до 15 мин
- **ETA:** 1 день
- **Эффект:** снизить нагрузку на DB на 20%
## Рекомендации
- Оптимизировать N+1 queries **критично перед релизом**
- Увеличить DB инстанс **желательно**
- Настроить кеш **можно отложить**
## Гипотеза и верификация (обязательный блок для каждой проблемы)
- **Проблема:** p99 latency: 3.2s на `/users`
- **Гипотеза:** в логах БД видно N+1 запросов на этот эндпоинт
- **Действие:** добавить eager loading
- **Верификация:** повторить тот же тест и подтвердить p99 < 800ms
## Графики
[Attach Grafana dashboard screenshot]
## Next Steps
- [ ] Фикс N+1 queries
- [ ] Повторный тест после оптимизации
- [ ] Stress test до поиска предела (500+ VU)Цифры без следующего действия: музей.
Типичный бесполезный отчёт: «достигли 5000 RPS, p95 2.3 s, оптимизировать БД». Неясно, можно ли жить на 5000 RPS (при таком p95 уже нет), что именно чинить и стоит ли это денег. Нужен ответ: при каком RPS укладываемся в p95, сколько теряем выше порога и какой компонент ломается первым.
Грабли
Стейджинг слабее прода, данные синтетические, внешние API заглушены. Результаты чистая вода. Окружение должно быть близко к проду, данные репрезентативные, зависимости либо подняты, либо это настоящий прод с согласия владельцев.
Упёрлись в Cloudflare или Nginx, не в код. Для IP раннера лимиты поднимают или отключают.
Первые запросы медленные из-за холодного кеша и JIT. Нужен прогрев:
export const options = {
stages: [
{ duration: "1m", target: 10 }, // Warm-up
{ duration: "5m", target: 100 }, // Основной тест
],
};k6 зелёный, сервер всё равно падает: нет CPU, памяти, диска, сети рядом с метриками теста.
Один прогон на два часа: непонятно, в какую минуту сломалось. Лучше микротесты: baseline 50 VU 5 минут, пик 150 VU 10 минут, stress 300 VU 5 минут.
CI
load-test:
stage: test
image: grafana/k6:latest
script:
- k6 run --out json=results.json tests/load/api.js
artifacts:
reports:
junit: results.json
only:
- main
when: manual # Запускаем вручную перед релизомname: Load Test
on:
workflow_dispatch: # Ручной запуск
jobs:
load-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run k6 test
uses: grafana/k6-action@v0.3.1
with:
filename: tests/load/api.js
cloud: true
token: ${{ secrets.K6_CLOUD_TOKEN }}На каждый коммит нагрузку не гоняю. Дорого и медленно. Перед релизом или ночью по расписанию.
Деньги
День-два инженера на тест. Часы простоя в пик стоят на порядок больше. Формула инцидента простая:
(Средний чек × Конверсия × Доля затронутого трафика × Длительность сбоя) + (Стоимость поддержки × Количество тикетов) + (Стоимость репутации × Коэффициент потерь)
Тест подставляет живые числа: при 300 RPS и 10% ошибок в чекауте теряем X в минуту.
Критично перед распродажей, запуском контента, массовыми выплатами, упоминанием в прессе. Вопросы те же: какой процент ошибок готовы терпеть в абсолютных запросах, при каком p95 падает конверсия, дешевле ли поднять архитектуру или заложить 0.5% потерь в план.
Первый серьёзный прогон почти всегда находит два-три узких места, о которых не знали. Лучше узнать так, чем из писем после пика.
См. также:


