惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
Blog — PlanetScale
Blog — PlanetScale
GbyAI
GbyAI
C
Check Point Blog
腾讯CDC
Stack Overflow Blog
Stack Overflow Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The GitHub Blog
The GitHub Blog
A
About on SuperTechFans
Recent Announcements
Recent Announcements
L
LangChain Blog
Microsoft Azure Blog
Microsoft Azure Blog
小众软件
小众软件
J
Java Code Geeks
博客园_首页
Jina AI
Jina AI
美团技术团队
H
Help Net Security
MyScale Blog
MyScale Blog
Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
人人都是产品经理
人人都是产品经理
Y
Y Combinator Blog
S
SegmentFault 最新的问题

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Локальное нагрузочное тестирование в Java с использование...
MishaBucha · 2026-05-14 · via Все публикации подряд на Хабре

Всем привет! Меня зовут Михаил, я работаю главным экспертом в ОТП Банке.

Я люблю тестировать свои решения и почти всегда пишу unit- и integration-тесты. Но вот с нагрузочным тестированием ситуация обычно совсем другая: о нем вспоминают ближе к релизу, когда архитектуру уже поздно менять.

В какой-то момент я поймал себя на мысли:

А как вообще заранее понять, сколько ресурсов будет потреблять сервис под нагрузкой?

Сколько памяти съест приложение? Когда упрется в CPU? Как поведет себя БД при разном кол-ве запросов?

Чтобы ответить на эти вопросы, я написал небольшую библиотеку для локального нагрузочного тестирования на Java Virtual Threads. Она запускает большое количество задач, собирает метрики и формирует отчет - прямо в консоли или в CSV.

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

Что тестируем сегодня

Я написал небольшой и очень простой код. Создаем пользователя в бд и отправляем запрос на регистрацию в смежную систему:

@Service
@RequiredArgsConstructor
public class UserService {

  private final UserRepository userRepository;

  private final OtherSystemClient otherSystemClient;

  @Transactional
  public void createUser() {
    String randomEmail = UUID.randomUUID() + "@gmail.com";
    User user = buildUser(randomEmail);
    otherSystemClient.registrationUser(new RegistrationDto(randomEmail));
    userRepository.save(user);
  }

  private User buildUser(String randomEmail) {
    return User.builder()
        .email(randomEmail)
        .status(UserStatus.NEW)
        .isActive(true)
        .name("Легенда")
        .build();
  }
}

@Component
public class OtherSystemClient {

  public RegistrationResponseDto registrationUser(RegistrationDto dto) {
    LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(10));
    return new RegistrationResponseDto(UserStatus.SUCCESS.name(), null);
  }
}

Самый очевидный вариант - поднять приложение и начать дергать HTTP endpoint через Postman, JMeter или любой другой инструмент.

Но здесь появляются проблемы:

  • не вся логика доступна через endpoint;

  • иногда хочется протестировать конкретный сервис или даже небольшой участок кода;

  • для локальной проверки поднимать полноценный нагрузочный стенд часто слишком дорого и долго.

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

High-load-tester библиотека

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

Например:

  • сервис;

  • repository;

  • интеграция с БД;

  • вызов внешнего API;

  • или даже отдельный метод.

При этом тест можно запускать:

  • локально;

  • внутри integration tests;

  • или как часть CI.

Ниже - пример integration-теста.

Внутри TestContext поднимается PostgreSQL через Testcontainers, а сам тест запускается в обычном @SpringBootTest.

class UserServiceLoadCurveIT extends TestContext {

  @Autowired
  private UserService userService;

  @Test
  public void shouldCreateUsersWithMediumRpsCurrentWork() {
    LoadTestReport report = RunnableChecker.run(
        RunnableTesting.builder()
            .requestCount(1000)
            .task(userService::createUser)
            .build()
    );
    Assertions.assertTrue(report.getErrors().isEmpty());
  }
}

И все, чтобы после этого мы получили полную сводку метрик, пример:

================= LOAD TEST REPORT =================

Total requests: 10000
Completed requests: 10000

Total duration: 1630 ms
Throughput: 6134.97 requests/sec

---------------- LATENCY ----------------

Average latency: 1165.37 ms
P95 latency: 1482.79 ms
P99 latency: 1491.56 ms

---------------- RESOURCES ----------------

Peak CPU usage: 51.45 %
Peak heap memory usage: 389 MB
Heap limit (MB): —

---------------- SNAPSHOTS ----------------

Collected metrics snapshots: 17

---------------- ERRORS ----------------

Errors count: 9763

====================================================

Какие метрики собираются

Идея простая - не перегружать отчет сотнями метрик, а дать набор ключевых:

  • насколько быстро система обрабатывает нагрузку;

  • где находится latency (avg / p95 / p99);

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

  • есть ли ошибки и в каком объеме.

@Builder
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class LoadTestReport {

  /** Запрошенное число выполнений задачи (как в конфигурации). */
  private long totalRequests;

  /** Фактически завершённых задач после блока finally исполнителя. */
  private long completedRequests;

  /** Длительность всего прогона по настенным часам, миллисекунды. */
  private long durationMs;

  /** Завершённые запросы в секунду (число завершённых / длительность в секундах). */
  private double throughput;

  /** Средняя латентность одной задачи, миллисекунды. */
  private double avgLatencyMs;

  /** Оценка 95-го персентиля латентности, миллисекунды. */
  private double p95LatencyMs;

  /** Оценка 99-го персентиля латентности, миллисекунды. */
  private double p99LatencyMs;

  /** Максимальная оценка загрузки CPU по снимкам метрик, в процентах. */
  private double peakCpuLoad;

  /** Максимальный зарегистрированный объём heap по снимкам, мегабайты. */
  private long peakMemoryMb;

  /** Временной ряд снимков метрик во время прогона (может быть пустым). */
  private List<MetricsSnapshot> snapshots;

  /** Краткие сообщения об ошибках (ожидание future, лимит памяти и т.п.). */
  private List<String> errors;

  /** Лимит heap (МБ), скопированный из конфигурации; если не задавали — null. */
  private Long heapLimitMb;
  // логика сбора метрик
}

