Статьи

Непрерывное стресс-тестирование для ваших приложений JAX-RS (и JavaEE) с Gatling + Gradle + Jenkins Pipeline

В этой статье я собираюсь объяснить, как использовать проект Gatling для написания стресс-тестов для ваших конечных точек JAX-RS Java EE и как интегрировать их с Gradle и Jenkins Pipeline , поэтому вместо простых стресс-тестов у вас есть непрерывное стресс- тестирование, при котором каждый коммит может запускать такие тесты автоматически, предоставляя автоматические подтверждения и более важную графическую обратную связь для каждого выполнения, чтобы вы могли отслеживать, как меняется производительность вашего приложения.

Первое, что нужно разработать, — это JAX-RS JavaEE-сервис:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Path("/planet")
@Singleton
@Lock(LockType.READ)
public class PlanetResources {
 
    @Inject
    SwapiGateway swapiGateway;
 
    @Inject
    PlanetService planetService;
 
    @Inject
    @AverageFormatter
    DecimalFormat averageFormatter;
 
    @GET
    @Path("/orbital/average")
    @Produces(MediaType.TEXT_PLAIN)
    @Asynchronous
    public void calculateAverageOfOrbitalPeriod(@Suspended final AsyncResponse response) {
 
        // Timeout control
        response.setTimeoutHandler(asyncResponse -> asyncResponse.resume(Response.status
                (Response.Status.SERVICE_UNAVAILABLE)
                .entity("TIME OUT !").build()));
        response.setTimeout(30, TimeUnit.SECONDS);
 
        try {
            // SwapiGateway is an interface to swapi.co (Star Wars API)
            JsonObject planets = swapiGateway.getAllPlanets();
            final JsonArray results = planets.getJsonArray("results");
             
            // Make some calculations with the result retrieved from swapi.co
            double average = planetService.calculateAverageOfOrbitalPeriod(results);
            final Response averageResponse = Response.ok(
                    averageFormatter.format(average))
                  .build();
            response.resume(averageResponse);
 
        } catch(Throwable e) {
            response.resume(e);
        }
    }
}

В этом нет ничего особенного, это асинхронная конечная точка JAX-RS, которая подключается к сайту swapi.co , получает всю информацию о планетах Звездных войн, вычисляет среднее значение орбитального периода и, наконец, возвращает его в виде текста. Для простоты я не собираюсь показывать вам все остальные классы, но они довольно просты, и в конце поста я предоставлю вам репозиторий github.

Приложение упаковано в файл war и развернуто на сервере приложений. В этом случае в Apache TomEE 7, развернутый внутри официального образа Apache TomEE Docker .

Следующим шагом является настройка скрипта сборки Gradle с зависимостями Gatling . Поскольку Gatling написан на Scala, вам нужно использовать плагин Scala .

01
02
03
04
05
06
07
08
09
10
apply plugin: 'java'
apply plugin: 'scala'
 
def gatlingVersion = "2.1.7"
 
dependencies {
    compile "org.scala-lang:scala-library:2.11.7"
    testCompile "io.gatling:gatling-app:${gatlingVersion}"
    testCompile "io.gatling.highcharts:gatling-charts-highcharts:${gatlingVersion}"
}

После этого пришло время написать наш первый стресс-тест. Важно отметить, что написание стресс-тестов для Gatling — это написание класса Scala с использованием предоставленного DSL. Даже для людей, которые никогда не видели Scala, интуитивно понятно, как им пользоваться.

Поэтому создайте каталог с именем src / test / scala и создайте новый класс AverageOrbitalPeriodSimulation.scala со следующим содержимым:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.starwars
 
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
import scala.util.Properties
 
// Extends from Simulation
class AverageOrbitalPeriodSimulation extends Simulation {
 
  // Gets the base URL where our service is running from environment/system property
  val LOCATION_PROPERTY = "starwars_planets_url";
  val location = Properties.envOrElse(LOCATION_PROPERTY,
                 Properties.propOrElse(LOCATION_PROPERTY, "http://localhost:8080/"))
 
  // configures the base URL
  val conf = http.baseURL(location)
   
  // defines the scenario to run, which in this case is a GET to endpoint defined in JAX-RS service
  val scn = scenario("calculate average orbital period")
    .exec(http("get average orbital period")
      .get("rest/planet/orbital/average"))
    .pause(1)
 
