← назад к разделу

Функциональные тесты отвечают на вопрос «работает ли». Нагрузочные — на вопросы, которые всплывают позже и больнее: сколько пользователей выдержит сервис, где он сломается и что произойдёт при пике. Отвечать на них в проде — дорого; для этого и существует нагрузочное тестирование.

нагрузку наращиваем слева направо: от 100 до 700 запросов в секунду запросов в секунду обслужено время ответа 100 200 300 400 500 600 700 поданоточка насыщениясервис отдаёт 450 в секунду цель: p99 до 300 мсp99 — 2,6 ссреднее — 200 мс среднее 200 мс прячет хвост: каждый сотый ждёт 2,6 секунды

Пока нагрузка ниже предела, обслуженных запросов ровно столько, сколько подали, и время ответа почти не растёт. После точки насыщения сервис всё равно отдаёт свои 450 запросов в секунду, а лишние ждут в очереди — p99 загибается вверх и доходит до 2,6 с. Среднее при этом остаётся 200 мс: на каждый застрявший запрос приходится девяносто девять быстрых, и они его вытягивают.

Обязательно

Виды нагрузки: четыре вопроса — четыре теста

  • Нагрузочный тест (load test) — «держим ли ожидаемую нагрузку?»: подаём плановый трафик (например, 200 запросов в секунду) и смотрим, укладываемся ли в целевую латентность.
  • Стресс-тест (stress test) — «где предел и как ломаемся?»: наращиваем нагрузку до деградации. Важен не только предел, но и характер отказа: плавное замедление или лавина ошибок и падение.
  • Тест на выносливость (soak test) — «выживем ли за ночь?»: умеренная нагрузка часами. Ловит то, что не видно за десять минут: утечки памяти, исчерпание соединений, распухание кэшей.
  • Тест на всплеск (spike test) — «переживём ли пик?»: резкий скачок (реклама вышла, рассылка ушла) и возврат к норме. Проверяет автомасштабирование и очереди.
load набор полка 200/с спуск stress ступень 200 ступень 400 ступень 600 отказ soak набор полка 3 часа спуск spike норма пик в 10 раз норма

Четыре вида нагрузки различаются формой прогона во времени: смотрите, где есть полка, где ступени, а где одиночный пик.

Как подаётся нагрузка: закрытая и открытая модель

Прежде чем говорить о цифрах, нужно понять, как именно генератор создаёт нагрузку. Способов два, они дают разные результаты на одной и той же системе, и половина неверно истолкованных отчётов растёт отсюда.

Закрытая модель. Вы задаёте число виртуальных пользователей — скажем, 50. Каждый работает в цикле: отправил запрос, дождался ответа, подумал секунду, отправил следующий. Число одновременных запросов в системе фиксировано и равно числу пользователей; темп запросов — следствие, а не задание.

Открытая модель. Вы задаёте темп: 200 запросов в секунду. Генератор выдаёт их по расписанию независимо от того, ответил ли сервис на предыдущие. Число одновременных запросов — следствие: если сервис отвечает медленнее, запросы накапливаются.

Разница принципиальная, и вот в чём. В закрытой модели медленный сервис сам себя защищает: пока пользователь ждёт ответа, он не отправляет новый запрос, то есть нагрузка автоматически падает вместе с деградацией. Это удобно и неправда: настоящие пользователи так себя не ведут. Когда ваш сервис отвечает не 100 мс, а 3 секунды, посетители не начинают заходить реже — они продолжают приходить с тем же темпом, и очередь растёт. Именно это в отчётах называется coordinated omission: генератор «сговорился» с сервисом и вместе с ним замедлился, поэтому хвост латентности в отчёте занижен, иногда в разы.

Отсюда практическое правило: воспроизводимая проверка целей делается открытой моделью — задан темп, сервис обязан его держать. Закрытая модель нужна для другого: когда вы честно моделируете ограниченное число клиентов, каждый из которых ждёт ответа, — пул соединений другого сервиса, сотрудники в интерфейсе. Для публичного HTTP-сервиса по умолчанию открытая.

