Как проводить нагрузочное тестирование на Python

от автора

Помните момент, когда сервис работал нормально, но после рассылки, рекламной кампании или наплыва пользователей начал тормозить? В такие секунды и выясняется, что без нагрузочного тестирования команда на самом деле не знает, где у системы потолок, как проседает производительность и в какой точке критично растёт время отклика. Хорошо настроенное нагрузочное тестирование на Python помогает опираться на цифры: сколько запросов выдерживает API, как ведут себя ключевые бизнес-сценарии и когда инфраструктура начинает деградировать.

Зачем бизнесу нагрузочное тестирование и что именно нужно проверять

Даже небольшой интернет-магазин, личный кабинет, CRM или внутренний сервис могут столкнуться с пиковыми всплесками: сезонные продажи, массовые загрузки документов, акции, интеграции, ночные пакетные операции. Если система не готова, пользователи видят зависания, ошибки и бесконечные спиннеры. Тестирование нужно абсолютно всем.

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

  • показывает реальную границу устойчивости системы;

  • помогает найти узкие места до релиза, а не после инцидента;

  • даёт аргументы для масштабирования инфраструктуры;

  • снижает риск потери выручки и репутации;

  • помогает уйти от субъективных ощущений и оценивать работу системы по конкретным показателям.

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

Хороший подход — делить объекты тестирования на три уровня:

  1. Локальный уровень — отдельные HTTP-запросы и эндпоинты.

  2. Прикладной уровень — цепочки действий внутри одного сервиса.

  3. Бизнес-уровень — сквозной процесс от начала до результата.

Именно на третьем уровне чаще всего вскрываются настоящие проблемы: лишние обращения к БД, повторная авторизация, каскадные вызовы между сервисами, неоптимальные сериализация и валидация.

Метрики: что считать

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

Базовый набор, за которым стоит следить:

  • RPS (requests per second) — сколько запросов сервис реально обрабатывает за секунду;

  • время отклика — среднее, медианное и, что особенно важно, перцентили;

  • процент ошибок;

  • количество таймаутов;

  • стабильность результата при длительном прогоне.

Отдельно важна не только пиковая нагрузка, но и тестирование стабильности. Сервис может неплохо пережить короткий всплеск, но начать «ползти» через 20–30 минут: растут задержки, копятся зависшие соединения, увеличивается потребление памяти, а часть операций начинает отваливаться только на длинной дистанции.

Есть ещё один важный момент: в реальных проектах не всегда есть быстрый доступ ко всем внутренним серверным метрикам. Поэтому часто ограничиваются клиентской стороной. Это нормально. Даже если вы собираете только то, что видно снаружи, уже можно честно оценить пользовательский опыт: сколько занял запрос, в какой момент начались ошибки, где скачком выросло время отклика и как меняется RPS (requests per second) при росте конкуренции.

Почему связка Python и Locust удобна для команды

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

Почему выбирают именно Locust:

  • сценарии пишутся обычным кодом, а не в экзотическом DSL;

  • легко описывать разные типы пользователей и их поведение;

  • можно гибко управлять числом виртуальных клиентов и скоростью разгона;

  • результаты удобно запускать локально, на стенде и в пайплайне CI/CD.

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

Вот как может выглядеть самый простой сценарий в Locust, если нужно быстро проверить базовую нагрузку на каталог и поиск:

from locust import HttpUser, task, between
class ShopUser(HttpUser): 
wait_time = between(1, 3)
@task(3)
def open_catalog(self):
    self.client.get("/api/catalog")
@task(2)
def search_products(self):
    self.client.get("/api/search", params={"q": "ноутбук"})
@task(1)
def open_product_card(self):
    self.client.get("/api/products/42")

Здесь один виртуальный пользователь чаще открывает каталог, чуть реже использует поиск и ещё реже заходит в карточку товара. Даже такой короткий пример уже показывает, как задаются базовые сценарии нагрузки и как потом сравниваются время отклика, ошибки и RPS (requests per second).

Как устроить проект: HTTPX, модели данных, хуки и переиспользуемые клиенты

Наивный путь выглядит так: написать большой locustfile, раскидать по нему запросы и вручную собирать статистику. Работать это будет недолго. Как только сценариев станет больше трёх-четырёх, код начнёт разваливаться.

Поэтому практичная архитектура обычно строится так:

  • централизованный конфиг;

  • базовый API-клиент;

  • доменные клиенты под конкретные сценарии;

  • модели запросов и ответов;

  • генерация тестовых данных;

  • прослойка для передачи метрик в систему отчётности.

Кастомный API-клиент на HTTPX

Для транспортного слоя удобно использовать HTTPX. Он даёт больше гибкости, чем классический Requests, особенно когда нужны тонкие настройки таймаутов, повторное использование соединений и event hooks. Проще говоря, HTTPX лучше подходит для аккуратной инженерной обвязки вокруг нагрузки.

Кастомный клиент полезен по трём причинам:

  • вся логика авторизации и заголовков лежит в одном месте;

  • общие правила обработки ошибок не дублируются в тестах;

  • запросы можно централизованно логировать и измерять.

Сравнение HTTPX и Requests в таком контексте обычно сводится к простому выводу: Requests хорош для быстрых скриптов, а HTTPX удобнее там, где нужен контроль, расширяемость и аккуратная интеграция с инфраструктурой теста.

Pydantic и модели данных

Ещё одна частая причина ложных результатов — кривые входные данные. Тест может вроде бы идти, но часть ошибок вызвана не поведением системы, а плохо собранным телом запроса. 