  // instead of simulating 10 users at once, it adds gradullay the 10 users during 3 seconds
  // asserts that there is no failing requests and that at max each request takes less than 3 seconds
  setUp(scn.inject(rampUsers(10).over(3 seconds)))
    .protocols(conf)
    .assertions(global.successfulRequests.percent.is(100), global.responseTime.max.lessThan(3000))
}

Каждое моделирование должно расширять объект моделирования. Это моделирование берет базовый URL-адрес службы из среды или системного свойства starwars_planets_url, создает сценарий, указывающий на конечную точку, определенную в JAX-RS , и, наконец, в течение 3 секунд будет постепенно добавлять пользователей, пока не будут запущены 10 пользователей одновременно. Тест пройдет только в том случае, если все запросы будут выполнены менее чем за 3 секунды.

Теперь нам нужно запустить этот тест. Вы заметите, что это не тест JUnit, поэтому вы не можете выполнить тест Run As JUnit . Что вам нужно сделать, это использовать исполняемый класс, предоставленный Gatling, который требует, чтобы вы передавали в качестве аргумента класс моделирования. Это действительно легко сделать с Gradle .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
task runLoadTest(type: JavaExec) {
    // before runnign the task we need to compile the tests
    dependsOn testClasses
    description = 'Stress Test Calculating Orbital Period'
    classpath = sourceSets.main.runtimeClasspath + sourceSets.test.runtimeClasspath
 
    // if starwars_planets_url is not provided we add the DOCKER_HOST one automatically
    def starwarsUrl;
    if (!System.env.containsKey('starwars_planets_url') && !System.properties.containsKey('starwars_planets_url')) {
        if (System.env.containsKey('DOCKER_HOST')) {
            starwarsUrl = System.env.DOCKER_HOST.replace("tcp", "http").replace("2376", "9090") + "/starwars/"
        } else {
            starwarsUrl = "http://localhost:8080/starwars/"
        }
    }
 
    jvmArgs = [ "-Dgatling.core.directory.binaries=${sourceSets.test.output.classesDir.toString()}" ]
 
    // Means that the url has been calculated here and we set it
    if (starwarsUrl != null) {
        environment["starwars_planets_url"] = starwarsUrl
    }
 
    // Gatling application
    main = "io.gatling.app.Gatling"
 
 
    // Specify the simulation to run and output
    args = [
            "--simulation", "org.starwars.AverageOrbitalPeriodSimulation",
            "--results-folder", "${buildDir}/reports/gatling-results",
            "--binaries-folder", sourceSets.test.output.classesDir.toString(),
            "--output-name", "averageorbitalperiodsimulation",
            "--bodies-folder", sourceSets.test.resources.srcDirs.toList().first().toString() + "/gatling/bodies",
    ]
}
 
// when running test task we want to execute the Gatling test
test.dependsOn runLoadTest

Мы определяем задачу Gradle типа JavaExec , поскольку нам нужно запустить исполняемый класс. Затем мы немного облегчаем жизнь разработчику, автоматически определяя, что если starwars_planets_url не задан, мы запускаем этот тест на машине, на которой установлен Docker, поэтому, вероятно, именно этот хост будет использоваться.

Наконец, мы перезаписываем переменную среды, если она требуется, мы устанавливаем класс runnable с необходимыми свойствами и настраиваем Gradle для выполнения этой задачи каждый раз, когда выполняется тестовая задача (./gradlew test).

Если вы запустите его, вы можете увидеть некоторые выходные сообщения от Гатлинга , а в конце концов такое сообщение, как: откройте следующий файл: /Users/…./stress-test/build/reports/gatling results / averageorbitalperiodsimulation-1459413095563 / index. HTML, и здесь вы можете получить отчет. Обратите внимание, что случайное число добавляется в конец каталога, и это важно, как мы увидим позже. Отчет может выглядеть так:

Снимок экрана 2016-03-31 в 10.36.15

В настоящее время мы интегрировали Гатлинга с Gradle , но здесь отсутствует часть, и она добавляет непрерывную часть в уравнение. Для добавления непрерывного стресс-тестирования мы будем использовать Jenkins и Jenkins Pipeline в качестве CI-сервера, поэтому для каждой фиксации выполняются стресс-тесты.   среди других задач, таких как компиляция, запуск модуля, интеграционные тесты или контроль качества кода.