В инструментах это выглядит так: в k6 закрытая модель — исполнители constant-vus и ramping-vus, открытая — constant-arrival-rate и ramping-arrival-rate; в Gatling закрытая — atOnceUsers/constantConcurrentUsers, открытая — constantUsersPerSec/rampUsersPerSec; в JMeter открытая модель требует отдельного модуля-таймера, а по умолчанию поток ждёт ответа, то есть модель закрытая.

И оговорка, без которой открытая модель обманывает так же: она работает, пока генератор вытягивает заданный темп. Упёрся в свои процессоры, потоки или лимит открытых соединений — и он тихо выдаёт меньше, чем вы просили. Поэтому в отчёте всегда сверяют, что план выдан целиком.

закрытая 50 клиентов каждый ждёт темп упал сам хвост занижен открытая 200 в секунду никого не ждёт темп держится хвост виден

Одно и то же замедление сервиса в двух моделях подачи: смотрите, как закрытая снижает темп сама и прячет хвост.

Метрики: что измерять

Пропускная способность — запросов в секунду (RPS/TPS). Ошибки — доля не-2xx и таймаутов. И главное — латентность перцентилями:

  • p50 (медиана) — типичный пользователь;
  • p95/p99 — хвост: каждый двадцатый/сотый запрос медленнее этого значения.

Среднее арифметическое — худшая метрика латентности: среднее 80 мс может скрывать p99 в 4 секунды, а страдающие 1% пользователей — это на большом сервисе тысячи людей. Цель формулируют перцентилями: «p99 < 300 мс при 500 RPS, ошибок < 0,1%». Такая цель — это SLO, внутренняя планка команды, и именно её меряет нагрузочный тест. SLA — уже договор с клиентом, где за нарушение платят деньгами или скидкой, поэтому в нём цифры обычно мягче, с запасом.

Смотрите на связку: с ростом нагрузки RPS растёт линейно, потом выходит на полку, а латентность у полки взлетает — это и есть предел системы (сатурация).

Инструменты

JMeterGatlingk6
Сценариисборка в GUI, план в XML, прогон без GUIкод: Scala, Java, Kotlin, JavaScript/TypeScriptкод на JavaScript
Сильная стороназрелость, море плагинов, любые протоколыэффективность, отчёты из коробкипростота, CI-дружелюбие, лёгкий бинарь
Слабая сторонатяжёлые сценарии в XML, дорогие потокираспределённый прогон приходится собирать рукамименьше протоколов вне HTTP
Кому подходиткоманды с историей на нём, не-HTTP протоколыJVM-команды, сценарии как кодновые проекты, нагрузка в pipeline

Про JMeter стоит уточнить отдельно, потому что из строки таблицы легко сделать неверный вывод: GUI нужен только чтобы собрать и отладить сценарий, а гоняют его без окна — jmeter -n -t план.jmx. Сам проект так и рекомендует, и в этом виде JMeter спокойно живёт в сборке наравне с остальными двумя.

Практическое правило: для нового проекта с HTTP/gRPC начинайте с k6 или Gatling — сценарии живут в git и гоняются в CI. JMeter выбирают за экзотические протоколы и накопленную экспертизу.

Сценарий целиком: десять строк на k6

Идея понятна, а до первого запуска обычно не доходит, потому что непонятно, из чего состоит сценарий. Вот полный рабочий сценарий на k6 — он подаёт заданный темп, проверяет ответы и сам решает, прошёл тест или нет.

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  scenarios: {
    plateau: {
      executor: 'constant-arrival-rate',
      rate: 200, timeUnit: '1s',
      duration: '5m',
      preAllocatedVUs: 100, maxVUs: 400,
    },
  },
  thresholds: {
    'http_req_duration{expected_response:true}': ['p(99)<300'],
    http_req_failed: ['rate<0.001'],
    dropped_iterations: ['count<1'],
  },
};

