paint-brush
Все, что вам нужно знать о стенде нагрузочного тестирования на основе тест-контейнеровк@avvero
223 чтения

Все, что вам нужно знать о стенде нагрузочного тестирования на основе тест-контейнеров

к Anton Belyaev15m2024/06/07
Read on Terminal Reader

Слишком долго; Читать

Цель этой статьи — продемонстрировать подход к созданию установки для нагрузочного тестирования так же, как пишутся обычные интеграционные тесты: в виде тестов Spock с использованием Testcontainers в среде проекта Gradle. Используются утилиты нагрузочного тестирования, такие как Gatling, WRK и Яндекс.Танк.
featured image - Все, что вам нужно знать о стенде нагрузочного тестирования на основе тест-контейнеров
Anton Belyaev HackerNoon profile picture

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


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


Цель этой статьи — продемонстрировать подход к созданию установки для нагрузочного тестирования так же, как пишутся обычные интеграционные тесты: в виде тестов Spock с использованием Testcontainers в среде проекта Gradle. Используются утилиты нагрузочного тестирования, такие как Gatling, WRK и Яндекс.Танк.

Создание среды нагрузочного тестирования

Набор инструментов: Gradle + Spock Framework + Testcontainers. Вариант реализации — отдельный модуль Gradle. Используются утилиты нагрузочного тестирования Gatling, WRK и Яндекс.Танк.


Существует два подхода к работе с тест-объектом:

  • Тестирование опубликованных изображений;
  • Сборка образов из исходного кода проекта и тестирование.


В первом случае у нас есть набор нагрузочных тестов, не зависящих от версии проекта и изменений. Этот подход легче поддерживать в будущем, но он ограничивается тестированием только опубликованных изображений. Мы, конечно, можем вручную создавать эти изображения локально, но это менее автоматизировано и снижает воспроизводимость. При запуске в CI/CD без необходимых образов тесты пройдут неудачно.


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


В данной статье рассмотрен первый подход. Благодаря Споку мы можем запускать тесты на нескольких версиях сервиса для сравнительного анализа:

 where: image | _ 'avvero/sandbox:1.0.0' | _ 'avvero/sandbox:1.1.0' | _

Важно отметить, что цель этой статьи — продемонстрировать организацию пространства тестирования, а не полномасштабное нагрузочное тестирование.

Целевая служба

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

Исходный код сервиса, включая Dockerfile, доступен в репозитории проекта Spring-sandbox .

Обзор структуры модуля

Поскольку мы углубимся в детали далее в статье, я хочу начать с краткого обзора структуры модуля load-tests Gradle, чтобы дать представление о его составе:

 load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing | |-- test/ | | |-- groovy/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- GatlingTests.groovy # Gatling load test file | | | | |-- WrkTests.groovy # Wrk load test file | | | | |-- YandexTankTests.groovy # Yandex.Tank load test file | | |-- java/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- FileHeadLogConsumer.java # Helper class for logging to a file | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ # WireMock setup for mocking external services | | | | | |-- health.json | | | | | |-- forecast.json | | | |-- yandex-tank/ # Yandex.Tank load testing configuration | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py | | | |-- wrk/ # LuaJIT scripts for Wrk | | | | |-- scripts/ | | | | | |-- getForecast.lua |-- build.gradle

Репозиторий проекта — https://github.com/avvero/testing-bench .

Среда

Из описания выше мы видим, что у сервиса есть две зависимости: сервис https://external-weather-api.com и база данных. Их описание будет представлено ниже, но начнём с того, что включим взаимодействие всех компонентов схемы в среде Docker — опишем сеть:

 def network = Network.newNetwork()

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

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


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


  • workingDirectory — каталог ресурсов модуля, непосредственно в load-tests/ .


  • reportDirectory — каталог результатов работы, включая метрики и логи. Подробнее об этом будет в разделе об отчетах.

База данных

Служба Sandbox использует Postgres в качестве базы данных. Опишем эту зависимость следующим образом:

 def postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withNetwork(network) .withNetworkAliases("postgres") .withUsername("sandbox") .withPassword("sandbox") .withDatabaseName("sandbox")


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

 'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox'


Структурой базы данных управляет само приложение с помощью Flyway, поэтому никаких дополнительных манипуляций с базой данных в тесте не требуется.

Издевательские запросы к https://external-weather-api.com

