Лекция №8. Тестирование производительности: Нагрузочное, стресс-тестирование, стабильность и масштабируемость системы.
§8.1. Зачем нужно тестирование производительности?
Вы наверняка слышали эту фразу от разработчиков (или даже сами так говорили): «У меня всё работает!».
И это действительно так. На локальной машине разработчика, где:
- База данных пустая (или почти пустая).
- Нет других пользователей.
- Сервер и база данных крутятся на одном мощном компьютере.
- Сеть работает мгновенно.
...всё летает. Запросы обрабатываются за миллисекунды, страницы открываются моментально. Но в реальном мире всё по-другому:
| Характеристика | На машине разработчика | В продакшене |
|---|---|---|
| Пользователи | 1 пользователь (разработчик) | 100, 1000, 10000 пользователей одновременно |
| Объем данных | Пустая база данных (10 записей) | Миллионы записей в БД |
| Нагрузка | Запросов в секунду: 1-2 | Запросов в секунду: 100-500 |
| Архитектура | Сервер и БД на одном компьютере | Сервер и БД на разных машинах (сетевые задержки) |
| Кеширование | Нет кеширования | Есть кеширование (и оно может не работать) |
Тестирование производительности
— это проверка того, как система ведет себя под нагрузкой, когда на нее одновременно приходит много запросов.
В предыдущих лекциях мы занимались функциональным тестированием:
| Что мы проверяли | Пример |
|---|---|
| Правильно ли работает логика? | При заказе списываются деньги? |
| Правильно ли сохраняются данные? | Заказ появился в БД? |
| Правильно ли работает интерфейс? | Кнопка «Купить» кликается и ведет на оплату? |
Тестирование производительности — это нефункциональное тестирование. Оно отвечает на вопрос: «Как быстро и надежно работает система под нагрузкой?».
| Что мы проверяем | Пример |
|---|---|
| Сколько запросов в секунду выдерживает сервер? | 100 RPS или 1000 RPS? (RPS – количество запросов в секунду) |
| Как быстро отвечает сервер? | 50 мс или 2 секунды? |
| Сколько пользователей может одновременно работать? | 100 или 10000? |
| Что происходит, когда нагрузка превышает норму? | Система падает или замедляется? |
| Как система работает долгое время? | Не падает ли через час из-за утечки памяти? |
Простая аналогия:
- Функциональное тестирование — это проверка, что автомобиль заводится, едет, тормозит, поворачивает.
- Тестирование производительности — это проверка, как автомобиль ведет себя на трассе на скорости 150 км/ч, резко тормозит, едет в гору с полным багажником.
Основная цель тестирования производительности
– система не должна упасть, когда придут пользователи. Самая страшная ситуация для бизнеса — это когда пользователи приходят, а система не работает.
Представьте интернет-магазин перед Черной пятницей. Обычно у них 1000 пользователей в день. В Черную пятницу — 10000.
| Сценарий | Последствия |
|---|---|
| Сайт падает | Пользователи уходят к конкурентам. Потеря продаж. Репутационные потери. |
| Сайт работает, но медленно | Пользователи раздражаются и уходят. Потеря продаж. |
| Сайт работает, но данные теряются | Заказы создаются, но не сохраняются. Пользователи думают, что заказали, а заказа нет. Адский колл-центр. |
Мы должны ответить на три главных вопроса:
1. «Выдержит ли система ожидаемую нагрузку?» (Нагрузочное тестирование).
2. «Как система ломается?» (Стресс-тестирование).
3. «Как долго система может работать без сбоев?» (Тестирование стабильности).
Когда мы проводим тесты производительности, мы измеряем четыре ключевые метрики.
1. RPS (Requests Per Second) — Запросов в секунду
RPS
– количество запросов, которые сервер может обработать за одну секунду.
Почему это важно
Чем выше RPS, тем больше пользователей может одновременно работать с системой.
Пример:
- Запрос к простому API (
GET /users/1) — 1000 RPS. - Запрос к сложному API (
POST /orders с расчетом скидки и отправкой email) — 50 RPS.
2. TTFB (Time To First Byte) — Время до первого байта
TTFB
– время от отправки запроса до получения первого байта ответа от сервера.
Что входит в TTFB:
- Время на установку соединения (TCP handshake).
- Время на обработку запроса сервером (пока сервер не начал отправлять ответ).
Почему это важно
Пользователь начинает ждать ответа с момента отправки запроса. Если TTFB большой — пользователь чувствует, что система «тормозит».
| TTFB (Time to First Byte) | Впечатление пользователя |
|---|---|
| < 100 мс | Отлично (мгновенно) |
| 100-300 мс | Хорошо (быстро) |
| 300-500 мс | Терпимо (заметно, но не критично) |
| > 500 мс | Медленно (пользователь начинает раздражаться) |
| > 2000 мс | Очень медленно (пользователь уходит) |
3. Latency (Задержка) — Полное время ответа
Latency
– полное время от отправки запроса до получения полного ответа.
Чем отличается от TTFB:
- TTFB — время до первого байта.
- Latency — время до последнего байта.
Почему это важно
Пользователь ждет не первый байт, а полный ответ. Если ответ большой (например, HTML-страница), Latency может быть значительно больше TTFB.
Как измеряют:
- p50 (медиана): 50% запросов укладываются в это время.
- p95: 95% запросов укладываются в это время.
- p99: 99% запросов укладываются в это время.
Почему важны процентили, а не среднее? Представьте:
- 99 запросов отработали за 100 мс.
- 1 запрос отработал за 10 секунд (из-за проблем с сетью).
wireframe
Среднее = (99 × 100 + 10000) / 100 = 199 мс.
Выглядит неплохо!.
Но p99 = 10000 мс. Это значит, что 1% пользователей ждали 10 секунд!
В тестах производительности мы всегда смотрим на p95 и p99, а не на среднее.
4. Error Rate (Процент ошибок)
Error Rate
– процент запросов, которые завершились ошибкой (статус-код 500, 503, 504, таймаут).
Почему это важно
Любая ошибка — это плохо. Если Error Rate > 0%, система уже начала «сыпаться».
| Error Rate | Что это значит |
|---|---|
| 0% | Идеально. Все запросы обработаны. |
| < 1% | Приемлемо (но нужно разобраться) |
| 1-5% | Плохо. Пользователи замечают проблемы. |
| > 5% | Катастрофа. Система фактически не работает. |
§8.2. Классификация тестов производительности: какой тест когда нужен
Представьте, что вы — врач. К вам пришел пациент. Вы не можете просто сказать «он болен» — нужно понять, что именно с ним не так. Для этого есть разные обследования: термометр, анализ крови, УЗИ, МРТ. Каждое обследование дает свой ответ на свой вопрос.
Точно так же в тестировании производительности существуют разные виды тестов. Каждый из них отвечает на свой вопрос:
| Вид теста | Какой вопрос задает |
|---|---|
| Нагрузочный | «Выдержит ли система обычную нагрузку?» |
| Стрессовый | «Где у системы точка отказа?» |
| Стабильности | «Не умрет ли система от усталости?» |
| Spike-тест | «Выдержит ли система внезапный удар?» |
Давайте разберем каждый из них подробно.
Вид тестирование №1. Нагрузочное тестирование (Load Testing)
Как это работает:
1. Вы узнаете у бизнеса: «Сколько пользователей у нас будет в пик?».
2. Например, бизнес говорит: «В Черную пятницу ожидаем 5000 пользователей одновременно».
3. Вы создаете нагрузку в 5000 виртуальных пользователей.
4. Проверяете: «Все ли запросы успешно обрабатываются? Укладываемся ли мы в целевое время ответа?».
Когда применяется:
- Перед крупными релизами.
- Перед сезонными распродажами (Черная пятница, Новый год).
- Когда бизнес меняет требования по количеству пользователей.
Что мы ищем:
-
Узкие места — где система тормозит под нагрузкой:
- База данных слишком медленная?
- Внешний API не успевает отвечать?
- Сетевые задержки?
- Деградацию — как меняется время ответа при росте нагрузки.
Вид тестирование №2. Стресс-тестирование (Stress Testing)
Как это работает:
1. Вы знаете, что нормальная нагрузка — 5000 пользователей.
2. Вы начинаете с 1000 пользователей и постепенно увеличиваете: 2000 → 3000 → 4000 → 5000 → 6000 → 7000...
3. В какой-то момент система начинает выдавать ошибки (500, 503, таймауты).
4. Это и есть точка отказа.
Зачем это нужно:
- Узнать запас прочности. Если норма — 5000 пользователей, а система падает только на 15000 — у вас есть тройной запас. Отлично! Если падает на 5500 — очень плохо, один лишний пользователь и всё упадет.
- Понять, как именно система ломается. Это поможет инженерам написать более устойчивый код.
- Проверить, восстанавливается ли система после падения. Если нагрузка упала, система возвращается в рабочее состояние или нужен перезапуск?
Вид тестирование №3. Тестирование стабильности (Soak / Endurance Testing)
Как это работает:
1. Вы создаете нагрузку, близкую к пиковой (например, 4000 пользователей при пике 5000).
2. Вы держите эту нагрузку в течение длительного времени — 4, 8, 24 часа.
3. Вы наблюдаете, что происходит с системой со временем.
Какие проблемы мы ищем:
| Проблема | Что это значит | Как проявляется |
|---|---|---|
| Утечка памяти (Memory Leak) | Программа не освобождает память, и она постепенно заканчивается | Время ответа растет, потом система падает с OutOfMemoryException |
| Утечка соединений | Соединения с БД не закрываются, и пул соединений исчерпывается | Через час система перестает отвечать на запросы к БД |
| Деградация кеша | Кеш заполняется и перестает работать эффективно | Время ответа медленно растет от часа к часу |
| Проблемы с логами | Логи заполняют диск, и системе некуда писать | Диск заполняется, система падает |
Когда применяется:
- Для систем, которые работают 24/7 без перезапуска (банковские системы, биржи, платежные шлюзы).
- Для систем с интенсивным логированием.
- Для систем, которые активно используют кеш.
Вид тестирование №4. Spike-тестирование (Внезапный скачок нагрузки)
Как это работает:
1. У вас была низкая нагрузка (100 пользователей).
2. Внезапно, за 5 секунд, нагрузка вырастает до 5000 пользователей.
3. Вы наблюдаете: система падает? Восстанавливается? Как быстро?
Сценарии, где это важно:
- Продажа билетов: билеты на концерт поступают в продажу. В 10:00 тысячи людей одновременно заходят на сайт.
- Акция в магазине: в 12:00 начинается распродажа. Наплыв пользователей за 1 минуту.
- Новостной портал: вышла новость, на сайт заходят в 10 раз больше пользователей, чем обычно.
Что мы проверяем:
- Умеет ли система масштабироваться быстро?
- Есть ли в системе очереди (тогда нагрузка распределяется, и система не падает)?
- Как долго система восстанавливается после падения?
Сравнительная таблица видов тестирования представлена ниже:
| Вид теста | Что делаем | Какую нагрузку | Что ищем | Когда применяем |
|---|---|---|---|---|
| Нагрузочный | Проверяем нормальную работу | Ожидаемую (пиковую) | Узкие места, деградацию | Регулярно, перед релизами |
| Стрессовый | Находим точку отказа | ЗА пределами нормы | Запас прочности, поведение при падении | Для критичных систем, перед высокими нагрузками |
| Стабильности | Проверяем долговременную работу | Пиковую (длительно) | Утечки памяти, деградацию | Для 24/7 систем |
| Spike | Проверяем внезапный скачок | Резко возрастающую | Способность к масштабированию | Для систем с пиковыми нагрузками |
Для того, чтобы выбрать правильный тест, необходимо бизнесу задать примерно следующие вопросы:
| Ответ бизнеса | Какой тест нужен |
|---|---|
| «У нас обычный сайт, 1000 посетителей в день, нагрузка равномерная» | Нагрузочный тест |
| «Мы запускаем акцию, ожидаем в 3 раза больше пользователей» | Стрессовый тест + Spike |
| «У нас билетная система, в 10:00 наплыв пользователей» | Spike-тест |
| «Наша система работает без перезапуска неделями» | Тест стабильности |
Опытные инженеры обычно проводят тесты в следующем порядке:
1. Smoke-тест (дымовой тест) — минимальная нагрузка (1-2 запроса), чтобы убедиться, что тестовый скрипт работает и система отвечает.
2. Нагрузочный тест — проверяем, что система работает в норме.
3. Стрессовый тест — увеличиваем нагрузку до падения.
4. Тест стабильности — проверяем, что система не умирает от усталости.
5. Spike-тест — проверяем внезапные скачки.
§8.3. Пишем нагрузочный тест на C# с NBomber
В прошлых лекциях мы писали тесты на C# с использованием xUnit. Мы знаем синтаксис, умеем работать с HttpClient, знаем, как отлаживать код в Visual Studio.
NBomber
— это фреймворк для нагрузочного тестирования, который позволяет писать тесты на обычном C#. Вам не нужно учить новый язык или сложный DSL (Domain Specific Language). Вы просто пишете C# код, который отправляет запросы к вашему API.
Почему это круто для нас:
| Возможность | Что это значит на практике |
|---|---|
| Знакомый C# | Вы пишете тесты на том же языке, что и ваше приложение. Не нужно учить Python, JavaScript или Go. |
| Отладка в IDE | Вы можете поставить breakpoint в Visual Studio и отлаживать нагрузочный тест как обычную программу. |
| Мощный API | NBomber предоставляет простые и понятные методы для создания HTTP-запросов, управления нагрузкой и сбора статистики. |
| Гибкость | NBomber не привязан к HTTP. Вы можете тестировать базы данных, WebSockets, gRPC и любые другие системы. |
В NBomber есть три ключевых понятия, которые нужно запомнить:
1. Scenario (Сценарий)
Сценарий
— это описание того, что делает один виртуальный пользователь. Это как «пользовательский путь» — например, «авторизоваться → поискать товар → добавить в корзину → оформить заказ».
Если рассмотреть абстрактную аналогию, то сценарий — это как инструкция для актера в театре: «Ты заходишь на сцену, говоришь фразу, садишься на стул». Каждый виртуальный пользователь (копия сценария) выполняет эту инструкцию.
2. Step (Шаг)
Шаг
– одно действие внутри сценария. Например, «отправить GET-запрос на /api/products» или «отправить POST-запрос на /api/orders».
Если расмотреть абстрактную аналогию и если сценарий — это вся пьеса, то шаг — это одно действие: «открыть дверь», «произнести монолог».
3. Load Simulation (Симуляция нагрузки)
Симуляция нагрузки
– настройка того, как много виртуальных пользователей будут выполнять сценарий. Например, «10 запросов в секунду в течение 30 секунд».
Если рассмотреть абстрактную аналогию и если сценарий — это инструкция для актера, то симуляция нагрузки — это сколько актеров одновременно выходят на сцену и как часто.
Давайте начнем с самого простого примера, чтобы увидеть структуру. Этот тест ничего не делает, кроме как ждет 1 секунду:
csharp
using NBomber.CSharp;
var scenario = Scenario.Create("hello_world_scenario", async context =>
{
// Здесь будет логика виртуального пользователя
// NBomber замерит, сколько времени это занимает
await Task.Delay(1_000); // Имитация работы
return Response.Ok(); // Сообщаем, что шаг успешен
})
.WithoutWarmUp() // Отключаем "прогрев" для простоты
.WithLoadSimulations(
Simulation.Inject(rate: 10, // 10 запросов
interval: TimeSpan.FromSeconds(1), // в секунду
during: TimeSpan.FromSeconds(30)) // в течение 30 секунд
);
NBomberRunner.RegisterScenarios(scenario).Run();
Теперь разберем код:
| Строка | Что делает |
|---|---|
Scenario.Create("hello_world_scenario", async context => { ... }) | Создает сценарий с именем «hello_world_scenario». Внутри — что делает виртуальный пользователь. |
await Task.Delay(1_000) | Имитирует работу (например, запрос к БД или API). NBomber замерит время выполнения. |
return Response.Ok() | Сообщает, что шаг выполнен успешно. Если вернуть Response.Fail() — тест засчитает ошибку. |
WithLoadSimulations(...) | Настраивает нагрузку. Simulation.Inject — это один из способов. |
Теперь давайте заменим Task.Delay на реальный HTTP-запрос. Для этого нам понадобится установить пакет NBomber.Http:
bash
dotnet add package NBomber.Http
А теперь — сам тест. Отправляем GET-запрос к API:
csharp
using NBomber.CSharp;
using NBomber.Http.CSharp;
var httpClient = Http.CreateDefaultClient(); // Создаем HttpClient
var scenario = Scenario.Create("get_products_scenario", async context =>
{
// 1. Формируем HTTP-запрос
var request = Http.CreateRequest(
"GET", "https://localhost:5001/api/products"
).WithHeader("Accept", "application/json");
// 2. Отправляем запрос и получаем ответ
var response = await Http.Send(httpClient, request);
// 3. Возвращаем результат
// Если статус-код 200-299 — Response.Ok(),
// иначе — Response.Fail()
return response;
})
.WithoutWarmUp()
.WithLoadSimulations(
Simulation.Inject(rate: 50, // 50 запросов
interval: TimeSpan.FromSeconds(1), // в секунду
during: TimeSpan.FromSeconds(30)) // в течение 30 секунд
);
NBomberRunner
.RegisterScenarios(scenario)
.Run();
Разбор HTTP-части:
1. Http.CreateDefaultClient() — создает HttpClient с настройками по умолчанию (таймауты, заголовки).
2. Http.CreateRequest("GET", "...") — создает объект HTTP-запроса.
3. .WithHeader("Accept", "application/json") — добавляет заголовок к запросу .
4. Http.Send(httpClient, request) — отправляет запрос и возвращает ответ. NBomber автоматически замерит:
- Сколько времени занял запрос (Latency).
- Успешно ли завершился запрос (по статус-коду).
- Размер переданных данных.
Часто нужно отправить данные на сервер (создать заказ, добавить пользователя). Вот как это делается:
csharp
var scenario = Scenario.Create("create_order_scenario", async context =>
{
// 1. Создаем объект с данными заказа
var orderData = new
{
ProductId = 1,
Quantity = 2,
CustomerEmail = "test@example.com"
};
// 2. Формируем POST-запрос с JSON-телом
var request = Http.CreateRequest(
"POST", "https://localhost:5001/api/orders"
).WithHeader("Content-Type", "application/json")
.WithJsonBody(orderData); // Автоматически сериализует в JSON!
// 3. Отправляем запрос
var response = await Http.Send(httpClient, request);
return response;
});
Что делает WithJsonBody
- автоматически превращает ваш C#-объект в JSON-строку и добавляет правильный заголовок
Content-Type: application/json.
В примерах выше мы использовали .WithoutWarmUp(). Но по умолчанию в NBomber есть WarmUp (прогрев).
Warpup (прогрев)
— период перед основным тестом, когда система «прогревается».
Зачем нужен прогрев:
| Без прогрева | С прогревом |
|---|---|
| Первый запрос может быть медленным (JIT-компиляция, инициализация соединений) | Система уже прогрета, результаты отражают реальную производительность |
| Результаты искажены «холодным стартом» | Результаты более точные и стабильные |
По умолчанию WarmUp длится 30 секунд. Вы можете изменить его:
csharp
.WithWarmUpDuration(TimeSpan.FromSeconds(60)) // 60 секунд прогрева
После запуска теста NBomber выводит в консоль отчёт со статистикой:
wireframe
======================= STATS =========================
Scenario: get_products_scenario
- ok count: 1500
- fail count: 0
- RPS: 50
- Latency (ms):
- min: 12.3
- mean: 45.6
- max: 234.5
- p50: 42.1
- p95: 89.3
- p99: 156.7
=======================================================
Что мы видим:
- RPS (Requests Per Second): сколько запросов в секунду обработал сервер.
- Latency (p50, p95, p99): время ответа в процентилях.
- ok count / fail count: сколько запросов успешных, сколько с ошибками.
§8.4. Управление нагрузкой в NBomber: от 0 до отказа
В предыдущем параграфе мы научились отправлять запросы к API с помощью NBomber.
Мы использовали Simulation.Inject.
Simulation.Inject
— простая модель нагрузки с постоянным количеством запросов в секунду.
Главная задача нагрузочного тестирования
— найти точку отказа (Breaking Point). Это момент, когда система перестает справляться с нагрузкой и начинает выдавать ошибки.
Как мы это делаем: мы начинаем с маленькой нагрузки и постепенно увеличиваем её, пока система не сломается.
В NBomber есть два основных способа задать нагрузку:
1. Simulation.Inject — Постоянная нагрузка
Вы задаете фиксированное количество запросов в секунду, и NBomber держит эту нагрузку в течение указанного времени.
csharp
Simulation.Inject(
rate: 50, // 50 запросов
interval: TimeSpan.FromSeconds(1), // в секунду
during: TimeSpan.FromSeconds(30) // в течение 30 секунд
)
Когда использовать
Когда вы хотите проверить систему под стабильной нагрузкой (например, нагрузочный тест).
2. Simulation.RampingInject — Постепенно растущая нагрузка
Вы задаете начальную нагрузку, конечную нагрузку и время, за которое нагрузка должна вырасти. NBomber плавно увеличивает количество запросов.
csharp
Simulation.RampingInject(
rate: 1_000, // Конечная нагрузка: 1000 RPS
interval: TimeSpan.FromSeconds(1), // Шаг: 1 секунда
during: TimeSpan.FromSeconds(120), // Общая длительность: 120 секунд
startRate: 10 // Начальная нагрузка: 10 RPS
)
Когда использовать
Когда вы ищете точку отказа. Нагрузка растет постепенно, и вы видите, на каком уровне начинаются ошибки.
Давайте напишем тест, который ищет точку отказа. Мы будем постепенно увеличивать нагрузку с 10 до 1000 RPS в течение 120 секунд:
csharp
using NBomber.CSharp;
using NBomber.Http.CSharp;
var httpClient = Http.CreateDefaultClient();
var scenario = Scenario.Create("stress_test_scenario", async context =>
{
var request = Http.CreateRequest(
"GET", "https://localhost:5001/api/products"
);
var response = await Http.Send(httpClient, request);
return response;
})
.WithLoadSimulations(
// Начинаем с 10 RPS, за 120 секунд увеличиваем до 1000 RPS
Simulation.RampingInject(
rate: 1_000, // Конечная нагрузка: 1000 RPS
interval: TimeSpan.FromSeconds(1), // Каждую секунду
during: TimeSpan.FromSeconds(120), // В течение 120 секунд
startRate: 10 // Начальная нагрузка: 10 RPS
)
);
NBomberRunner
.RegisterScenarios(scenario)
.Run();
Что произойдет во время теста:
wireframe
Время 0 сек: RPS = 10 → система работает, ошибок нет
Время 10 сек: RPS = 92 → система работает, ошибок нет
Время 20 сек: RPS = 175 → система работает, ошибок нет
Время 30 сек: RPS = 257 → система работает, ошибок нет
Время 40 сек: RPS = 340 → система работает, ошибок нет
Время 50 сек: RPS = 422 → время ответа увеличивается
Время 60 сек: RPS = 505 → время ответа растет
Время 70 сек: RPS = 587 → ПОЯВЛЯЮТСЯ ПЕРВЫЕ ОШИБКИ! (точка отказа)
Время 80 сек: RPS = 670 → ошибок становится больше
Время 90 сек: RPS = 752 → система почти не отвечает
На 70-й секунде, когда нагрузка достигла примерно 587 RPS, появились первые ошибки. Это и есть Breaking Point.
Часто мы не знаем, какую максимальную нагрузку выдержит система. Поэтому используется ступенчатый подход:
csharp
var scenario = Scenario.Create("step_stress_test", async context =>
{
var request = Http.CreateRequest(
"GET", "https://localhost:5001/api/products"
);
var response = await Http.Send(httpClient, request);
return response;
})
.WithLoadSimulations(
// Ступень 1: 100 RPS → 2 минуты
Simulation.Inject(100, TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(120)),
// Ступень 2: 200 RPS → 2 минуты
Simulation.Inject(200, TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(120)),
// Ступень 3: 400 RPS → 2 минуты
Simulation.Inject(400, TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(120)),
// Ступень 4: 800 RPS → 2 минуты
Simulation.Inject(800, TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(120)),
// Ступень 5: 1600 RPS → 2 минуты
Simulation.Inject(1600, TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(120))
);
Что мы увидим:
| Ступень | Нагрузка | Результат |
|---|---|---|
| 1 | 100 RPS | Все запросы успешны |
| 2 | 200 RPS | Все запросы успешны |
| 3 | 400 RPS | Время ответа увеличилось |
| 4 | 800 RPS | Появились ошибки (точка отказа) |
| 5 | 1600 RPS | Много ошибок, система «лежит» |
Есть важное правило
Таймаут в нагрузочном тесте должен быть МЕНЬШЕ, чем таймаут реального пользователя.
| Если таймаут теста слишком большой | Если таймаут теста правильно настроен |
|---|---|
| Тест будет долго ждать медленные запросы | Тест быстро «убивает» медленные запросы |
| Результаты искажены (включают очень медленные запросы) | Результаты отражают реальное поведение |
| Не видно проблемы с производительностью | Видно, что система не укладывается в SLA |
SLA (Service Level Agreement)
— это официальный договор, в котором прописано, сколько времени система обязана тратить на ответ (латентность), сколько запросов она обязана выдерживать в секунду (пропускная способность) и какой процент времени она обязана работать (доступность).
Как это выглядит в коде:
csharp
var httpClient = Http.CreateDefaultClient(
client =>
{
// ⚠️ ВАЖНО: таймаут в тесте = 3 секунды
// Реальный пользователь ждет 5-10 секунд
client.Timeout = TimeSpan.FromSeconds(3);
}
);
var scenario = Scenario.Create("test_with_timeout", async context =>
{
var request = Http.CreateRequest(
"GET", "https://localhost:5001/api/slow"
);
var response = await Http.Send(httpClient, request);
// Если запрос выполняется дольше 3 секунд ->
// будет ошибка (Timeout)
return response;
});
Что произойдет:
1. Запрос отправлен.
2. Сервер не отвечает 3 секунды.
3. HttpClient выбрасывает исключение TaskCanceledException.
4. NBomber засчитывает это как ошибку (Fail Count +1).
Почему таймаут должен быть быстрее пользователя:
| Таймаут пользователя | Таймаут в тесте | Почему |
|---|---|---|
| 10 секунд (пользователь ждет) | 5 секунд | Если сервер отвечает за 6 секунд — это уже проблема, которую нужно зафиксировать. |
| 30 секунд (пользователь ждет) | 10 секунд | Нагрузочный тест должен быстро находить проблемы. |
Для интерпретирования результатов с таймаутами существует 3 сценария.
Сценарий №1: Таймаутов нет (0% ошибок)
| Показатель | Значение |
|---|---|
| RPS | 500 |
| Latency (p95) | 150 мс |
| Ошибки | 0% |
Вывод
Система работает отлично. Точка отказа еще не достигнута.
Сценарий №2: Появились таймауты (>0% ошибок)
| Показатель | Значение |
|---|---|
| RPS | 587 |
| Latency (p95) | 2500 мс |
| Ошибки (таймауты) | 5% |
Вывод
Система достигла предела. 5% запросов не укладываются в таймаут 3 секунды. Это точка отказа.
Сценарий №3: Массовые таймауты
| Показатель | Значение |
|---|---|
| RPS | 587 |
| Latency (p95) | 2500 мс |
| Ошибки (таймауты) | 45% |
Вывод
Система «лежит». Большинство запросов не успевают ответить за таймаут.
А что если система падает без ошибок, но медленно? Иногда система не выдает ошибок 500, но становится очень медленной. Например, при 500 RPS время ответа = 50 мс, а при 600 RPS = 5000 мс.
В этом случае точка отказа — это не ошибки, а нарушение SLA (Service Level Agreement).
wireframe
// Устанавливаем порог: p95 должен быть < 500 мс
// Если становится больше — тест считается проваленным
§8.5. Пишем нагрузочный тест на JavaScript с Grafana k6
В предыдущем параграфе мы познакомились с NBomber — нагрузочным тестом на C#. Но в индустрии есть и другой популярный инструмент — Grafana k6.
k6
— это современный фреймворк для нагрузочного тестирования, написанный на Go, но тесты для него пишутся на JavaScript.
Почему k6 крут для нагрузочного тестирования:
| Возможность | Что это значит на практике |
|---|---|
| Легковесный | k6 — это один исполняемый файл (~20 МБ). Не нужно устанавливать тяжелые IDE или фреймворки. |
| Консольный | Все тесты запускаются из командной строки. Идеально для CI/CD (GitHub Actions, GitLab CI). |
| Интеграция с Grafana | k6 умеет отправлять метрики в Grafana Cloud или локальный Prometheus. Вы видите графики в реальном времени. |
| Понятный синтаксис | Тесты пишутся на JavaScript — простом и знакомом языке. Даже если вы не JS-разработчик, синтаксис легко читается. |
Grafana
— это платформа для визуализации данных. Она умеет показывать графики, таблицы и дашборды на основе данных из разных источников.
Любой тест на k6 состоит из трех основных частей.
1. options — Настройки теста
Здесь мы задаем, как будет нагружаться система: сколько виртуальных пользователей (VUs), как долго, как меняется нагрузка.
javascript
export const options = {
// Настройка нагрузки через stages (ступени)
stages: [
{ duration: '30s', target: 20 },// Разгон до 20 VUs за 30 секунд
{ duration: '1m', target: 20 },// Держим 20 VUs в течение 1 минуты
{ duration: '30s', target: 0 },// Спад до 0 VUs за 30 секунд
],
};
VUs
— это виртуальный пользователь. Каждый VU выполняет сценарий (функцию default) и повторяет его снова и снова. Если у вас 20 VUs, это значит, что 20 пользователей одновременно выполняют ваш сценарий.
2. default — Сценарий пользователя
Это функция, которая описывает, что делает один виртуальный пользователь.
javascript
export default function () {
// Здесь мы описываем действия пользователя
// Например: войти на сайт, поискать товар, оформить заказ
}
3. check — Проверка корректности
Мы проверяем, что ответ сервера корректен (статус-код 200, тело ответа содержит нужные данные).
javascript
import { check } from 'k6';
check(res, {
'status is 200': (r) => r.status === 200,
});
Давайте напишем полноценный тест, который отправляет POST-запрос на авторизацию.
javascript
// 1. Импортируем необходимые модули
import http from 'k6/http'; // Для отправки HTTP-запросов
import { check } from 'k6'; // Для проверки ответов
// 2. Настраиваем нагрузку
export const options = {
stages: [
{ duration: '30s', target: 10 },// Разгон до 10 VUs за 30 секунд
{ duration: '1m', target: 10 },// Держим 10 VUs в течение 1 минуты
{ duration: '30s', target: 0 },// Спад до 0 VUs за 30 секунд
],
};
// 3. Сценарий виртуального пользователя
export default function () {
// URL эндпоинта
const url = 'https://quickpizza.grafana.com/api/users/token/login';
// Тело запроса (JSON)
const payload = JSON.stringify({
username: 'default',
password: '12345678',
});
// Заголовки запроса
const params = {
headers: {
'Content-Type': 'application/json',
},
};
// Отправляем POST-запрос
const res = http.post(url, payload, params);
// Проверяем, что статус-код = 200
check(res, {
'response status is 200': (r) => r.status === 200,
});
}
Теперь разберем код по частям:
| Часть | Что делает |
|---|---|
import http from 'k6/http' | Импортируем модуль для HTTP-запросов |
import { check } from 'k6' | Импортируем функцию для проверок |
export const options = { stages: [...] } | Настраиваем профиль нагрузки |
export default function () { ... } | Сценарий одного виртуального пользователя |
http.post(url, payload, params) | Отправляем POST-запрос |
check(res, { ... }) | Проверяем, что ответ корректен |
Для проверки корректности ответов в k6 используется механизм checks.
check
— это аналог Assert в юнит-тестах. Но есть важное отличие: в k6 проверка не останавливает тест, если она провалилась. k6 просто записывает, сколько проверок прошло успешно, а сколько — нет.
Почему это важно
В нагрузочном тесте мы не хотим останавливать тест при первой ошибке. Мы хотим увидеть, сколько процентов запросов падает под нагрузкой.
javascript
// Проверяем несколько условий одновременно
check(res, {
'status is 200': (r) => r.status === 200,
'response body contains username':
(r) => r.body.includes('default'),
'response time < 500ms': (r) => r.timings.duration < 500,
});
Что мы видим в отчете:
wireframe
✓ status is 200
✓ response body contains username
✗ response time < 500ms
- ✓ — проверка прошла успешно.
- ✗ — проверка провалилась.
В конце теста k6 показывает процент успешных проверок:
wireframe
checks.........................: 98.5% 197 out of 200
Это значит, что 98.5% всех проверок прошли успешно. Если этот процент падает — система начинает «сыпаться».
Что еще можно проверять в checks?
После того как вы сохранили скрипт в файл (например, test.js), запускаете его командой:
bash
k6 run test.js
Что вы увидите в консоли:
wireframe
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: test.js
output: -
✓ status is 200
✓ body contains "success"
checks.....................: 100.00% ✓ 200 ✗ 0
data_received..............: 45 kB 1.5 kB/s
data_sent..................: 12 kB 408 B/s
http_req_duration..........: avg=45ms p95=89ms p99=156ms
http_req_failed............: 0.00% ✓ 0 ✗ 200
Что мы видим:
- checks: 100% успешных проверок.
- http_req_duration: среднее время ответа = 45 мс, p95 = 89 мс, p99 = 156 мс.
- http_req_failed: 0% ошибок. Все запросы успешны.
§8.6. Управление нагрузкой в k6: stages и ramping-vus
В предыдущем параграфе мы научились писать простой тест на k6 с фиксированной нагрузкой (10 VUs в течение 1 минуты).
Главная задача нагрузочного тестирования
— найти точку отказа (Breaking Point). Это момент, когда система перестает справляться с нагрузкой.
Для этого нам нужно уметь управлять нагрузкой: начинать с малого, постепенно увеличивать, искать момент, когда появляются ошибки.
stages
— это способ задать профиль нагрузки в k6. Вы описываете, как меняется количество виртуальных пользователей (VUs) во времени.
| Фаза | Что происходит | Зачем нужна |
|---|---|---|
| Разгон (Ramp-up) | Нагрузка растет от 0 до целевого значения | Прогрев системы, проверка, как система реагирует на рост нагрузки |
| Пик (Steady-state) | Нагрузка держится на постоянном уровне | Проверка стабильности под постоянной нагрузкой |
| Спад (Ramp-down) | Нагрузка падает до 0 | Проверка, как система восстанавливается после нагрузки |
Как это выглядит в коде:
javascript
export const options = {
stages: [
// РАЗГОН: с 0 до 20 VUs за 30 секунд
{ duration: '30s', target: 20 },
// ПИК: держим 20 VUs в течение 1 минуты
{ duration: '1m', target: 20 },
// СПАД: с 20 до 0 VUs за 30 секунд
{ duration: '30s', target: 0 },
],
};
Для аналогии можно рассмотреть следующую ситуацию. Вы заходите в спортзал. Начинаете с легкой разминки (разгон), потом бежите с постоянной скоростью (пик), потом замедляетесь и останавливаетесь (спад).
Теперь давайте напишем тест, который ищет точку отказа. Мы будем постепенно увеличивать нагрузку ступенями.
javascript
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
// Ступень 1: 10 VUs
{ duration: '1m', target: 10 },
// Ступень 2: 20 VUs
{ duration: '1m', target: 20 },
// Ступень 3: 50 VUs
{ duration: '1m', target: 50 },
// Ступень 4: 100 VUs
{ duration: '1m', target: 100 },
// Ступень 5: 200 VUs
{ duration: '1m', target: 200 },
// Ступень 6: 500 VUs
{ duration: '1m', target: 500 },
// Ступень 7: 1000 VUs
{ duration: '1m', target: 1000 },
],
};
export default function () {
const res = http.get('https://your-api.com/products');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
}
Что произойдет во время теста:
wireframe
Время 0-1 мин: VUs = 10 → ✅ Все запросы успешны, время ответа ~50 мс
Время 1-2 мин: VUs = 20 → ✅ Все запросы успешны, время ответа ~50 мс
Время 2-3 мин: VUs = 50 → ✅ Все запросы успешны, время ответа ~60 мс
Время 3-4 мин:VUs = 100 → ⚠️ Все запросы успешны, время ответа ~150 мс
Время 4-5 мин:VUs = 200 → ⚠️ Все запросы успешны, время ответа ~400 мс
Время 5-6 мин: VUs = 500 → ❌ 10% ошибок! (таймауты) ТОЧКА ОТКАЗА
Время 6-7 мин: VUs = 1000 → ❌ 40% ошибок! Система "лежит"
На ступени 500 VUs появились первые ошибки. Это значит, что система не выдерживает больше 200 VUs. То есть мы нашли точку отказа.
ramping-vus
— это альтернативный способ управления нагрузкой. Вместо stages (ступени), вы используете более гибкий экзекутор.
Когда использовать ramping-vus:
- Когда вам нужно больше контроля над нагрузкой.
- Когда вы хотите использовать exec для разных сценариев.
- Когда вам нужна более сложная логика изменения нагрузки.
Как это выглядит в коде:
javascript
export const options = {
scenarios: {
stress_test: {
executor: 'ramping-vus', // Используем ramping-vus
startVUs: 0, // Начинаем с 0 пользователей
stages: [
{ duration: '30s', target: 50 }, // Разгон до 50 VUs за 30 секунд
{ duration: '1m', target: 50 }, // Держим 50 VUs в течение 1 минуты
{ duration: '30s', target: 100 }, // Увеличиваем до 100 VUs за 30 сек
{ duration: '1m', target: 100 }, // Держим 100 VUs в течение 1 минуты
{ duration: '30s', target: 0 }, // Спад до 0 VUs за 30 секунд
],
gracefulRampDown: '30s', // Время на "мягкое" завершение
},
},
};
Что такое gracefulRampDown? Это время, которое k6 дает виртуальным пользователям на завершение текущего запроса перед остановкой. Если у вас есть запросы, которые выполняются долго, это позволяет им завершиться корректно.
Стресс-профиль
— это специальный профиль нагрузки, который используется для поиска точки отказа. Он выглядит как ступеньки: нагрузка растет, держится, снова растет, держится, и так до падения.
javascript
export const options = {
stages: [
// Ступень 1: нормальная нагрузка (проверяем стабильность)
{ duration: '2m', target: 100 },
// Ступень 2: 2× нормальной нагрузки
{ duration: '2m', target: 200 },
// Ступень 3: 3× нормальной нагрузки
{ duration: '2m', target: 300 },
// Ступень 4: 4× нормальной нагрузки (ищем точку отказа)
{ duration: '2m', target: 400 },
// Ступень 5: 5× нормальной нагрузки (система должна упасть)
{ duration: '2m', target: 500 },
// Спад: возвращаемся к норме и проверяем восстановление
{ duration: '1m', target: 100 },
],
};
Что мы проверяем таким профилем:
| Ступень | Нагрузка | Что проверяем |
|---|---|---|
| 1 | 100 VUs | Базовую стабильность |
| 2 | 200 VUs (2×) | Увеличение нагрузки в 2 раза |
| 3 | 300 VUs (3×) | Увеличение нагрузки в 3 раза |
| 4 | 400 VUs (4×) | 4× — возможно, здесь будет точка отказа |
| 5 | 500 VUs (5×) | 5× — система должна упасть |
| Спад | 100 VUs | Восстановление после падения |
Для интерпретирования результатов было разработано три сценария, которые стоит разобрать.
Как и в NBomber, в k6 есть понятие прогрева (Warm-up). Некоторые системы требуют времени, чтобы «разогреться» перед основным тестом.
javascript
export const options = {
stages: [
// Сначала прогрев — минимальная нагрузка
{ duration: '1m', target: 10 }, // Прогрев системы
// Потом основной тест
{ duration: '2m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '2m', target: 400 },
{ duration: '2m', target: 800 },
],
};
Зачем нужен прогрев в k6:
| Без прогрева | С прогревом |
|---|---|
| Первые результаты искажены (JIT-компиляция, кеширование) | Результаты стабильные и точные |
| Точка отказа может быть занижена | Точка отказа отражает реальную производительность |
§8.7. Анализ результатов: RPS, TTFB и «Красные» метрики
Вы запустили нагрузочный тест. NBomber или k6 вывели в консоль кучу цифр. Что они значат? Как понять, хорошо это или плохо? Где точка отказа?
В этом параграфе мы научимся читать результаты нагрузочных тестов и понимать, что на самом деле происходит с системой.
Самая частая ошибка новичков
— смотреть на среднее арифметическое время ответа (mean).
Почему среднее — это плохо? Представьте, что вы отправили 100 запросов:
- 99 запросов отработали за 10 мс.
- 1 запрос отработал за 10 000 мс (10 секунд).
Среднее арифметическое: (99 × 10 + 10000) / 100 = 109.9 мс.
Выглядит отлично! «Система работает быстро, среднее время ответа — 110 мс». Но реальность: 1% пользователей ждали 10 секунд! Это катастрофа. Что нужно смотреть вместо среднего?
Процентили
— это значения, ниже которых лежит определенный процент запросов.
| Процентиль | Что значит | Пример |
|---|---|---|
| p50 (медиана) | 50% запросов быстрее этого значения | 50% запросов < 10 мс |
| p95 | 95% запросов быстрее этого значения | 95% запросов < 50 мс |
| p99 | 99% запросов быстрее этого значения | 99% запросов < 200 мс |
В нашем примере (99 запросов по 10 мс, 1 запрос по 10000 мс):
| Метрика | Значение | Что это значит |
|---|---|---|
| Среднее | 110 мс | «Всё хорошо» (ложь) |
| p50 | 10 мс | Половина запросов — 10 мс |
| p95 | 10 мс | 95% запросов — 10 мс |
| p99 | 10 000 мс | 1% запросов — 10 секунд! |
Вот теперь видна реальная картина! Мы видим, что у системы есть проблема с «длинными хвостами» (long tails) — 1% запросов выполняются очень долго.
Как выглядят процентили в отчетах NBomber (C#):
wireframe
Latency (ms):
- min: 12.3
- mean: 45.6 ← НЕ СМОТРИМ!
- max: 234.5
- p50: 42.1 ← СМОТРИМ!
- p95: 89.3 ← СМОТРИМ!
- p99: 156.7 ← СМОТРИМ!
k6 (JavaScript):
wireframe
http_req_duration..............: avg=45ms p(95)=89ms p(99)=156ms
↑ ↑ ↑
НЕ СМОТРИМ! СМОТРИМ! СМОТРИМ!
RPS (Requests Per Second)
— пропускная способность. Это количество запросов, которое система может обработать за одну секунду.
Где смотреть при использовании в NBomber:
wireframe
RPS: 587
при использовании k6:
wireframe
http_reqs......................: 2345 39.1/s
↑
Это RPS
Как интерпретировать:
| RPS | Что это значит |
|---|---|
| Чем выше RPS — тем лучше | Система может обслужить больше пользователей |
| RPS падает с ростом нагрузки | Система начинает «захлебываться» |
| RPS держится стабильно | Система работает хорошо |
Пример интерпретации:
wireframe
При нагрузке 50 VUs: RPS = 1000
При нагрузке 100 VUs: RPS = 1000
При нагрузке 200 VUs: RPS = 1000
При нагрузке 300 VUs: RPS = 980 ← начало падения
При нагрузке 400 VUs: RPS = 750 ← система "захлебывается"
Вывод
Система выдерживает до 200 VUs без потери производительности. При 300 VUs начинает падать RPS.
TTFB (Time To First Byte)
— время до первого байта. Время от отправки запроса до получения первого байта ответа.
Что входит в TTFB:
- Время на установку соединения (TCP handshake).
- Время на обработку запроса сервером.
- Время на начало отправки ответа.
В NBomber TTFB обычно не выводится отдельно, но есть в метриках Network, но в k6 он находится:
wireframe
http_req_waiting...............: avg=45ms p(95)=89ms
↑
Это TTFB
Как интерпретировать:
| TTFB | Впечатление пользователя |
|---|---|
| < 100 мс | Отлично (мгновенно) |
| 100-300 мс | Хорошо (быстро) |
| 300-500 мс | Терпимо (заметно) |
| 500-1000 мс | Медленно (раздражает) |
| > 2000 мс | Очень медленно (пользователь уходит) |
Error Rate
— Процент ошибок (самая важная метрика). Процент запросов, которые завершились ошибкой.
Какие ошибки считаются:
- HTTP-статусы 5xx (500, 502, 503, 504).
- Таймауты (запрос не уложился в таймаут).
- Ошибки соединения (не удалось подключиться к серверу).
Где смотреть в NBomber:
wireframe
ok count: 1500
fail count: 0
в k6:
http_req_failed................: 0.00% ✓ 0 ✗ 200
Как интерпретировать:
| Error Rate | Что это значит | Действие |
|---|---|---|
| 0% | Идеально. Все запросы обработаны. | Отлично! |
| 0.1-1% | Приемлемо, но требует внимания | Нужно разобраться |
| 1-5% | Плохо. Пользователи замечают проблемы. | Срочно исправлять! |
| > 5% | Катастрофа. Система фактически не работает. | Остановить релиз! |
Важное правило заключается в следующем
Если Error Rate > 0% — система УЖЕ сломалась. Даже 0.1% ошибок означает, что реальные пользователи получают ошибки. В хорошей системе Error Rate = 0% при нормальной нагрузке.
В k6 существует понятие порогового значения.
Thresholds
— это критерии приемлемости (SLO — Service Level Objectives). Вы задаете пороговые значения, и если метрика хуже порога — тест считается проваленным.
Зачем нужны thresholds:
1. Автоматизация CI/CD. Тест падает, если система не соответствует требованиям.
2. Объективность. Не нужно гадать, «хорошо» это или «плохо».
3. Бизнес-требования. Вы можете проверить, выполняет ли система SLA (Service Level Agreement).
Как это выглядит в k6:
javascript
export const options = {
thresholds: {
// 95% запросов должны быть < 500 мс
'http_req_duration': ['p(95) < 500'],
// 99% запросов должны быть < 1000 мс
'http_req_duration': ['p(99) < 1000'],
// Не более 1% ошибок
'http_req_failed': ['rate < 0.01'],
// Не менее 1000 RPS
'http_reqs': ['rate > 1000'],
},
};
Пример отчета с thresholds:
wireframe
✓ http_req_duration..............: avg=45ms p(95)=89ms ✓ p(95) < 500
✓ http_req_failed................: 0.00% ✓ rate < 0.01
✗ http_reqs......................: 950 ✗ rate > 1000
Тест провален, потому что RPS (950) меньше требуемого (1000). Нужно оптимизировать систему. Именно эти пороговые значения сообщают нам о том, что можно ли отправлять проект в релиз или нет.
§8.8. Антипаттерны в тестировании производительности
В предыдущих параграфах мы научились писать нагрузочные тесты, управлять нагрузкой и анализировать результаты. Но есть одна важная вещь, о которой нужно помнить:
Информация
Нагрузочный тест, проведенный с ошибками, хуже, чем его отсутствие.
Почему? Потому что он создает ложное чувство уверенности. Вы смотрите на зеленый отчет и думаете: «Всё отлично, система выдерживает нагрузку». А в реальности система падает при первом же наплыве пользователей.
Давайте разберем три самых «смертельных» антипаттерна в тестировании производительности».
Антипаттерн №1. «Тестируем на ноутбуке против локальной БД»
Почему это плохо? В реальном продакшене всё иначе:
| Что у вас на ноутбуке | Что в продакшене |
|---|---|
| Ноутбук с 16 ГБ RAM | Сервер с 64+ ГБ RAM |
| База данных на том же компьютере | База данных на отдельном сервере (сетевые задержки) |
| Пустая база (10 записей) | Миллионы записей в БД |
| Нет других пользователей | Другие сервисы используют ту же БД |
| Нет сетевых задержек | Сетевые задержки 10–50 мс |
Так же можно привести пример из жизни. Новичок пишет тест на ноутбуке:
csharp
// Тест на ноутбуке против локальной БД
var scenario = Scenario.Create("local_test", async context =>
{
var request = Http.CreateRequest(
"GET", "https://localhost:5001/api/products"
);
var response = await Http.Send(httpClient, request);
return response;
})
.WithLoadSimulations(
Simulation.Inject(100, TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(30))
);
Результат на ноутбуке:
- RPS: 1000.
- p95: 15 мс.
- Ошибки: 0%.
Новичок радуется: «Система работает идеально!». Запускаем этот же тест против продакшена (через сеть, с реальной БД).
Результат в продакшене:
- RPS: 120 (в 8 раз меньше!).
- p95: 450 мс (в 30 раз медленнее!).
- Ошибки: 0.5% (появились ошибки!).
Вывод
Тест на ноутбуке дал абсолютно нерелевантные результаты. Система, которая на ноутбуке работала идеально, в продакшене еле дышит.
Что нужно делать правильно:
1. Тестируйте в окружении, максимально похожем на продакшен.
- Тестовая среда должна иметь такие же характеристики (CPU, RAM, сеть).
- База данных должна быть на отдельном сервере (или в Docker-контейнере с сетевыми задержками).
- В БД должно быть реалистичное количество данных.
2. Если нет тестового окружения — используйте Docker Compose.
yaml
version: '3'
services:
app:
build: .
ports:
- "5000:5000"
db:
image: postgres:15
environment:
POSTGRES_PASSWORD: password
Это хотя бы частично имитирует разделение сервисов.
3. Используйте профили нагрузки, учитывающие сетевые задержки.
- Добавляйте задержки (латентность) в тесты.
- В NBomber можно добавить Simulation.Inject с учетом времени ответа.
Антипаттерн №2. «Проверил один эндпоинт»
Почему это плохо?
| Если проверили только один эндпоинт | Что на самом деле происходит |
|---|---|
GET /api/products работает быстро | POST /api/orders может работать медленно |
GET /api/users работает быстро | PUT /api/users/1 может работать медленно |
| Один эндпоинт без аутентификации | Эндпоинт с аутентификацией может падать |
Приведем пример из жизни. Разработчик проверил только GET-запросы (чтение данных) и решил, что система готова.
Но в реальности:
POST /api/orders(создание заказа) требует записи в 5 таблиц, отправки email, вызова платежного шлюза.PUT /api/users/1(обновление профиля) требует проверки прав, обновления кеша, отправки уведомлений.DELETE /api/orders/1(удаление заказа) требует каскадного удаления связанных данных.
Все эти эндпоинты могут быть узкими местами! Что нужно делать правильно:
1. Тестируйте все критичные эндпоинты.
GET /api/products(чтение).POST /api/orders(создание).PUT /api/users/1(обновление).DELETE /api/orders/1(удаление).
2. Тестируйте комплексные сценарии.
- Пользователь → авторизация → поиск → добавление в корзину → оформление заказа.
- Это имитирует реальное поведение пользователя.
javascript
// ✅ ПРАВИЛЬНО: комплексный сценарий
export default function () {
// 1. Авторизация
const authRes = http.post('https://api.com/login', {
username: 'test',
password: '123456',
});
const token = JSON.parse(authRes.body).token;
// 2. Получение списка товаров
const productsRes = http.get('https://api.com/products', {
headers: { Authorization: `Bearer ${token}` },
});
// 3. Создание заказа
const orderRes = http.post('https://api.com/orders', {
productId: 1,
quantity: 2,
}, {
headers: { Authorization: `Bearer ${token}` },
});
// 4. Проверка статуса заказа
const orderId = JSON.parse(orderRes.body).orderId;
const statusRes = http.get(`https://api.com/orders/${orderId}/status`, {
headers: { Authorization: `Bearer ${token}` },
});
}
3. Ищите узкие места системно.
- Если
POST /api/ordersмедленный — возможно, проблема в БД. - Если
PUT /api/users/1медленный — возможно, проблема в кеше. - Если
DELETEмедленный — возможно, проблема в каскадном удалении.
Антипаттерн №3. «Игнорируем время разогрева» (Warm-up)
Почему это плохо? Первые запросы медленнее по нескольким причинам:
| Причина | Что происходит |
|---|---|
| JIT-компиляция (.NET) | Код компилируется в машинный код при первом вызове |
| Кеширование | Данные загружаются в кеш при первом запросе |
| Соединение с БД | Пул соединений создается при первом запросе |
| Инициализация | Статические конструкторы, синглтоны создаются при первом вызове |
Приведем пример из жизни. Новичок запускает тест на 60 секунд и сразу начинает измерять:
wireframe
Время 0-5 сек: RPS = 50 (система прогревается)
Время 5-10 сек: RPS = 100 (система прогрелась)
Время 10-15 сек: RPS = 100
Время 15-20 сек: RPS = 100
...
Если включить первые 5 секунд в результаты, средний RPS будет занижен. Что нужно делать правильно:
1. Добавьте Warm-up (прогрев) перед основным тестом.
var scenario = Scenario.Create("test", async context =>
{
// ... тест ...
})
.WithWarmUpDuration(TimeSpan.FromSeconds(30)) // 30 секунд прогрева
.WithLoadSimulations(
Simulation.Inject(100, TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(60))
);
2. Игнорируйте первые запросы в отчетах.
Некоторые инструменты позволяют исключить первые N секунд из статистики.
javascript
export const options = {
// Игнорируем первые 30 секунд в отчетах
discardResponseBodies: true,
// ... или используем thresholds с учетом прогрева
};
3. Проведите отдельный «прогревочный» запуск.
Запустите тест на 30-60 секунд с низкой нагрузкой, чтобы система «нагрелась». Затем запустите основной тест.
Тест производительности без Warm-up
— это спринтер, у которого замеряют время с первого шага.