export default function () {
  const res = http.get('https://stage.example.com/api/orders?page=1');
  check(res, { 'ответ 200': (r) => r.status === 200 });
}

Разберём по частям, потому что каждая отвечает на один из вопросов выше. executor: 'constant-arrival-rate' — открытая модель: двести запросов в секунду независимо от того, как сервис отвечает. preAllocatedVUs и maxVUs — сколько исполнителей генератор держит наготове, чтобы этот темп вытянуть (по закону Литтла их нужно не меньше, чем rate × ожидаемое время ответа, и с запасом на деградацию). thresholds — это и есть цель из SLO, записанная кодом: если p99 вылез за 300 мс или ошибок больше одной на тысячу, k6 завершается с ненулевым кодом, то есть тест падает, как обычный тест в сборке. Третий порог — dropped_iterations — проверяет самого генератора: если он не смог выдать заданный темп, результат недействителен, и об этом лучше узнать из красной сборки, чем из неверного вывода.

Сценарий живёт в репозитории рядом с кодом и запускается одной командой:

k6 run --summary-export=summary.json load/orders.js

В других инструментах тот же смысл выглядит так — отличается синтаксисом, не устройством:

Gatling на Java:

public class OrdersSimulation extends Simulation {
    HttpProtocolBuilder httpProtocol = http.baseUrl("https://stage.example.com");

    ScenarioBuilder orders = scenario("orders")
            .exec(http("list").get("/api/orders?page=1").check(status().is(200)));

    {
        setUp(orders.injectOpen(constantUsersPerSec(200).during(300)))
                .protocols(httpProtocol)
                .assertions(global().responseTime().percentile4().lt(300),
                            global().failedRequests().percent().lt(0.1));
    }
}

В Go-командах чаще всего берут тот же k6 (он сам написан на Go, сценарии на JavaScript) или vegeta: утилиту командной строки и одновременно библиотеку, у которой открытая модель встроена. Тот же сценарий как программа на vegeta:

rate := vegeta.Rate{Freq: 200, Per: time.Second} // открытая модель: 200 запросов в секунду
targeter := vegeta.NewStaticTargeter(vegeta.Target{Method: "GET", URL: "https://stage.example.com/api/orders?page=1"})
attacker := vegeta.NewAttacker()

var metrics vegeta.Metrics
for res := range attacker.Attack(targeter, rate, 5*time.Minute, "orders") {
	metrics.Add(res)
}
metrics.Close()

fmt.Printf("p99=%s success=%.4f\n", metrics.Latencies.P99, metrics.Success)
if metrics.Latencies.P99 > 300*time.Millisecond || metrics.Success < 0.999 || uint64(metrics.StatusCodes["200"]) != metrics.Requests {
	os.Exit(1) // порог как условие падения
}

vegeta держит заданный темп независимо от ответов и заводит новые горутины, пока темп не выдан, поэтому «упёрся ли генератор» смотрят по metrics.Errors (таймауты, исчерпанные соединения) и по нагрузке самой машины. Для gRPC, сложных сценариев с состоянием и отчётов в CI Go-команды обычно всё равно останавливаются на k6.

Для Node естественный выбор тот же k6, но и Gatling умеет сценарии на JavaScript и TypeScript (пакеты @gatling.io/core и @gatling.io/http), DSL тот же:

import { simulation, scenario, constantUsersPerSec, global } from "@gatling.io/core";
import { http, status } from "@gatling.io/http";

export default simulation((setUp) => {
  const httpProtocol = http.baseUrl("https://stage.example.com");

  const orders = scenario("orders")
    .exec(http("list").get("/api/orders?page=1").check(status().is(200)));

  setUp(orders.injectOpen(constantUsersPerSec(200).during(300)))
    .protocols(httpProtocol)
    .assertions(global().responseTime().percentile4().lt(300),
                global().failedRequests().percent().lt(0.1));
});

