Мониторим и нагружаем приложения Jmix

Инструменты мониторинга работы приложений могут быть полезны не только DevOps«ам, но и разработчикам для исследования производительности приложения в поиске, например узких мест в его работе, поэтому в данной статье мы не только настроим мониторинг для Jmix-приложения, но и подготовимся к его синтетическому нагрузочному тестированию. Особенностью платформы Jmix в силу того, что на использует фреймворк Vaadin, является тот факт, что работа UI интегрирована с бекендом, но это также значит и то, что и метрики можно использовать прозрачно, т.е. замерять ими работу интерфейсного слоя. 

32568adccdfc21050d49a1e42c4e2724.png

Создаем проект и включаем метрики

Для разработки вам понадобиться среда разработки IntelliJ IDEA с установленным плагином Jmix.

В качестве примера возьмем jmix-onboarding. Это такой более полноценный Hello World, т.е. демонстрационный проект.

Для его получения себе на компьютер в среде разработки выберем File → New → Project from Version Control и в появившемся диалоге укажем адрес репозитория: https://github.com/jmix-framework/jmix-onboarding-2.git

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

Для подключения Actuator добавим в build.gradle его зависимости:

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'

По умолчанию метрики отключены. Чтобы включить их в application.properties добавим следующие строчки:

management.endpoints.web.exposure.include=prometheus,health,info,metrics
management.endpoint.health.show-details=always

Настраиваем авторизацию

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