Если у нас нет возможности, необходимости или желания запускать реальный компонент в контейнере, мы можем предоставить макет его API. Для сервиса https://external-weather-api.com используется WireMock.


Объявление контейнера WireMock будет выглядеть так:

 def wiremock = new GenericContainer<>("wiremock/wiremock:3.5.4") .withNetwork(network) .withNetworkAliases("wiremock") .withFileSystemBind("${workingDirectory}/src/test/resources/wiremock/mappings", "/home/wiremock/mappings", READ_WRITE) .withCommand("--no-request-journal") .waitingFor(new LogMessageWaitStrategy().withRegEx(".*https://wiremock.io/cloud.*")) wiremock.start()


WireMock требует фиктивной конфигурации. Инструкция withFileSystemBind описывает привязку файловой системы между путем к локальному файлу и путем внутри контейнера Docker. В этом случае каталог "${workingDirectory}/src/test/resources/wiremock/mappings" на локальном компьютере будет смонтирован в /home/wiremock/mappings внутри контейнера WireMock.


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

 load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ | | | | | |-- health.json | | | | | |-- forecast.json


Чтобы гарантировать, что макетные файлы конфигурации правильно загружены и приняты WireMock, вы можете использовать вспомогательный контейнер:

 helper.execInContainer("wget", "-O", "-", "http://wiremock:8080/health").getStdout() == "Ok"


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

 def helper = new GenericContainer<>("alpine:3.17") .withNetwork(network) .withCommand("top")


Кстати, в IntelliJ IDEA версии 2024.1 появилась поддержка WireMock , а IDE предоставляет подсказки при формировании файлов макетной конфигурации.

Конфигурация запуска целевой службы

Объявление сервисного контейнера Sandbox выглядит следующим образом:

 def javaOpts = ' -Xloggc:/tmp/gc/gc.log -XX:+PrintGCDetails' + ' -XX:+UnlockDiagnosticVMOptions' + ' -XX:+FlightRecorder' + ' -XX:StartFlightRecording:settings=default,dumponexit=true,disk=true,duration=60s,filename=/tmp/jfr/flight.jfr' def sandbox = new GenericContainer<>(image) .withNetwork(network) .withNetworkAliases("sandbox") .withFileSystemBind("${reportDirectory}/logs", "/tmp/gc", READ_WRITE) .withFileSystemBind("${reportDirectory}/jfr", "/tmp/jfr", READ_WRITE) .withEnv([ 'JAVA_OPTS' : javaOpts, 'app.weather.url' : 'http://wiremock:8080', 'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox' ]) .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Started SandboxApplication.*")) .withStartupTimeout(Duration.ofSeconds(10)) sandbox.start()

Известные параметры и настройки JVM включают:

  • Сбор информации о событиях сбора мусора.
  • Использование Java Flight Recorder (JFR) для записи данных о производительности JVM.


Дополнительно настраиваются каталоги для сохранения результатов диагностики сервиса.

Ведение журнала

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

 .withLogConsumer(new FileHeadLogConsumer("${reportDirectory}/logs/${alias}.log"))


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

Реализация нагрузочных тестов

Существует множество инструментов для нагрузочного тестирования. В этой статье я предлагаю рассмотреть возможность использования трёх из них: Gatling, Wrk и Яндекс.Танк. Все три инструмента можно использовать независимо друг от друга.

Гатлинг

Gatling — это инструмент нагрузочного тестирования с открытым исходным кодом, написанный на Scala. Он позволяет создавать сложные сценарии тестирования и предоставляет подробные отчеты. Основной файл моделирования Gatling подключается как ресурс Scala к модулю, что позволяет удобно работать с использованием всего спектра поддержки IntelliJ IDEA, включая подсветку синтаксиса и навигацию по методам для справки по документации.


Конфигурация контейнера для Гатлинга следующая:

 def gatling = new GenericContainer<>("denvazh/gatling:3.2.1") .withNetwork(network) .withFileSystemBind("${reportDirectory}/gatling-results", "/opt/gatling/results", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/scala", "/opt/gatling/user-files/simulations", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/resources", "/opt/gatling/conf", READ_WRITE) .withEnv("SERVICE_URL", "http://sandbox:8080") .withCommand("-s", "MainSimulation") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Please open the following file: /opt/gatling/results.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ); gatling.start()

Настройка практически идентична другим контейнерам:

  • Смонтируйте каталог для отчетов из reportDirectory .
  • Подключите каталог для файлов конфигурации workingDirectory .
  • Подключите каталог для файлов моделирования workingDirectory .