В Python берут Locust (locust --headless -u 200 -r 50 --run-time 5m -f load/orders.py). Порог как условие падения там пишут обработчиком события quitting:

from locust import HttpUser, constant_throughput, events, task


class Orders(HttpUser):
    host = "https://stage.example.com"
    wait_time = constant_throughput(1)      # каждый пользователь: 1 запрос в секунду; 200 пользователей дают 200 в секунду

    @task
    def list_orders(self):
        with self.client.get("/api/orders?page=1", catch_response=True) as r:
            if r.status_code != 200:
                r.failure(f"status {r.status_code}")


@events.quitting.add_listener
def check_thresholds(environment, **kw):
    total = environment.stats.total
    if total.fail_ratio > 0.001 or total.get_response_time_percentile(0.99) > 300:
        environment.process_exit_code = 1   # порог как условие падения

Оговорка про модель: constant_throughput держит темп на уровне одного пользователя, и если ответ длится дольше секунды, этот пользователь темп не выдаст. Поэтому пользователей берут с запасом (по закону Литтла, как preAllocatedVUs в k6) и сверяют в отчёте фактический RPS с заданным: просел — результат недействителен, как и dropped_iterations в k6.

Что в этих двадцати строках уже правильно и стоит повторить в своём первом сценарии: открытая модель, порог как условие падения, проверка кода ответа (без неё сервис, отдающий пятисотые мгновенно, покажет великолепную латентность) и отдельная проверка, что генератор выдал план.

Методика: пять шагов

  1. Цель из SLO: «p99 < 300 мс при 500 RPS» — без цели тест не интерпретировать.
  2. Реалистичный сценарий: микс операций как в проде (95% чтение / 5% запись, а не однородный GET), реалистичные данные, прогрев кэшей отдельным этапом.
  3. Стенд, похожий на прод: те же лимиты CPU/памяти, та же база с прод-объёмом данных. Тест против пустой базы на мощном стенде измеряет ничего.
  4. Генератор — не узкое место: нагрузку подают с отдельной машины (не с ноутбука через VPN) и проверяют, что упирается система, а не генератор.
  5. Анализ у полки: метрики приложения и инфраструктуры (мониторинг в момент теста) — CPU, пул соединений БД, GC-паузы: предел всегда во что-то конкретное упирается.

Профиль прогона: разогрев, набор, полка, спуск

«Подали 200 запросов в секунду на пять минут» — так тест не устроен. У любого прогона четыре участка, и путаница между ними портит анализ чаще, чем неверно выбранный инструмент.

Разогрев. Первые минуты система не в рабочем состоянии: код ещё не оптимизирован, кеши пусты, соединения с базой не открыты, у базы не прогрет собственный кеш страниц. Цифры этого участка не описывают ни прод, ни ваш сервис — они описывают старт. Разогрев делают явно: небольшой темп на несколько минут, и в анализ он не входит.

Набор. Плавный рост до целевого темпа. Нужен не для вежливости, а чтобы увидеть, где именно начинается деградация: при мгновенной подаче полного трафика вы получите только «плохо» без ответа «с какого места». Скорость набора выбирают так, чтобы автомасштабирование успевало (если оно есть) — иначе вы измеряете его задержку, а не сервис.

Полка. Главный участок: целевой темп держится достаточно долго, чтобы проявилось то, что не видно за минуту. Ориентир — не меньше десяти минут, а лучше двадцать-тридцать: за это время успевают сработать срок жизни кеша, вытеснение, накопление в очередях и рост объёма данных от самого теста. Выводы делают по полке, причём по её второй половине.

Спуск. Плавное снижение до нуля. Полезен тем, что показывает восстановление: за сколько рассасывается очередь, возвращается ли время ответа к исходному, не остались ли зависшие соединения. Сервис, который после снятия нагрузки десять минут отвечает плохо, — это находка, и видна она только на спуске.