@Configuration
public class ActuatorSecurityConfiguration {
    @Bean
    @Order(JmixSecurityFilterChainOrder.FLOWUI - 10)
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/actuator/**")
                .authorizeHttpRequests((authorize) -> authorize.requestMatchers("/actuator/**").permitAll());
        return http.build();
    }
}

Создадим подкаталог docker и в нем файл docker-compose.yml для проекта с PostgreSQL:

services:
  jmix-onboarding-postgres:
    container_name: jmix-onboarding-postgres
    image: postgres:latest
    ports:
      - "5432:5432"
    volumes:
      - postgres:/var/lib/postgresql/data
      - storage:/storage
    environment:
      - POSTGRES_USER=onboarding
      - POSTGRES_PASSWORD=onboarding
      - POSTGRES_DB=onboarding
  Jmix-onboarding:
    container_name: jmix-onboarding
    image: jmix-onboarding:0.0.1-SNAPSHOT
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=jmix-onboarding-postgres
      - DB_USER=onboarding
      - DB_PASSWORD=onboarding
      - DB_PORT=5432
      - DB_NAME=onboarding
    depends_on:
      - jmix-onboarding-postgres
volumes:
  postgres: {}
  storage: {}

Создадим профиль для продуктива в файле application-prod.properties с датасорсом для PostgreSQL из Docker:

main.datasource.url = jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
main.datasource.username = ${DB_USER}
main.datasource.password =${DB_PASSWORD}
jmix.localfs.storage-dir = /storage

Датасорс для разработки вытащим из application.properties в application-dev.properties

main.datasource.url = jdbc:hsqldb:file:.jmix/hsqldb/onboarding
main.datasource.username = sa
main.datasource.password =

Для поддержки PostgreSQL также надо добавить драйвер СУБД в секцию dependencies файла build.gradle

    runtimeOnly 'org.postgresql:postgresql'

Для того, чтобы профили включались автоматически добавим в build.gradle передачу параметров профиля:

bootRun {
    args = ["--spring.profiles.active=dev"]
}

tasks.named("bootBuildImage") {
    environment["BPE_APPEND_JAVA_TOOL_OPTIONS"] = " -Dspring.profiles.active=prod"
}

Собираем Docker-образ:

./gradlew bootBuildImage -Pvaadin.productionMode=true

Запускаем контейнеры:

 docker compose -f ./docker/docker-compose.yml up -d

Проверяем, что приложение отдает данные для мониторинга:

curl http://localhost:8080/actuator/prometheus

Запускаем мониторинг

Создаем конфигурацию docker/config/grafana/provisioning/datasources/all.yaml с датасорсом прометеуса:

apiVersion: 1
 
datasources:
  - name: Prometheus
    label: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true

Создаем Docker-файл для Graphana docker/config/grafana/Dockerfile

FROM grafana/grafana
ADD ./provisioning /etc/grafana/provisioning

Создаем docker/config/prometheus.yml

scrape_configs:
  - job_name: 'onboarding_monitoring'
    scrape_interval: 5s
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['jmix-onboarding:8080']

Создаем docker/monitoring.yml

services:
  grafana:
    build: './config/grafana'
    user: root
    ports:
      - 3000:3000
    volumes:
      - ./grafana:/var/lib/grafana
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
  prometheus:
    image: prom/prometheus
    user: root
    ports:
      - 9090:9090
    volumes:
      - ./config/prometheus.yml:/etc/prometheus/prometheus.yml
      - ./prometheus:/prometheus

Запускаем мониторинг:

docker compose -f ./docker/monitoring.yml start

Для проверки можно зайти веб-интерфейс Prometheus, открыв в браузере http://localhost:9090 и убедиться, что статус работы сборщика метрик там позитивный.

Настраиваем панели и графики

Для Grafana есть готовый JVM-дашборд (https://grafana.com/grafana/dashboards/4701-jvm-micrometer/.), который сразу выглядит красиво. Но мы пойдем немного дальше и будем делать дашборды с только специфичными для работы подсистем фреймворка Jmix данными.

В Jmix уже встроен сбор метрик для основных структурных единиц — представлений (Views) и загрузчиков данных (Data Loaders) и когда вы добавляется свои экраны и пишете в них логику для работы с данными, то автоматически получаете возможность отслеживать их производительность отдельно.

Для представлений название метрики jmix.ui.views

Поддерживаются тэги: lifeCycle, view мониторятся следующие фазы жизненного цикла:

  • Create

  • Load

  • Init

  • Before show

  • Ready

  • Inject

  • Before close

  • After close

Результат для Prometheus будет выглядеть примерно так:

jmix_ui_views_seconds_max{lifeCycle="load",view="User.list",} 0.0412709

Для загрузчиков данных название метрики jmix.ui.data

Поддерживаются тэги: lifeCycle, view, dataLoader мониторятся следующие фазы жизненного цикла:

  • Pre-load

  • Load

  • Post-load

Запись метрик работает только для загрузчиков данных у которых определен Id

Пример метрики даталоадера для Prometheus:

jmix_ui_data_seconds_max{dataLoader="usersDl",lifeCycle="load",view="User.list",} 0.005668899

Заходим на http://localhost:3000 добавляем дашборд и в него метрики из датасорса Prometheus

jmix_ui_data_seconds_max{dataLoader="usersDl"} -> dashboards

Также это можно сделать через Explore выбрав соответствующий датасорс и вводя имя метрики и ее параметры вы элементы выбора. По нажатию Run query должен построиться предпросмотр графика. Но это произойдет только если уже есть какие-то данные, т.е. надо авторизоваться в приложении, и хотя бы зайти на управление пользователями.

6b5070a98096686304e75cd1822b5fc6.png

Добавляем свои метрики

При помощи API Micrometer вы можете добавлять свои метрики: таймеры, счетчики. Добавим такой в слушатель для событий сущности.

a0e3a33d5b4768a98d74d1089856356b.png6a42e3fd005952b64d0c2e6e1f9c60a7.png

И добавим счетчик сохранений сущности User, при помощи следующего кода:

package com.company.onboarding.listener;

import com.company.onboarding.entity.User;
import io.jmix.core.event.EntitySavingEvent;
import io.micrometer.core.instrument.MeterRegistry;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class UserEventListener {

    @Autowired
    MeterRegistry meterRegistry;

    @EventListener
    public void onUserLoading(final EntitySavingEvent event) {
        meterRegistry.counter("onboarding_on_user_save").increment();
    }
}

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

bded56d08c027580a7435644d36631e9.png

Включаем отображение в REST для проекта

Для Jmix есть аддон, который позволяет автоматически отображать сущности, интерфейсы к ним и другие сервисные методы в веб-сервисы, с автоматическим документированием в OpenAPI. Такой подход позволяет использовать фреймворк как основу для разработки сервисов с минимальными затратами на генерацию интерфейса управления данными (backoffice) и достаточно развитыми возможностями кастомизации как интерфейсной части, так и связанной с ними бизнес-логики.

Для включения аддона добавим в build.properties зависимости модулей:

implementation 'io.jmix.authserver:jmix-authserver-starter' 
implementation 'io.jmix.rest:jmix-rest-starter'

Добавим минимальную конфигурацию безопасности для сервиса, в файле application.properties:

# The client id is my-client 
spring.security.oauth2.authorizationserver.client.myclient.registration.client-id=my-client
# The client secret (password) is my-secret 
spring.security.oauth2.authorizationserver.client.myclient.registration.client-secret={noop}my-secret 
# Enable Client Credential grant for the my-client 
spring.security.oauth2.authorizationserver.client.myclient.registration.authorization-grant-types=client_credentials 
# Client credentials must be passed in the Authorization header using the HTTP Basic authentication scheme 
spring.security.oauth2.authorizationserver.client.myclient.registration.client-authentication_methods=client_secret_basic 
# Use opaque tokens instead of JWT 
spring.security.oauth2.authorizationserver.client.myclient.token.access-token-format=reference 
# access token time-to-live 
spring.security.oauth2.authorizationserver.client.myclient.token.access-token-time-to-live=24h
jmix.authserver.client.myclient.client-id = my-client 
jmix.authserver.client.myclient.resource-roles = user-management, rest-minimal

Также надо создать класс роли доступа в пакете com.company.onboarding.security:

@ResourceRole(name = "User management", code = UserManagementRole.CODE, scope = "API")
public interface UserManagementRole {

    String CODE = "user-management";

    @EntityAttributePolicy(entityClass = User.class, attributes = "*", action = EntityAttributePolicyAction.MODIFY)
    @EntityPolicy(entityClass = User.class, actions = EntityPolicyAction.ALL)
    void user();
}

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

curl -X POST http://localhost:8080/oauth2/token --basic --user my-client:my-secret -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=client_credentials"

И получим список пользователей подставив полученный токен в новый запрос:

curl -X GET http://localhost:8080/rest/entities/User -H "Authorization: Bearer "

Устанавливаем JMeter

JMeter — инструмент нагрузочного тестирования, который позволял проектировать тесты в режиме no/low-code еще до того как это стало мейнстримом.

Для запуска JMeter потребуется установить JDK 11 и сделать так, чтобы она использовалась при запуске.
Мне для этого потребовалось сделать небольшой шелл-скрипт запуска в /usr/local/bin c таким содержанием:

#!/bin/bash
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/
/opt/apache-jmeter-5.6.3/bin/jmeter

Запускаем тесты

Добавим переменные

ded7af4ecc275101c34756fc9aa4eb07.png

Создадим Thread Group с названием, например, REST Users

Уберем галочку с Infinite и определим 20 повторений для 10и параллельных пользователей

547f7e1e0f659a06c8e367a7cc403700.png

Аутентификация

Поскольку наш сервис закрыт на авторизацию, тредгруппам JMeter понадобиться проходить эту процедуру хотя бы однажды за сеанс. Для реализации этого элемент If Controller из группы Logic Controller. Назовем его Authenticate.

Добавим в него HTTP семплер для POST-запроса выполняемого на адрес /oauth2/token

Из параметров потребуется передавать только grant_type=client_credentials

Параметры подключения оформим из переменных, которые указывали ранее на уровне Thread Group«ы, подстановка которых оформляется как ${__V (имя_переменной)}

3cc28974da757fe759b729bdfc50d53b.png

Нам также потребуется передавать значение логина и пароля в заголовке запроса Authorization. Для этого в Authenticate добавим элемент HTTP Header Manager и зададим ему значение заголовка Authorization Basic bXktY2xpZW50Om15LXNlY3JldA==.

473c11b6c2a4066196ef35a33e9e3ad7.png

Это значение получается из строк логина и пароля соединенных символом »:» и закодированным в Base64. Я воспользовался онлайн сервисом-конвертором чтобы получить строку для моих значений. И встроенным HTTP клиентом IntelliJ IDEA чтобы отладить параметры запроса.

06f3f030c7725f18f66976330c3214a2.png

Для выполнения условия только при первом запросе в параметрах If Controller зададим соответствующее условие:

${__groovy(ctx.getThreadNum() == 0 &&  vars.getIteration() == 1,)}

Также добавим в него JSON Extractor из Post Processors получающий токен с помощью выражения

$.access_token

В переменную access_token

ebd5e27b082b993d348c18ee2da250e2.png

И сохранялку значения BeanShell Assertation с выражением:

${__setProperty(access_token, ${access_token})};

Запрос для списка пользователей

В группу добавим HTTP сэмплер Users List для получения пользователей с ресурса rest/entities/User

da68cca95ffb7a487f8566b874017e0f.png

Также надо добавить HTTP Header Manager

И BeanShell PreProcessor для установки заголовка с токеном в таким коде:

import org.apache.jmeter.protocol.http.control.Header;

var authHeader = sampler.getHeaderManager().getFirstHeaderNamed("Authorization");
if (authHeader == null) {
    sampler.getHeaderManager().add(new Header("Authorization", "Bearer" + props.get("access_token")));
} else {
    authHeader.setValue("Bearer" + props.get("access_token"));
}

В родительскую группу добавим HTTP Cookie Manager из подменю Add → Config Element.

Также добавим Summary Report из подменю Add → Listener.

Запуск тестов

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

После запуска самое время посмотреть в мониторинг.

Остановив выполнение запросов кнопкой «Стоп», в секции Summary Report появятся данные по выполненным запросам.

Тестирование пользовательских интерфейсов

Разработка UI-тестов

JMeter также умеет выполнять тесты при помощи WebDriver, который использует реальные браузеры и выполняет тестирование за счет скриптования или записи действий пользователей в них.

На момент написания статьи ChromeDriver требовал достаточно устаревшей версии 114, совмещать которую на компьютере разработчика с актуальной представлялось достаточно нетривиальным, поэтому сработал альтернативный вариант с использованием GeckoDriver и дистрибутивом Firefox ESR

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

838ba1e243d32c793170eb3d6531ce15.png

Например, скрипт входа в систему для Jmix 2.x может иметь такой код:

var props = WDS.vars

WDS.sampleResult.sampleStart()
WDS.browser.get(props.get('jmix_proto') + '://' + props.get('jmix_host') + ':' + props.get('jmix_port'))

var pkg = JavaImporter(org.openqa.selenium, org.openqa.selenium.support.ui)
var wait = new pkg.WebDriverWait(WDS.browser, java.time.Duration.ofSeconds(100))

// wait for login screen
wait.until(pkg.ExpectedConditions.presenceOfElementLocated(pkg.By.cssSelector('input[name=username]')))

var login = WDS.browser.findElement(pkg.By.cssSelector('input[name=username]'))
var password = WDS.browser.findElement(pkg.By.cssSelector('input[name=password]'))
var submit = WDS.browser.findElement(pkg.By.cssSelector('vaadin-button[slot=submit]'))

login.clear()
login.sendKeys(['admin'])
password.clear()
password.sendKeys(['admin'])
submit.click()

// wait main app screen

wait.until(pkg.ExpectedConditions.presenceOfElementLocated(pkg.By.cssSelector('vaadin-app-layout[id="MainView"]')))

var appMenu = WDS.browser.findElement(pkg.By.cssSelector('a[href=users]'))
appMenu.click()

wait.until(pkg.ExpectedConditions.presenceOfElementLocated(pkg.By.cssSelector('[id="usersDataGrid"]')))

WDS.sampleResult.sampleEnd()

Скрипт основных действий в панели управления, будут выглядеть так:

var props = WDS.vars
var pkg = JavaImporter(org.openqa.selenium, org.openqa.selenium.support.ui)
var wait = new pkg.WebDriverWait(WDS.browser, java.time.Duration.ofSeconds(120))
var tu = JavaImporter(java.util.concurrent.TimeUnit)

WDS.sampleResult.sampleStart()
WDS.browser.manage().window().setPosition(new pkg.Point(0, 0))
WDS.browser.manage().window().setSize(new pkg.Dimension(1280, 1024))
WDS.browser.get(props.get('jmix_proto') + '://' + props.get('jmix_host') + ':' + props.get('jmix_port'))
wait.until(pkg.ExpectedConditions.presenceOfElementLocated(pkg.By.cssSelector('vaadin-app-layout[id="MainView"]')))
var usersMenu = WDS.browser.findElement(pkg.By.cssSelector('a[href="users"]'))

usersMenu.click()

wait.until(pkg.ExpectedConditions.presenceOfElementLocated(pkg.By.cssSelector('[id="usersDataGrid"]')))

var tableRow  = WDS.browser.findElement(pkg.By.cssSelector('[id="usersDataGrid"]')).getShadowRoot().findElements(pkg.By.cssSelector('table tbody tr td')).get(0)
tableRow.click()

wait.until(pkg.ExpectedConditions.presenceOfElementLocated(pkg.By.cssSelector('[id="editBtn"]:not(.v-disabled)')))

var editButton = WDS.browser.findElement(pkg.By.cssSelector('[id="editBtn"]'))
editButton.click()

wait.until(pkg.ExpectedConditions.presenceOfElementLocated(pkg.By.cssSelector('[id="closeBtn"]')))

var closeButton = WDS.browser.findElement(pkg.By.cssSelector('[id="closeBtn"]'))
closeButton.click()

WDS.sampleResult.sampleEnd()
WDS.sampleResult.setSuccessful(true)

Тут еще важно не забыть, что переменная jmix.host должна указывать на внешний адрес хост-машины либо на Docker-хост 172.17.0.1. В этом случае приложение должно быть запущено в контейнере. Делается это просто, в каталоге с проектом выполняем:

./gradlew -Pvaadin.productionMode=true bootBuildImage

А затем запускаем интерактивно:

docker run -it docker.io/library/jmix-onboarding:0.0.1-SNAPSHOT

где jmix-onboarding:0.0.1-SNAPSHOT это имя образа, которое вам выведет команда сборки.

Есть у вас не получается работать с DockerHub, инструкцию для запуска простого реестра Docker-образов можно найти в нашей предыдущей статье «Настройка конвейерной сборки Java-проектов в GitLab» (https://habr.com/ru/companies/haulmont/articles/810151/).

Для реальных кейсов потребуется развить скрипты тестирования так, чтобы они покрывали весь основной функционал приложения.

Благодаря тому, что подсистема интерфейса Jmix Flow UI производит стандартный HTML код, у вас не будет проблем с выборкой элементов селекторами. Однако, надо учитывать, что «внутрянка» многих компонентов спрятана в теневое дерево (Shadow DOM), в которое поиск элемента заходит только через вызов метода getShadowRoot (), как вот в этом примере:

var tableRow  = WDS.browser.findElement(pkg.By.cssSelector('[id="usersDataGrid"]')).getShadowRoot().findElements(pkg.By.cssSelector('table tbody tr td')).get(0)

Также при разработке тестов следует учитывать, что между имитацией действия пользователя и результатом может существовать временная задержка, вызванная обработкой его запросов и отображением результата, поэтому чтобы тесты не заваливались понапрасну следует везде в таких моментах расставлять вызов ожидания результатов при помощи wail.until (WebDriverWait) и аналогов.

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

Автоматизация

Сложность настройки окружения для тестирования, а также необходимость запускать тесты в автоматическом режиме приводит нас к мысли о запуске их в Docker-контейнерах. Как минимум, это поможет нам не даунгрейдить основной браузер до совместимых с тестовой оснасткой версий. Для запуска тестов нам потребуется Docker-файл, который мы разместим в отдельном каталоге-проекте, например ffjmeter.

FROM ubuntu:22.04

ARG JMETER_VERSION="5.6.3"
ENV JMETER_HOME /opt/apache-jmeter-${JMETER_VERSION}
ENV JMETER_BIN ${JMETER_HOME}/bin
ENV JMETER_PLUGINS_MANAGER_VERSION 1.3
ENV CMDRUNNER_VERSION 2.2
ARG GECKODRIVER_VERSION=0.34.0

ENV JMETER_DOWNLOAD_URL https://archive.apache.org/dist/jmeter/binaries/apache-jmeter-${JMETER_VERSION}.tgz

RUN apt-get -qq update
RUN apt-get -qq -y install software-properties-common
RUN add-apt-repository ppa:mozillateam/ppa
RUN apt install wget gnupg unzip curl openjdk-11-jdk-headless firefox-esr -y
RUN mkdir -p /tmp/dependencies && \
  curl -L --silent ${JMETER_DOWNLOAD_URL} > /tmp/dependencies/apache-jmeter-${JMETER_VERSION}.tgz && \
  mkdir -p /opt && \
  tar -xzf /tmp/dependencies/apache-jmeter-${JMETER_VERSION}.tgz -C /opt && \
  rm -rf /tmp/dependencies

ENV PATH $PATH:$JMETER_BIN

WORKDIR ${JMETER_HOME}/bin

RUN curl --location --silent --show-error --output ${JMETER_HOME}/lib/ext/jmeter-plugins-manager-${JMETER_PLUGINS_MANAGER_VERSION}.jar \
    http://search.maven.org/remotecontent?filepath=kg/apc/jmeter-plugins-manager/${JMETER_PLUGINS_MANAGER_VERSION}/jmeter-plugins-manager-${JMETER_PLUGINS_MANAGER_VERSION}.jar \
 && curl --location --silent --show-error --output ${JMETER_HOME}/lib/cmdrunner-${CMDRUNNER_VERSION}.jar \
    http://search.maven.org/remotecontent?filepath=kg/apc/cmdrunner/${CMDRUNNER_VERSION}/cmdrunner-${CMDRUNNER_VERSION}.jar \
 && curl --location --silent --show-error --output ${JMETER_HOME}/lib/jmeter-plugins-webdriver-4.9.1.0.jar \
    https://repo1.maven.org/maven2/kg/apc/jmeter-plugins-webdriver/4.9.1.0/jmeter-plugins-webdriver-4.9.1.0.jar \
 && curl --location --silent --show-error --output ${JMETER_HOME}/lib/selenium-remote-driver-4.9.1.jar \
    https://repo1.maven.org/maven2/org/seleniumhq/selenium/selenium-remote-driver/4.9.1/selenium-remote-driver-4.9.1.jar \
 && curl --location --silent --show-error --output ${JMETER_HOME}/lib/selenium-api-4.21.0.jar \
    https://repo1.maven.org/maven2/org/seleniumhq/selenium/selenium-api/4.21.0/selenium-api-4.21.0.jar

RUN ln -sf /usr/bin/firefox-esr /usr/bin/firefox

RUN java -cp ${JMETER_HOME}/lib/ext/jmeter-plugins-manager-${JMETER_PLUGINS_MANAGER_VERSION}.jar org.jmeterplugins.repository.PluginManagerCMDInstaller

RUN curl -fL -o /tmp/geckodriver.tar.gz \
         https://github.com/mozilla/geckodriver/releases/download/v${GECKODRIVER_VERSION}/geckodriver-v${GECKODRIVER_VERSION}-linux64.tar.gz \
 && tar -xzf /tmp/geckodriver.tar.gz -C /tmp/ \
 && chmod +x /tmp/geckodriver \
 && mv /tmp/geckodriver ${JMETER_HOME}/bin

RUN ./PluginsManagerCMD.sh install jpgc-webdriver webdriver-sampler jmeter-plugins-webdriver selenium-remote

COPY launch.sh /usr/local/bin
RUN chmod +x /usr/local/bin/launch.sh



ENTRYPOINT ["/usr/local/bin/launch.sh"]

Скрипт использует запускалку launch.sh, ее мы создаем рядом с таким содержимым:

#!/bin/bash

export JVM_ARGS="-Xms1024m -Xmx8G -XX:MaxMetaspaceSize=1024m"
echo "JVM_ARGS=${JVM_ARGS}"
echo "/opt/apache-jmeter-5.6.3/bin/jmeter args=$@"

jmeter $@

Меняя параметры JVM_ARGS можно подкрутить лимиты выполнения тестов.

Собрать контейнер в образ с меткой ffjmeter можно командой:

docker build -t docker/ffjmeter .

А для запуска тестов удобно сделать еще один скриптик run_tests.sh:

#!/bin/bash

NOW=$(date '+%Y-%m-%d_%H_%M');
JMX_FILENAME="jmix-benchmark.jmx";
VOLUME_PATH="/home/${USER}/projects/testjmix-jmeter";
JMETER_PATH="/root/jmeter";

docker run --name "jmeter_${NOW}" --volume "${VOLUME_PATH}":"${JMETER_PATH}" docker/ffjmeter -n -t "${JMETER_PATH}/${JMX_FILENAME}" -l "${JMETER_PATH}/result_${NOW}.jtl" -j "${JMETER_PATH}/jmeter_${NOW}.log"

docker rm jmeter_${NOW}

VOLUME_PATH указывает на каталог хостового компьютера в котором мы разрабатываем JMeter-тесты, а JMX_FILENAME определяет какой именно тест-кейс запускать.

После прогона тестов контейнер будет удаляться, а логи и отчет оставаться в каталоге с проектом.

Используя JTL-файл отчета можно построить HTML-страничку с наглядно представленными результатами.

0a271cd52a47c5bc37736b5e8adcc8fb.png

Для этого в первом поле формы надо выбрать оставшийся от работы тестового контейнера CSV/JTL-файл отчета, во второй можно указать пустой (но существующий) файл и в третьем — пустой каталог.

dc9b3d1f4eec37c343da3514cefd3397.png

Таким образом, у теперь нас есть контейнер, который подойдет как для разработки тестов, так и для их выполнения в среде неприрывной доставки CI/CD. Разработку можно вести не выходя из сред разработки тестов JMeter и браузера. А при помощи настроенного мониторинга мы можем вживую наблюдать изменения показателей происходящие при работе тестов. Сочетая использование среды разработки тестов с графическим интерфейсом и их запускалок в контейнерах мы можем как разделить нагрузку между компьютером разработки и серверами, так и решить проблему установки различных версий браузеров и другого необходимого программного обеспечения на компьютере разработчика.

30d2a74373bcb9b5bd7413cbc1ba130e.png

© Habrahabr.ru