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

# Лекция №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

```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

```bash
dotnet add package NBomber.Http
```

А теперь — сам тест. Отправляем **GET-запрос** к **API**:

csharp

```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

```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

```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

```csharp
Simulation.Inject(
    rate: 50,                          // 50 запросов
    interval: TimeSpan.FromSeconds(1), // в секунду
    during: TimeSpan.FromSeconds(30)   // в течение 30 секунд

)
```

Когда использовать

Когда вы хотите проверить систему под стабильной нагрузкой (например, нагрузочный тест).

**2. Simulation.RampingInject — Постепенно растущая нагрузка**

Вы задаете начальную нагрузку, конечную нагрузку и время, за которое нагрузка должна вырасти. **NBomber** плавно увеличивает количество запросов.

csharp

```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

```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

```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

```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

```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

```javascript
export default function () {

  // Здесь мы описываем действия пользователя
  // Например: войти на сайт, поискать товар, оформить заказ

}
```

**3. check — Проверка корректности**

Мы проверяем, что **ответ сервера корректен** (*статус-код 200, тело ответа содержит нужные данные*).

javascript

```javascript
import { check } from 'k6';

check(res, {
  'status is 200': (r) => r.status === 200,
});
```

Давайте напишем полноценный тест, который отправляет POST-запрос на авторизацию.

javascript

```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

```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

```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

```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

```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

```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

```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

```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

```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

```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

```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

```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 (прогрев) перед основным тестом.**

```csharp
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

```javascript
export const options = {
  // Игнорируем первые 30 секунд в отчетах
  discardResponseBodies: true,
  // ... или используем thresholds с учетом прогрева
};
```

**3. Проведите отдельный «прогревочный» запуск.**

Запустите тест на 30-60 секунд с низкой нагрузкой, чтобы система «нагрелась». Затем запустите основной тест.

**Тест производительности без Warm-up**

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