Чего в анализ не берут: разогрев целиком, участок набора (он о переходном режиме), первые минуты полки после выхода на темп и всё, что после начала спуска. Практически это значит, что пятиминутный прогон не даёт ни одной пригодной цифры: минута разогрева, минута набора, минута стабилизации — и от полки остаются две минуты, в которых кеш ещё не вытеснялся.

У разных видов нагрузки профиль свой: у теста на выносливость полка — это часы, у теста на всплеск полка короткая, а интересен как раз резкий набор и спуск, у стресс-теста полки нет вовсе — вместо неё ступени, растущие до отказа, и с выдержкой на каждой.

Что делать с результатом

Тест закончился, в отчёте полка на 450 запросах в секунду и p99 в 280 мс на ней. Это ещё не результат работы — это данные. Результатом становится решение, и оно выводится в четыре шага.

Шаг 1: сравнить с целью. Цель была 500 запросов в секунду при p99 меньше 300 мс. Полка на 450 означает, что цель не выполнена, — и это уже вывод, с которым можно идти дальше. Если бы полка была на 900, вывод тоже есть: запас двукратный, оптимизировать нечего, деньги лучше потратить в другом месте.

Шаг 2: посчитать, сколько нужно копий. Одна копия держит 450 запросов в секунду. Цель 700 — значит, копий нужно две, но не «700 / 450 = 1,6, округлим». Считают с двумя поправками. Первая: запас на пик — трафик неровный, и вечерний пик обычно в полтора-два раза выше среднего, поэтому целевой темп берут по пику, а не по среднему за сутки. Вторая: запас на отказ — если при потере одной копии остальные должны удержать нагрузку, копий нужно на одну больше, чем по расчёту. Отсюда типичная планировка: 700 × 1,5 (пик) / 450 = 2,4 → три копии, плюс одна на отказ → четыре, и загрузка каждой в норме получается около 40 %. Число «40 %» не эстетика: копия, загруженная на 85 %, при любом всплеске оказывается на полке, то есть в режиме, где время ответа взлетает.

Шаг 3: проверить, что масштабирование вообще работает. Расчёт выше предполагает, что две копии держат вдвое больше. Это верно, только пока узкое место внутри копии. Если предел упирался в общий ресурс — базу, брокер, внешнюю службу, лимит соединений — вторая копия не добавит пропускной способности, а ускорит наступление предела. Проверяется одним прогоном на двух копиях: получили 900 вместо 450 — масштабирование линейное, получили 520 — вы нашли общий ресурс, и дальше речь не про копии.

Шаг 4: решить, масштабировать или чинить. Масштабировать разумно, когда предел линейный и деньги дешевле работы. Чинить — когда предел упирается в одно конкретное место, найденное в метриках: запрос без индекса (первый подозреваемый почти всегда), пул соединений меньше числа рабочих потоков, последовательный вызов соседа внутри обработки запроса, ненужная сериализация. Отличие видно по цифрам: если процессор копии загружен на 30 %, а полка есть, — масштабирование лишнее, узкое место в ожидании, и его надо искать, потому что копии будут ждать так же.

И последнее, без чего тест не имеет продолжения: числа записывают. Полка, p99 на полке, узкое место, дата, версия — в файл рядом с кодом сценария. Следующий прогон через месяц отвечает на вопрос, который важнее абсолютной цифры: стало хуже или лучше. Отчёт, который существует только в чате, через две недели равносилен отсутствию теста.

