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

от автора

Опыт работы с тестовыми сценариями достиг высокого уровня удобства благодаря использованию Testcontainers. С помощью Testcontainers появилась возможность удобно создавать окружение для интеграционных тестов (см. статью Изоляция в тестах с Кафкой). Мы можем поднимать контейнеры с различными версиями разных баз данных, брокеров сообщений и т.д. При фокусе на интеграционные тесты, Testcontainers становится незаменимым компонентом среды выполнения. Нагрузочное тестирование, хотя и встречается реже, чем функциональное, может быть более увлекательным с технической точки зрения и с точки зрения инженерного любопытства. Интересно наблюдать за графиками и исследовать детали работы конкретного сервиса. Это редкая, но для меня более захватывающая задача.

Цель данной статьи — продемонстрировать подход к созданию стенда для нагрузочного тестирования в том виде, в котором пишутся обычные интеграционные тесты: в форме spock-тестов с использованием Testcontainers в среде Gradle проекта. В качестве утилит нагрузочного тестирования используются Gatling, WRK и Yandex.Tank.

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

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

В работе с объектом для тестирования можно выделить два подхода:

  • тестирование готовых образов;

  • сборка образов из исходного кода проекта и тестирование.

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

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

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

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

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

Целевой сервис

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

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

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

Дальше по статье я расскажу подробно о деталях, но вначале хотел бы сделать краткий обзор структуры Gradle модуля load-tests для получения информации о составе:

load-tests/ |-- src/ |   |-- gatling/ |   |   |-- scala/ |   |   |   |-- MainSimulation.scala         # Основной файл симуляции Gatling |   |   |-- resources/ |   |   |   |-- gatling.conf                 # Файл конфигурации Gatling |   |   |   |-- logback-test.xml             # Конфигурация Logback для тестирования |   |-- test/ |   |   |-- groovy/ |   |   |   |-- pw.avvero.spring.sandbox/ |   |   |   |   |-- GatlingTests.groovy      # Файл нагрузочного теста с Gatling |   |   |   |   |-- WrkTests.groovy          # Файл нагрузочного теста с Wrk |   |   |   |   |-- YandexTankTests.groovy   # Файл нагрузочного теста с Yandex.Tank |   |   |-- java/ |   |   |   |-- pw.avvero.spring.sandbox/ |   |   |   |   |-- FileHeadLogConsumer.java # Вспомогательный класс логирования в файл |   |   |-- resources/ |   |   |   |-- wiremock/ |   |   |   |   |-- mappings/                # WireMock набор для мокирования внешних сервисов |   |   |   |   |   |-- health.json           |   |   |   |   |   |-- forecast.json |   |   |   |-- yandex-tank/                 # Конфигурация тестирования нагрузки с Yandex.Tank |   |   |   |   |-- ammo.txt |   |   |   |   |-- load.yaml |   |   |   |   |-- make_ammo.py |   |   |   |-- wrk/                         # Скрипты LuaJIT для Wrk |   |   |   |   |-- scripts/                  |   |   |   |   |   |-- getForecast.lua |-- build.gradle

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

Окружение

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

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, Yandex.Tank. Все три инструмента можно использовать независимо друг от друга.

Gatling

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

Описание контейнера для Gatling следующее:

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 для сервиса Sandbox. Хотя, как указано выше, за счет использования сетевых псевдонимов, можно зашить URL прямо в код сценария.

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

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

load-tests/ |-- src/ |   |-- gatling/ |   |   |-- scala/ |   |   |   |-- MainSimulation.scala         # Основной файл симуляции Gatling |   |   |-- resources/ |   |   |   |-- gatling.conf                 # Файл конфигурации Gatling |   |   |   |-- logback-test.xml             # Конфигурация Logback для тестирования

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

Я не буду подробно рассказывать про DSL сценариев этого инструмента, на Хабре много отличных статей на эту тему. С кодом используемого сценария предлагаю ознакомиться в репозитории проекта.

Wrk

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 пишет в лог отчет, на содержимое которого можно завязаться при выставлении ожидания.

Yandex.Tank

Yandex.Tank — это инструмент для нагрузочного тестирования, разработанный в Яндексе. Он поддерживает различные движки нагрузочного тестирования, такие как 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.

  • Логи целевого сервиса.

  • Логи Wrk.

  • JFR (Java Flight Recording).

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

  • Отчет Gatling.

  • Логи Gatling.

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

  • Логи Wrk.

Если использовался Yandex.Tank:

  • Файлы результатов Yandex.Tank, дополнительно выгрузка в Overload.

  • Логи Yandex.Tank.

Структура директории отчетов приведена ниже:

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, который позволяет легко и эффективно организовать окружение для таких тестов.

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

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

Спасибо за внимание к статье, и удачи в вашем стремлении к написанию полезных тестов!


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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *