Стенд для нагрузочного тестирования на Testcontainers
Стенд для нагрузочного тестирования на Testcontainers (материалы для статьи)
Использование Testcontainers кардинально улучшило процесс работы с тестовыми сценариями. Благодаря этому инструменту, создание окружений для интеграционных тестов стало проще (см. статью Изоляция в тестах с Kafka). Теперь мы можем легко поднимать контейнеры с разными версиями баз данных, брокеров сообщений и других сервисов. Для интеграционных тестов 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 мы можем запускать тесты на нескольких версиях сервиса для сравнительного анализа:
Важно отметить, что цель статьи — демонстрация организации пространства для тестирования, а не полновесной нагрузки.
Целевой сервис
В качестве объекта для тестирования давайте возьмем простой 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://external-weather-api.com и база данных. Их описание будет предоставлено ниже, но начнем с того, чтобы предоставить возможность всем компонентам схемы коммуницировать в докер среде — опишем сеть:
и предоставим сетевые псевдонимы для каждой компоненты. Это чрезвычайно удобно и позволит нам статически описывать параметры интеграции.
Зависимости типа WireMock и непосредственно сами утилиты нагрузочного тестирования требуют настройки для работы. Это могут быть как параметры, которые можно передать в контейнер, так и целые файлы, директории с которыми нужно примонтировать к контейнерам. Кроме этого, нам необходимо забирать результаты их работы из контейнеров. Для решения этих задач необходимо предоставить два набора директорий:
workingDirectory
— директория ресурсов модуля, непосредственно по адресуload-tests/
.reportDirectory
— директория для результатов работы, включая метрики и логи. Подробнее про это будет в разделе статьи про отчеты.
База данных
В качестве базы данных сервис Sandbox использует Postgres. Опишем эту зависимость следующим образом:
В декларации указан сетевой псевдоним postgres
, по нему сервис Sandbox и будет обращаться к базе. Для завершения описания интеграции с базой в сервис нужно передать параметры
Структура базы формируется самим приложением через Flyway, поэтому никаких дополнительных манипуляций с базой в тесте не нужны.
Мокирование запросов к https://external-weather-api.com
Если у нас нет возможности, необходимости или желания поднять реальный компонент в контейнере, можно предоставить мок для его API. Для сервиса https://external-weather-api.com используется WireMock.
Декларация контейнера WireMock будет выглядеть следующим образом:
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, можно воспользоваться вспомогательным контейнером:
Сам вспомогательный контейнер описан следующим образом:
К слову, в IntelliJ IDEA версии 2024.1 появилась поддержка WireMock, и IDE дает подсказки при формировании файлов настройки мокирования.
Настройка запуска целевого сервиса
Декларация контейнера сервиса Sandbox выглядит следующим образом:
Из интересного тут можно отметить наличие следующих параметров и настроек JVM:
- Сборка информации о событиях сборки мусора.
- Использование Java Flight Recorder (JFR) для записи данных о работе JVM.
Кроме того, настроены директории для сохранения результатов диагностики сервиса.
Логирование
Если необходим вывод логов работы любого используемого контейнера в файл, а это скорее всего понадобится именно на этапе написания тестовых сценариев и настройки, можно воспользоваться следующей инструкцией при описании контейнера:
В данном случае применяется класс FileHeadLogConsumer
, который позволяет писать в файл ограниченный объем лога. Это делается потому, что весь лог скорее всего никогда не понадобится в рамках сценариев нагрузочного тестирования, а частичного будет достаточно, чтобы оценить, правильно ли сервис работает или нет.
Реализация нагрузочных тестов
Существует множество инструментов для нагрузочного тестирования (некоторые из них разобраны в статье Обзор инструментария для нагрузочного и перформанс-тестирования). В данной статье я предлагаю рассмотреть использование трех из них: Gatling, Wrk, Yandex.Tank. Все три инструмента можно использовать независимо друг от друга.
Gatling
Gatling — это open-source инструмент для нагрузочного тестирования, написанный на Scala. Он позволяет создавать сложные сценарии тестирования и предоставляет подробные отчеты. Основной файл симуляции Gatling подключен в виде scala ресурса к модулю, что позволяет удобно работать с ним, используя весь спектр поддержки со стороны IntelliJ IDEA, включая подсветку синтаксиса и переход по методам для получения справки из документации.
Описание контейнера для Gatling следующее:
Схема настройки практически идентична прочим контейнерам:
- Монтируем директорию для отчетов с
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 следующее:
Для работы Wrk с запросами к сервису Sandbox требуется описание запроса через Lua-скрипт, для этого монтируем директорию скриптов с workingDirectory
. С помощью команды запускаем Wrk, указывая скрипт и URL метода целевого сервиса. По результатам своей работы Wrk пишет в лог отчет, на содержимое которого можно завязаться при выставлении ожидания.
Yandex.Tank
Yandex.Tank — это инструмент для нагрузочного тестирования, разработанный в Яндексе. Он поддерживает различные движки нагрузочного тестирования, такие как JMeter и Phantom. Для хранения и отображения результатов нагрузочных тестов можно использовать специальный бесплатный сервис Overload.
Ниже представлено описание контейнера:
Конфигурация тестирования для 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 и настройки контейнеров демонстрируют, как можно эффективно интегрировать различные инструменты и управлять параметрами тестирования. Кроме того, был описан процесс логирования и сохранения результатов тестов, что важно для анализа и улучшения производительности приложений. В будущем возможно расширение данного подхода для поддержки более сложных сценариев и интеграции с другими инструментами для мониторинга и анализа.
Спасибо за внимание к статье, и удачи в вашем стремлении к написанию полезных тестов!