Типичные ошибки

  • Смотреть на среднее вместо перцентилей — см. выше.
  • Мерить без прогрева: первые минуты сервис прогревает кэши и пулы соединений, а JVM ещё и JIT, эти цифры не о проде.
  • Игнорировать coordinated omission: если генератор ждёт ответа перед следующим запросом, он сам замедляется вместе с сервисом и занижает хвост латентности. Помогает открытая модель нагрузки (arrival rate в k6, вливание пользователей в секунду в Gatling): она держит заданный темп, как бы медленно сервис ни отвечал. Но помогает ровно до тех пор, пока генератор этот темп вытягивает: упёрся в свои потоки, соединения или процессор — и хвост опять занижен. Поэтому в отчёте сверяют, что план выдан целиком: в k6 смотрят на dropped_iterations, в Gatling — на отставание фактически запущенных сценариев от заданных.
  • Однократный запуск: результат без повторения — случайность, а не измерение.
  • Один эндпоинт вместо пользовательского пути: обстрел GET /api/products показывает предел кеша, а не сервиса. В проде пользователь смотрит список, открывает товар, кладёт в корзину и оформляет заказ — с разной ценой запросов и разными таблицами. Сценарий строят из пути с реалистичными долями, а не из самого удобного адреса.
  • Генератор стал узким местом незаметно: упёрся не в процессор, а в число открытых соединений, в лимит файловых дескрипторов или в один сетевой интерфейс. Признак — темп не растёт при росте заданной нагрузки, а время ответа растёт «идеально ровно»; проверяется тем, что прогон с двух машин даёт больше, чем с одной.
  • Прогон против общего контура без предупреждения: соседняя команда полдня разбирается с таймаутами, которых нет.
Дополнительно: при первом чтении можно пропустить

Глубже: закон Литтла: почему у полки время ответа взлетаетрасширенное

У картинки с полкой есть объяснение в одну строку, и оно стоит того, чтобы его знать, потому что превращает нагрузочный тест из наблюдения в расчёт.

Число запросов, одновременно находящихся в системе, равно темпу, умноженному на время ответа. Это закон Литтла; он верен для любой очереди и не требует предположений о распределении:

N = λ × W
N — сколько запросов внутри системы одновременно
λ — темп поступления (запросов в секунду)
W — время, которое запрос проводит в системе (секунды)

Считать по нему просто. Сервис отвечает за 50 мс при 200 запросах в секунду: внутри одновременно 200 × 0,05 = 10 запросов. Значит, десяти рабочих потоков достаточно, а двадцать соединений к базе — уже запас. Теперь время ответа выросло до 500 мс при том же темпе: внутри 200 × 0,5 = 100 запросов. Тот же сервис с тем же трафиком требует в десять раз больше одновременных ресурсов — потоков, соединений, памяти на буферы.

Отсюда видно, почему полка выглядит именно так. Пока есть свободные ресурсы, рост темпа обслуживается и время ответа почти не меняется. Как только какой-то ресурс исчерпан (все потоки заняты, пул соединений пуст), сервис больше не может увеличить пропускную способность — λ на выходе перестаёт расти, это и есть полка. А запросы продолжают поступать с тем же темпом, значит N растёт, и по тому же закону при неизменной пропускной способности растёт W: время ответа увеличивается ровно потому, что запрос всё дольше стоит в очереди, а не потому, что его стали медленнее обрабатывать.

Практические следствия, которыми пользуются постоянно:

  • Проверка результата на правдоподобие. Отчёт говорит «500 запросов в секунду при p50 в 200 мс» — значит, внутри было около ста одновременных запросов. Если у сервиса пул соединений на двадцать и один поток на запрос, цифры не сходятся, и надо искать ошибку в измерении.
  • Расчёт размеров по цели. Нужно держать 700 запросов в секунду с ответом 100 мс: N = 70 одновременных запросов. Дальше это делится на число копий и сравнивается с их настройками.
  • Диагностика причины. Если при росте нагрузки выросло N, а пропускная способность нет, вы нашли предел ресурса, и его надо искать в метриках, а не в коде: занятые потоки, пустой пул, очередь в брокере.

Глубже: где живёт нагрузочный тестрасширенное

Нагрузочный тест — не уровень пирамиды, а отдельная ось: он отвечает не на вопрос «работает ли», а на вопрос «сколько держит», и поэтому живёт по другим правилам.

