АШШумилкин А. О.ОАиП · ПиТПМ
ПиТПМ · Лекция 08

Лекция №8. Тестирование производительности: Нагрузочное, стресс-тестирование, стабильность и масштабируемость системы.

6 610 слов8 разделов0 иллюстраций
↓ Скачать Markdown

§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 и отлаживать нагрузочный тест как обычную программу.
Мощный APINBomber предоставляет простые и понятные методы для создания 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))
);

Что мы увидим:

СтупеньНагрузкаРезультат
1100 RPSВсе запросы успешны
2200 RPSВсе запросы успешны
3400 RPSВремя ответа увеличилось
4800 RPSПоявились ошибки (точка отказа)
51600 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% ошибок)

ПоказательЗначение
RPS500
Latency (p95)150 мс
Ошибки0%

Вывод

Система работает отлично. Точка отказа еще не достигнута.

Сценарий №2: Появились таймауты (>0% ошибок)

ПоказательЗначение
RPS587
Latency (p95)2500 мс
Ошибки (таймауты)5%

Вывод

Система достигла предела. 5% запросов не укладываются в таймаут 3 секунды. Это точка отказа.

Сценарий №3: Массовые таймауты

ПоказательЗначение
RPS587
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).
Интеграция с Grafanak6 умеет отправлять метрики в 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 },
  ],
};

Что мы проверяем таким профилем:

СтупеньНагрузкаЧто проверяем
1100 VUsБазовую стабильность
2200 VUs (2×)Увеличение нагрузки в 2 раза
3300 VUs (3×)Увеличение нагрузки в 3 раза
4400 VUs (4×)4× — возможно, здесь будет точка отказа
5500 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 мс
p9595% запросов быстрее этого значения95% запросов < 50 мс
p9999% запросов быстрее этого значения99% запросов < 200 мс

В нашем примере (99 запросов по 10 мс, 1 запрос по 10000 мс):

МетрикаЗначениеЧто это значит
Среднее110 мс«Всё хорошо» (ложь)
p5010 мсПоловина запросов — 10 мс
p9510 мс95% запросов — 10 мс
p9910 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

— это спринтер, у которого замеряют время с первого шага.