Сам тест описывается через простой конфиг:

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class RunnableTesting {

  /** Код одной единицы нагрузки; вызывается столько раз, сколько задано в поле ниже. */
  private Runnable task;

  /** Сколько раз отправить задачу в пул виртуальных потоков. */
  private int requestCount;

  /** Верхний предел используемого heap (МБ), или null — не проверять. */
  private Long heapLimitMb;
}

Сейчас библиотека поддерживает три базовых параметра:

  • сама нагрузочная задача;

  • количество запусков;

  • лимит heap (если нужно симулировать ограниченные условия).

Зачем это все нужно?

Давайте смоделируем довольно типичную ситуацию.

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

Хочется быстро ответить на вопросы:

  • что будет при высокой конкуренции за соединения?

  • как система деградирует под нагрузкой?

  • где начинаются блокировки и ожидания?

  • как ведет себя код при параллельных транзакциях?

Для примера ограничим пул подключений к БД:

spring:
  jpa:
    hibernate:
      ddl-auto: create-drop
  datasource:
    hikari:
      # Для демонстрации исчерпания пула: мало слотов + короткое ожидание выдачи соединения
      maximum-pool-size: 20
      minimum-idle: 0
      connection-timeout: 500

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

================= LOAD TEST REPORT =================

Total requests: 1000
Completed requests: 1000

Total duration: 587 ms
Throughput: 1703.58 requests/sec

---------------- LATENCY ----------------

Average latency: 440.37 ms
P95 latency: 566.43 ms
P99 latency: 567.26 ms

---------------- RESOURCES ----------------

Peak CPU usage: 27.73 %
Peak heap memory usage: 68 MB
Heap limit (MB): —

---------------- SNAPSHOTS ----------------

Collected metrics snapshots: 6

---------------- ERRORS ----------------

Errors count: 517

====================================================

Также я еще вывывел ошибки:
[org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction и бла бла бла]

С помощью такого подхода можно очень быстро менять условия эксперимента:

  • уменьшать или увеличивать pool;

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

  • проверять поведение кода при перегрузке БД;

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

Про ресурсы и честность локальных тестов

Когда начинаешь гонять нагрузочные тесты локально, быстро всплывает очевидная проблема:
твоя машина ≠ продакшен.

И это важно понимать.

Есть разные способы приблизить локальные тесты к реальности:

  • ограничение CPU (cgroups / Docker)

  • лимиты памяти (-Xmx, container limits)

  • имитация задержек сети

  • throttling через rate limit

  • запуск в контейнерах

  • использование выделенных стендов

Но важно другое: эта библиотека не пытается заменить полноценный load testing инструмент.

Ее задача другая:

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

при необходимости тест можно дополнительно “приземлить” к реальным условиям - через Docker лимиты, настройку JVM или запуск в CI-окружении, но это уже слой над библиотекой, а не её ответственность.

Как все это работает

Теперь коротко разберем внутреннее устройство.

1. Запуск нагрузки через virtual threads

Все задачи отправляются в Executors.newVirtualThreadPerTaskExecutor():

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    ...
}

Это позволяет дешево создать тысячи конкурентных задач без классических thread pool ограничений.

2. Сбор метрик во время выполнения

Параллельно запускается отдельный ScheduledExecutorService, который каждые 100 мс снимает:

  • CPU usage

  • heap memory

  • количество активных задач

  • прогресс выполнения

3. Постобработка результатов

После завершения прогона считаются:

  • latency (avg / p95 / p99)

  • throughput

  • пики CPU и памяти

  • ошибки выполнения

Возможность собирать отчеты в виде CSV

Иногда одного вывода в консоль недостаточно.

Например, когда нужно:

  • сравнить несколько прогонов;

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

  • или просто сохранить историю изменений производительности.

Для этого в библиотеке есть возможность сохранить отчет в CSV:

  @Test
  public void currentTest() {
    LoadTestReport report = RunnableChecker.run(
        RunnableTesting.builder()
            .requestCount(1000)
            .task(userService::createUser)
            .build()
    );
    CsvReportGenerator.generateRunnableReport("report/load-report.csv", report);
  }

CSV содержит все ключевые метрики прогона, поэтому его можно:

  • открыть в Excel / Google Sheets;

  • сравнить разные конфигурации;

  • построить свои графики поверх данных.

Итог

Эта библиотека - не попытка заменить полноценные инструменты нагрузочного тестирования.

Она про другое: быстрые локальные эксперименты, когда нужно понять, как ведет себя конкретный кусок кода под конкурентной нагрузкой, без подготовки стендов и сложной инфраструктуры.

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

  • что будет, если увеличить нагрузку в 10–100 раз?

  • где упрется система: CPU, память или база?

  • как быстро деградирует код при конкуренции?

Virtual threads здесь выступают просто как удобный механизм для генерации высокой конкуренции без накладных расходов на потоки.

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

Мне было интересно исследовать, как virtual threads ведут себя под высокой нагрузкой и можно ли сделать JVM-native performance framework для тестирования Runnable/Callable без HTTP-слоя.

В процессе получился небольшой experimental framework, которого пока нет в maven.

Сама библиотечка - https://github.com/MishaBucha/high-load-tester/tree/develop

Всем спасибо за внимание!)