Дополнительно в контейнер передаются параметры:

  • Переменная среды SERVICE_URL со значением URL-адреса службы песочницы. Хотя, как упоминалось ранее, использование сетевых псевдонимов позволяет жестко закодировать URL-адрес непосредственно в коде сценария.


  • Команда -s MainSimulation для запуска конкретной симуляции.


Напоминаем структуру исходного файла проекта, чтобы понять, что и куда передается:

 load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing

Поскольку это конечный контейнер, и мы ожидаем получить результаты после его завершения, мы устанавливаем ожидание .withRegEx(".*Please open the following file: /opt/gatling/results.*") . Тест завершится, когда это сообщение появится в логах контейнера или через 60 * 2 секунды.


Я не буду углубляться в DSL сценариев этого инструмента. Посмотреть код использованного сценария можно в репозитории проекта .

работа

Wrk — простой и быстрый инструмент нагрузочного тестирования. Он может генерировать значительную нагрузку при минимальных ресурсах. Ключевые особенности включают в себя:

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


Конфигурация контейнера для Wrk следующая:

 def wrk = new GenericContainer<>("ruslanys/wrk") .withNetwork(network) .withFileSystemBind("${workingDirectory}/src/test/resources/wrk/scripts", "/tmp/scripts", READ_WRITE) .withCommand("-t10", "-c10", "-d60s", "--latency", "-s", "/tmp/scripts/getForecast.lua", "http://sandbox:8080/weather/getForecast") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Transfer/sec.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) wrk.start()


Чтобы Wrk работал с запросами к сервису Sandbox, необходимо описание запроса через Lua-скрипт, поэтому монтируем каталог скриптов из workingDirectory . С помощью команды запускаем Wrk, указав скрипт и URL-адрес целевого метода сервиса. Wrk записывает отчет в журнал на основе своих результатов, который можно использовать для определения ожиданий.

Яндекс.Танк

Яндекс.Танк — инструмент нагрузочного тестирования, разработанный Яндексом. Он поддерживает различные механизмы нагрузочного тестирования, такие как JMeter и Phantom. Для хранения и отображения результатов нагрузочного тестирования можно использовать бесплатный сервис Overload .


Вот конфигурация контейнера:

 copyFiles("${workingDirectory}/src/test/resources/yandex-tank", "${reportDirectory}/yandex-tank") def tank = new GenericContainer<>("yandex/yandex-tank") .withNetwork(network) .withFileSystemBind("${reportDirectory}/yandex-tank", "/var/loadtest", READ_WRITE) .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Phantom done its work.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) tank.start()


Конфигурация нагрузочного тестирования для Sandbox представлена двумя файлами: load.yaml и ammo.txt . В составе описания контейнера файлы конфигурации копируются в reportDirectory , который будет смонтирован как рабочий каталог. Вот структура исходных файлов проекта, чтобы понять, что и куда передается:

 load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- yandex-tank/ | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py

Отчеты

Результаты тестов, включая записи и журналы производительности JVM, сохраняются в каталоге build/${timestamp} , где ${timestamp} представляет собой временную метку каждого запуска теста.


Для просмотра будут доступны следующие отчеты:

  • Журналы сборщика мусора.
  • Журналы WireMock.
  • Целевые журналы обслуживания.
  • Журналы работы.
  • JFR (запись полета Java).


Если использовался Гатлинг:

  • Отчет Гатлинга.
  • Гатлинговые бревна.


Если использовался Wrk:

  • Журналы работы.


Если использовался Яндекс.Танк:

  • Файлы результатов Яндекс.Танка, с дополнительной загрузкой в Overload .
  • Журналы Яндекс.Танка.


Структура каталогов для отчетов следующая:

 load-tests/ |-- build/ | |-- ${timestamp}/ | | |-- gatling-results/ | | |-- jfr/ | | |-- yandex-tank/ | | |-- logs/ | | | |-- sandbox.log | | | |-- gatling.log | | | |-- gc.log | | | |-- wiremock.log | | | |-- wrk.log | | | |-- yandex-tank.log | |-- ${timestamp}/ | |-- ...

Заключение

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


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


Предоставленные примеры конфигурации Gatling, Wrk и Яндекс.Танка, а также настройки контейнера демонстрируют, как эффективно интегрировать различные инструменты и управлять параметрами тестирования.


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


Спасибо за внимание к этой статье и удачи в написании полезных тестов!