Частота. На каждый коммит его не гоняют: он занимает полчаса-час, требует отдельного контура и его результат зависит от соседей. Разумная раскладка — короткий прогон (пять-десять минут по ключевому пути) на кандидата выпуска или по ночам, и полный прогон перед заметными изменениями: новая версия базы, изменение схемы, переписанный обработчик, ожидаемый рост трафика перед распродажей.

Свой контур. Нагрузочный прогон ломает работу другим — это то, о чём забывают чаще всего. Общий тестовый контур, по которому в это время кто-то проверяет свою задачу, получает таймауты, и команда рядом полдня ищет несуществующую ошибку. Поэтому у нагрузочного теста либо отдельный контур, либо забронированное время, объявленное заранее. Если контур общий и разделить никак — предупреждение в канал перед запуском это минимальная вежливость, а лучше поднять контур на время прогона и погасить после.

И тем более не в проде. Нагрузочный прогон против прода — это авария, устроенная своими руками, и возвращают её не отменой теста, а разбором инцидента. Исключение существует и называется иначе: проверка на живой системе делается доливом малой доли трафика на одну копию с готовностью мгновенно снять, а не подачей плановой нагрузки, и этим занимаются тогда, когда контур уже нельзя сделать похожим.

Данные. Стенд с пустой базой не измеряет ничего: запрос без индекса на тысяче строк летает, а на десяти миллионах ложится. Нужен прод-подобный объём, и здесь появляется требование, которое нельзя обойти: это персональные данные, и на стенде они должны быть обезличены — имена, телефоны, адреса и платёжные данные заменены, при сохранении формы и распределения (та же длина строк, та же доля повторов, то же распределение по датам, иначе планы запросов будут другими). Способа два: обезличивающая выгрузка из прода по расписанию или генератор, который создаёт данные нужного объёма и формы. Второй дороже в написании и безопаснее в эксплуатации.

Что ещё нужно, кроме данных. Тестовые учётные записи (не чужие настоящие), заглушки внешних систем с реалистичной задержкой (иначе вы измеряете провайдера, а его тестовый контур отвечает быстрее боевого и может просто отключить вас за поток запросов), и прогретые кеши как отдельный этап.

Коротко

  • Четыре вида: load — плановая нагрузка, stress — поиск предела, soak — часы работы (утечки), spike — резкий пик.
  • Метрики: RPS, доля ошибок и латентность перцентилями (p50/p95/p99); среднее — вводит в заблуждение, цель (SLO) формулируют перцентилями.
  • Инструменты: k6 — сценарии на JS и CI; Gatling — код и отчёты для JVM-команд; JMeter — зрелость и не-HTTP протоколы.
  • Методика: цель из SLO → реалистичный сценарий → прод-подобный стенд → генератор не узкое место → анализ метрик у предела.
  • Главные грабли: среднее вместо перцентилей, тест без прогрева, coordinated omission, вывод по одному прогону.
  • Модель нагрузки: закрытая (задано число пользователей, каждый ждёт ответа) занижает хвост, потому что замедляется вместе с сервисом; для проверки целей нужна открытая (задан темп), и отдельно проверяется, что генератор этот темп выдал.
  • Закон Литтла N = λ × W: у полки пропускная способность перестаёт расти, поэтому при том же темпе растёт число запросов внутри системы, а с ним и время ответа.
  • Профиль прогона: разогрев и набор в анализ не входят, выводы по второй половине полки длиной хотя бы десять минут, спуск показывает восстановление.
  • Результат — не цифра, а решение: сравнить с целью, посчитать копии с запасом на пик и на отказ, проверить линейность на двух копиях, и только потом выбирать между масштабированием и поиском узкого места.
  • Нагрузочный тест — отдельная ось, а не уровень пирамиды: свой контур или забронированное время, обезличенные данные прод-объёма, не на общем стенде без предупреждения и не в проде.

Что почитать дальше