Исторически задания Jenkins настраивались с помощью веб-интерфейса, требуя, чтобы пользователи вручную создавали задания, заполняли детали задания и создавали конвейер через веб-браузер. Кроме того, это позволяет отделить конфигурацию задания от фактического создаваемого кода.

С введением плагина Jenkins Pipeline . Этот плагин представляет собой Groovy DSL, который позволяет реализовать весь процесс сборки в файле и сохранить его вместе с его кодом. Jenkins 2.0 поставляется по умолчанию с этим плагином, но если вы используете Jenkins 1.X, вы можете установить его как любой другой плагин ( https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Plugin )

Так что теперь мы можем начать кодировать наш плагин релиза, но для целей этого поста будет освещена только стрессовая часть. Вам нужно создать файл с именем Jenkinsfile (имя не обязательно, но это фактическое имя) в корне вашего проекта, и в этом случае со следующим содержимым:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
stage 'Compile And Unit Test'
 
stage 'Code Quality'
 
stage 'Integration Test'
 
stage 'Acceptance Test'
 
// defines an stage for info purposes
stage 'Stress Test'
 
def dockerHost = '...'
//defines a node to run the stage
node {
  // get source code from location where Jenkinsfile (this) is located.
  // you could use stash/unstash to get sources from previous stages instead of getting from SCM
  checkout scm
  // defines the environment variable for stress test
  withEnv(["starwars_planets_url=http://${dockerHost}:9090/starwars/"]) {
    // executes shell script
    sh './gradlew test'
  }
   
}

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

Последний шаг в Jenkins — это создание нового задания типа Pipeline и установка местоположения Jenkinsfile. Так что перейдите в Jenkins> New Item> Pipeline и дайте название работе.

Снимок экрана 2016-03-31 в 11.55.28

Тогда вам нужно всего лишь перейти в раздел Pipeline и настроить хранилище SCM, в котором хранится проект.

Снимок экрана 2016-03-31 в 11.56.30

И затем, если вы правильно настроили хуки от Jenkins и вашего сервера SCM, это задание будет выполняться для каждого коммита, поэтому ваши стресс-тесты будут выполняться непрерывно.

Конечно, вы, наверное, заметили, что стресс-тесты выполняются, но в Jenkins не публикуются отчеты, поэтому у вас нет возможности увидеть или сравнить результаты разных выполнений. По этой причине вы можете использовать плагин publishHtml для хранения сгенерированных отчетов в Jenkins . Если у вас еще не установлен плагин, вам нужно установить его как любой другой плагин Jenkins .

Плагин PublishHtml позволяет публиковать некоторые html-файлы, созданные с помощью нашего инструмента сборки, в Jenkins, чтобы они были доступны пользователям и также классифицировались по номеру сборки. Вам необходимо настроить местоположение каталога файлов для публикации, и здесь мы находим первую проблему. Помните ли вы, что Гатлинг генерирует каталог со случайным числом? Таким образом, мы должны исправить это в первую очередь. Вы можете следовать различным стратегиям, но самая простая из них — просто переименовать каталог в известное статическое имя после тестов.

Откройте файл сборки Gradle и добавьте следующий контент.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
task(renameGatlingDirectory) << {
    // find the directory
    def report = {file -> file.isDirectory() && file.getName().startsWith('averageorbitalperiodsimulation')}
    def reportDirectory = new File("${buildDir}/reports/gatling-results").listFiles().toList()
    .findAll(report)
    .sort()
    .last()
     
    // rename to a known directory
    // should always work because in CI it comes from a clean execution
    reportDirectory.renameTo("${buildDir}/reports/gatling-results/averageorbitalperiodsimulation")
}
 
// it is run after test phase
test.finalizedBy renameGatlingDirectory

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

Последний шаг — добавить после вызова оболочки в следующий вызов Jenkinsfile:

1
publishHTML(target: [reportDir:'stress-test/build/reports/gatling-results/averageorbitalperiodsimulation', reportFiles: 'index.html', reportName: 'Gatling report', keepAll: true])

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

Снимок экрана 2016-03-31 в 15.22.30

И все, благодаря Gradle и Jenkins вы можете легко реализовать стратегию непрерывного стресс-тестирования и просто использовать код на языке, на котором говорят все разработчики.

Мы продолжаем учиться,

Алекс.