Что даёт Pydantic:

  • строгую валидацию входных и выходных структур;

  • понятные схемы для команды;

  • быстрый поиск ошибок в полях и типах;

  • меньше шансов испортить тест случайной опечаткой.

Почему не TypedDict и не dataclasses? Они полезны, но слабее там, где нужна жёсткая проверка данных во время выполнения. Для нагрузочного контура это критично. Если вы нагружаете сервис некорректными сущностями, выводы о системе будут искажены.

Faker и реалистичные данные

Когда тесты бьют в API одинаковыми значениями, они быстро перестают быть похожими на жизнь. Faker решает эту проблему: генерирует имена, адреса, идентификаторы, суммы, даты и другие сущности, чтобы сценарии нагрузки выглядели живее.

Ниже — пример, где Pydantic отвечает за структуру запроса, а Faker помогает не гонять тесты на одинаковых данных:

from faker import Faker
from pydantic import BaseModel, EmailStr
fake = Faker("ru_RU")
class CreateUserRequest(BaseModel):
    name: str
    email: EmailStr
    city: str
payload = CreateUserRequest(
    name=
fake.name(),
    email=
fake.email(),
    city=
fake.city(),
)
print(payload.model_dump())

Такой подход полезен сразу по двум причинам. Во-первых, тест не ломается из-за случайно криво собранного тела запроса. Во-вторых, система получает более реалистичные данные, а значит, и результаты проверки становятся ближе к реальному поведению пользователей.

Это особенно полезно в системах, где поведение зависит от состава данных: фильтры, поиск, персональные кабинеты, банковские операции, оформление заказов.

Event hooks и передача метрик в Locust

Один из сильных приёмов — использовать event hooks в HTTPX, чтобы перехватывать момент отправки запроса и получения ответа. Так можно измерять длительность на уровне транспортного клиента, а потом пробрасывать эти данные в Locust.

На практике это работает так:

  1. Перед отправкой фиксируется стартовое время.

  2. После ответа считается длительность.

  3. В систему отчётности передаются статус, размер ответа, ошибка и другие метрики.

Пример ниже показывает саму идею: в request hook сохраняется момент отправки запроса, а в response hook длительность передаётся в Locust:

Для production use потребуется доработка!

import time
import httpx
def make_request_hook():
    def on_request(request: httpx.Request):
        request.extensions["timing"] = {
            "perf": time.perf_counter(),
            "wall": time.time(),
        }
    return on_request
def make_response_hook(environment):
    def on_response(response: httpx.Response):
        timing = response.request.extensions["timing"]
        
environment.events.request.fire(
            request_type=response.request.method,
            name=response.request.url.path,
            response_time=(time.perf_counter() - timing["perf"]) * 1000,
            response_length=len(response.content),
            response=response,
            context={},
            exception=None,
            start_time=timing["wall"],
            url=str(response.request.url),
        )
    return on_response

Смысл здесь не в самой длине кода, а в том, что транспортный слой начинает отдавать метрики в единый контур наблюдения. За счёт этого можно использовать HTTPX как удобный клиент, но не терять сводную статистику, которую собирает Locust.

Здесь же пригодятся замыкания в Python. Через closure можно передать в хук контекст конкретного клиента, окружения или теста без лишних глобальных переменных. Это простой приём, но в больших проектах он сильно упрощает код.

Базовый и специализированные клиенты

Хороший базовый клиент абстрагирует общую HTTP-логику: методы GET/POST, ретраи, заголовки, сериализацию, обработку исключений. А поверх него уже строятся специализированные клиенты. Например, условный OperationsClient может отвечать только за операции предметной области: создание перевода, проверку статуса, подтверждение, получение истории.

Так архитектура остаётся модульной:

  • тесты описывают действия пользователя;

  • клиентский слой управляет транспортом;

  • модели данных отвечают за корректность;

  • Locust управляет нагрузкой и сводной статистикой.

Что важно не упустить перед запуском

Даже хороший инструмент не спасёт, если сама постановка теста слабая. Самые частые ошибки здесь довольно приземлённые:

  • запускают только один синтетический сценарий и считают, что этого достаточно;

  • смотрят только на средние значения, игнорируя хвосты распределения;

  • не фиксируют, на каких данных и в каком окружении запускался тест;

  • не выносят параметры в конфиг;

  • не встраивают проверки в CI/CD, из-за чего результаты невозможно сравнивать от релиза к релизу.

Нормальный минимальный чек-лист перед прогоном выглядит так:

  • определены критичные пользовательские пути;

  • понятны допустимые пороги по ошибкам и задержкам;

  • зафиксированы входные данные и окружение;

  • конфигурация централизована;

  • результаты можно повторить и сравнить позже.

Итог

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

Связка Python, Locust, HTTPX, Pydantic и Faker даёт практичный баланс между скоростью разработки и глубиной анализа. Если строить тесты вокруг реальных пользовательских сценариев, собирать ключевые метрики и регулярно запускать их в одном и том же виде, команда получает не абстрактный отчёт, а рабочий инструмент для улучшения качества сервиса.

Главная мысль простая:

  • измеряйте не «нагрузку вообще», а важные для бизнеса сценарии;

  • смотрите не только на факт ответа, но и на время отклика, ошибки и RPS (requests per second);

  • стройте архитектуру тестов так, чтобы её можно было поддерживать месяцами, а не один спринт.

ссылка на оригинал статьи https://habr.com/ru/articles/1028950/