Pull to refresh
287.32
TINKOFF
IT’s Tinkoff — просто о сложном

Нагрузочное тестирование на фреймворке Gatling

Reading time 14 min
Views 23K

Статья публикуется от имени Масленникова Сергея, sergeymasle


UPD. Добавлен раздел "Реализация расширения для Gatling"


Gatling


Продолжаем цикл статей про нагрузочное тестирование на фреймворке gatling.io. В этой статье расскажем про основные приемы использования Gatling DSL, которые в большинстве случаев используются при разработке любых скриптов нагрузочного тестирования. Итак, прошу под кат.


Структура Gatling'a


В предыдущей нашей статье мы писали про установку SBT и настройку окружения для фреймворка Gatling. После выполнения всех действий у вас должен появится проект(1), структура которого представлена на изображении ниже.


Структура проекта


В файле plugins.sbt(2) должен быть подключен плагин gatling для sbt, если файл не был создан, то создайте его вручную. Обратите внимание, что необходимо указывать свежую версию плагина, на момент написания статьи она является 2.2.2. Код файла ниже.


//plugins.sbt
addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.2")

Далее в каталоге src/test необходимо создать каталог resources(3). В нем располагаются файлы настроек, а также тестовые данные. Файл gatling.conf содержит основные настройки, logback.xml отвечает за уровень логирования и интерфейсы вывода логов. Эти файлы можно взять бандла https://gatling.io/download/.


Директория scala содержит пакеты с тестами. Имена пакетов можно называть как угодно, но как правило компании используют свое инвертированное имя ru.tcsbank.load.


Файл BasicSimulation основной файл тестов и является точкой входа запуска скриптов.
В директории target\gatling(6) генерируются отчеты по логу запуска (тот что вы видите в консоли). Следует заглядывать туда чаще — она очень быстро растет.


Главный файл проекта — build.sbt(7). Содержит зависимости на все библиотеки, что вы подключаете. Именно в нем указывается ссылка на фреймворк Gatling, его код ниже.


//build.sbt
/*
Подключаем плагин Gatling. Сам плагин должен быть указан в plugins.sbt
*/
enablePlugins(GatlingPlugin) 

/*
Имя вашего проекта
*/
name := "GatlingForArticle"

/*
Версия проекта
*/
version := "0.1"

/*
Указываем версию Scala
*/
scalaVersion := "2.12.4"

/*
Библиотеки фреймворка
*/
libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.3.0" % "test,it"
libraryDependencies += "io.gatling"            % "gatling-test-framework"    % "2.3.0" % "test,it"

/*
Параметры, с которыми будет запущена JVM
*/
javaOptions in Gatling := overrideDefaultJavaOptions("-Xss10m", "-Xms2G", "-Xmx8G")

Последний важный файл — это gatling.log. именно в нем можно увидеть отправляемые запросы и ответы. Чтобы видеть все запросы не забудьте раскомментировать строчку "ALL HTTP" в файле logback.xml.


Основная идея Gatling


На первый взгляд скрипты и DSL Gatling могут показаться сложными, но если понять идею построения скриптов, то все станет довольно просто.


Gatling представляет виртуального пользователя в виде сценария scenario(). Для сценария указывается количество пользователей через метод inject, модель нагрузки, а также настройки протокола http и различные условия эмуляции. Все это указывается в конструкции setUp(). Для примера, при проведении нагрузки интернет магазина для покупателя и администратора будут несколько сценариев:


  1. Покупатель совершает покупку
  2. Покупатель только складывает в корзину
  3. Покупатель только смотрит товары

Такое разделение позволяет легко корректировать нагрузку увеличивая количество виртуальных пользователей для конкретных сценариев.


Сценарий представляет собой цепочку выполнений именно в эту цепочку внутри функции exec() помещаются запросы.


-сценарий
    +-выполнение(запрос1)
    +-выполнение(запрос2)
    +-выполнение(запрос3)
    +-Если(<условие>){
        выполнение(запрос4)
      } иначе {
        выполнение(запрос5)
      }

Цепочка выполнений начинается с выражения scenario(<Name>) и далее собирается путем вызова функции exec().


#1
val scn2 = scenario("ChainScenario")
            .exec(http().get())
            .exec(http().get())
            .exec(http().get())
            .doIfOrElse(true) {
              exec(http().get())
            } {
              exec(http().get())
            }

Если проводить сравнения эмуляции виртуальных пользователей JMeter и Gatling, то можно выделить некоторую особенность. В JMeter пользователи помещаются в катушку ThreadGroup, где задается их количество и именно она(катушка) многократно воспроизводит скрипт виртуальных пользователей по циклу. Т.е. при "поднятии" двух виртуальных пользователей они будут выполнять один и тот же сценарий пока не закончится время теста.


Gatling управляет виртуальными пользователями несколько иначе. При поднятии двух виртуальных пользователей они выполнят свой сценарий и на этом закончат свою работу. Для того, чтобы пользователи выполняли сценарий в цикле необходимо помещать цепочку в блок цикла. Рассмотрим простой скрипт теста, который представлен на сайте https://gatling.io/docs/current/quickstart/#gatling-scenario-explained, его можно взять за основу.


//BasicSimulation.scala
package ru.tcsbank.gatling

/*
Необходимые библиотеки для работы:
io.gatling.core.Predef._ - функции ядра
import io.gatling.http.Predef._ - функции HTTP
import scala.concurrent.duration._ - функции для временных интервалов, чтобы можно было писать `4 minutes`, `15 seconds`
*/
import io.gatling.core.Predef._ 
import io.gatling.http.Predef._ 
import scala.concurrent.duration._

/*
Основной класс теста. 
Именно этот класс, расширяемый от Simulation, ищет фреймворк во время запуска.
*/
class BasicSimulation extends Simulation { 

  /*
  Настройки для HTTP
  */
  val httpConf = http 
    .baseURL("http://computer-database.gatling.io") 
    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") 
    .doNotTrackHeader("1")
    .acceptLanguageHeader("en-US,en;q=0.5")
    .acceptEncodingHeader("gzip, deflate")
    .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0")

  /*
  Сценарий скриптов. Здесь в виде "цепочки" пишем запросы, таймеры, отчеты (pacing) и все остальное связанное со сценарием скриптов.
  */
  val scn = scenario("BasicSimulation") 
    .exec(
        http("request_1")  
        .get("/")
    ) 
    .pause(5) 

  /*
  Сценарий нагрузки. Здесь указываем характер генерируемой нагрузки, модель поднятия пользователей, их количество и длительность нагрузки.
  */
  setUp( 
    scn.inject(atOnceUsers(1)) 
  ).protocols(httpConf) 
}

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


Сценарий скриптов начинается с присвоения константе функции scenario(). Имя сценария должно быть уникальным! Далее вызывается функция exec(), которая принимает на вход другие функции, реализующие тестовые сценарий: http и websocket. Именно в ней выполняются все действия для эмуляции запросов.


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


HTTP


Фреймворк по умолчанию поддерживает следующие методы GET, POST, PUT, PATCH, DELETE, OPTIONS. Рассмотрим в качестве примера написание запросов GET и POST. Для начала присвоим константе scn функцию сценария и напишем в exec() простой GET-запрос:


val scn = scenario("GetScenario")
    .exec(
        http("GETRequest")
          /*
          .get("/foo.php") или можно указать полный путь
          */
          .get("http://bar.com/foo.php")
    )

Если же необходимо установить headers, то добавляем следующее:


val scn = scenario("GetScenario")
    .exec(
        http("GETRequest")
          .get("http://bar.com/foo.php")
          /*
          .headers("foo","bar") или через Map
          */
          .headers(
            Map(
                "foo1" -> "bar1",
                "foo2" -> "bar2"
            )
          )
    )

Передаем параметры в запрос:


val scn = scenario("GetScenario")
    .exec(
        http("GETRequest")
          .get("http://bar.com/foo.php")
          .headers(
            Map(
                "foo1" -> "bar1",
                "foo2" -> "bar2"
            )
          )
          /*
          .queryParam("param","value") или через Map
          */
          .queryParamMap(
            Map(
              "param1" -> "value1",
              "param2" -> "value2"
            )
          )

Переметры также можно передавать непосредственно через функцию .get("http://bar.com/foo.php?param=value"). Если есть загрузка статических ресурсов, то используем resources() для параллельной загрузки.


val scn = scenario("GetScenario")
    .exec(
        http("GETRequest")
          .get("http://bar.com/foo.php")
          .headers(
            Map(
                "foo1" -> "bar1",
                "foo2" -> "bar2"
            )
          )
          .queryParamMap(
            Map(
              "param1" -> "value1",
              "param2" -> "value2"
            )
          )
          .resources(
            http("css").get("/main.css"),
            http("js").get("http://bar.com/main.js")
          )

Для метода POST при передаче параметров используются функция formParam().


val scn = scenario("PostScenario")
    .exec(
        http("GETRequest")
          .post("http://bar.com/foo.php")
          .headers(
            Map(
                "foo1" -> "bar1",
                "foo2" -> "bar2"
            )
          )
          /*
          .formParam("param","value") или через Map
          */
          .formParamMap(
            "param1" -> "value",
            "param2" -> "value2"
          )

Чтобы передать данные напрямую через тело запроса необходимо использовать body().


val scn = scenario("PostScenario")
    .exec(
        http("GETRequest")
          .post("http://bar.com/foo.php")
          .headers(
            Map(
                "foo1" -> "bar1",
                "foo2" -> "bar2"
            )
          )
          .body(
            /*
            В тройных кавычках можно не экранировать одинарные. Функция stripMargin
            удаляет символы вертикальный черты, которые служат для сохранения отступов
            */
            StringBody(
                """
                  |{
                  | "login": "password"
                  |}""".stripMargin 

            )
          )

Проверки


При эмуляции запросов требуется проверять код ответа или наличие какого-либо текста в теле ответа. Также нередко требуется извлечь данные из ответа. Все это может выполнить с помощью функции check(). Проверки необходимо производить после функции http-метода.


val scn = scenario("CheckScenario")
    .exec(
      http("ForChecks")
        .get("/check.php")
        /*
        Проверяем, что код ответа равен 200
        */
        .check(status.is(200))

        /*
        Проверяем код ответа, который может принимать 200 или 500
        */
        .check(status.in(200, 500))

        /*
        Проверяем тело на наличие строки foo_bar
        */
        .check(substring("foo_bar"))

        /*
        Проверяем тело на наличие строки удовлетворяющей регулярному выражению
        */
        .check(regex(""" \d{4} – \d{4}"""))

        /*
        Находим строку удовлетворяющую регулярному выражению и сохраняем в переменную
        */
        .check(regex("""key=(\d{4}-\w{4})""").saveAs("AuthKey"))

        /*
        На случай, когда данные могут быть, а могут не быть.
        В этой конструкции checkIf()() принимает анонимную функцию с параметрами Response и Session. 
        Затем в Response тело ответа проверяем на наличие "id=" и если есть, то сохраняет в параметр
        "id_some_value"
        */
        .check(
          checkIf(
            (r: Response, s: Session) => r.body.string.contains("id=")
          )(
            regex("""id=(\d+)""").saveAs("id_some_value")
          )
        )
    )

Сессия


В Session хранятся все данные виртуального пользователя и переменные. Если вы хотите что-то предать в рамках сценария, то это нужно делать через сессию.


val scn = scenario("SessionScenario")
    /*
    Задаем через лямбда-выражение значение для переменной password_param
    Только через лямбду. Нет, по-другому нельзя.
    */
    .exec(
        session => session.set("password_param","anyPassword")
     )
    .exec(
      http("param")
        .get("/anything.php")
        /*
        Через конструкцию "${}" получаем значение переменной
        */
        .queryParam("login","${password_param}")
    )

Динамические значения нельзя напрямую передать в DSL-функции, так как Scala использует CallByValue они будут получены при компиляции и далее всегда использоваться без получения новых.


exec(
        http("timestamp")
          .get("/")
          /*
          Скомпилируется, но при каждом вызове будет возвращать первое 
          полученное значение
          */
          .queryParam("timestamp", System.currentTimeMillis() )
          /*
          Получаем значение из Session, которое сохранили ранее
          */
          .queryParam("timestamp", session => { session("var").as[String] } )
          /*
          Just magic!
          Получаем через анонимную функцию. X - просто константа
          */
          .queryParam("timestamp", x => { System.currentTimeMillis() } )  
      )

Логические конструкции


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


val scn = scenario("doIfSimulation")
    /*
    Устанавливаем для переменной foo значение bar 
    */
    .exec(
      session => session.set("foo","bar")
    )
    /*
    Выполняется проверка, что переменная foo содержит значение bar.
    Если значение содержится, то выполняется запрос.
    */
    .doIf(session => session("cond").as[String].startsWith("bar")){ //
      exec(
        http("IFrequest")
          .get("/")
      )
    }

Тестовые данные


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


csv("foo.csv") // данные, разделенные запятой
tsv("foo.tsv") // данные, разделенные табуляцией
ssv("foo.ssv") // данные, разделенные точкой с запятой
separatedValues("foo.txt", '#') // данные разделенные другим символом


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


//csv файл
model,
Macbook,
MacBook Pro,
ASUS Eee PC,
Acer,
Asus,
Sony Vaio,
Chromebook

Gatling при чтении файла использует первую строку как имена параметров, и в последствии при чтении значений сохраняет их под этими именами. Таким образом, в параметр ${model} будут подставляться значения с именами ноутбуков, описанных в csv файле.


Чтобы читать csv-файл, необходимо вызвать функцию csv().


/*
Функции чтения файлов имеют также стратегии выборки данных
.queue    // последовательное чтение данных
.random   // чтение случайным образом
.shuffle  // сначала перемешивает данные, затем читает последовательно
.circular // при достижении конца файла чтение производится с начала
*/
val feeder = csv("data.csv").circular

val scn = scenario("doIfSimulation").feed(feeder) //#1
    .repeat(5)(
      exec(
        http("request")
          .get("/")
      )
      // .feed(feeder) #2
      .exec(http("search").get("/computers?f=${model}"))
    )

Итак, мы создали переменную feeder и указали имя файла, который лежит в src\test\resources\data.csv. В сценарии мы вызываем функцию feed() и указываем константу feeder. Чтение нового значения происходит каждый раз, когда вызывается функция feed().


При варианте #1 функция feed() вызывается до repeat(), таким образом, в переменной ${model} будет использоваться первое считанное значение на 5 итераций.


При варианте #2 значение будет считываться перед каждым запросом.


Модели нагрузки


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


nothingFor(duration) — указывается длительность паузы duration перед стартом нагрузки


atOnceUsers(nbUsers) — виртуальные пользователи в количестве nbUsers будут “подниматься” сразу (по готовности).


rampUsers(nbUsers) over(duration) — в течение времени duration будут "подниматься" виртуальные пользователи в количестве nbUsers через равные временные интервалы.


constantUsersPerSec(rate) during(duration) — указывается частота “поднятия” виртуальных пользователей rate (вирт. польз. в секунду) и временной интервал duration. В течении duration количество виртуальных пользователей будет увеличиваться на rate каждую секунду.


constantUsersPerSec(rate) during(duration) randomized — аналогично верхней конструкции только временные интервалы между "поднятием" виртуальных пользователей будут случайными.


rampUsersPerSec(rate1) to (rate2) during(duration) — в течение времени duration виртуальные пользователи будут увеличиваться с частоты rate1 до частоты rate2.


rampUsersPerSec(rate1) to(rate2) during(duration) randomized — аналогично верхней конструкции только временные интервалы между "поднятиями" виртуальных пользователей будут случайными.


splitUsers(nbUsers) into(injectionStep) separatedBy(duration) — через каждый временной интервал duration будут добавляться виртуальные пользователи по модели injectionStep, пока их количество не достигнет nbUsers. В injectionStep можно указать модели описанные выше.


splitUsers(nbUsers) into(injectionStep1) separatedBy(injectionStep2) — аналогично верхней конструкции только разделителем модель injectionStep2.


heavisideUsers(nbUsers) over(duration) — виртуальные пользователи в количестве nbUsers будут подниматься ступенями за время duration.


Запуск нагрузки


Вариант 1


Для запуска нагрузки самый простой способ — это использовать bundle. Необходимо поместить файл скрипта в gatling-charts-highcharts-bundle-2.3.0\user-files\simulations\ и далее запустить gatling-charts-highcharts-bundle-2.3.0\bin\gatling.bat. В консоли будет предложен выбор скрипта для запуска.


Терминал Gatling


Наш скрипт под вариантом 6. После выбора произойдет генерация нагрузки с выводом информации в консоль.


Вариант 2


Этот вариант предполагает запуск нагрузки непосредственно из IDE IntelliJ IDEA Community.


После того, как произвели все действия по настройке библиотек, нажимаем ALT+F12 и открываем терминал. В терминале набираем команду sbt.


Запуск SBT


После загрузки всех компонентов производим запуск скриптов командой gatling:testOnly.


Консоль Gatling


В консоли будет отображаться текущее состояние нагрузки.
Чтобы производить запуск из панели запуска IDEA, необходимо добавить нашу команду на запуск в SBT Task.


Создание SBT Task


Реализация расширения для Gatling


В документации по Gatling написано, что из коробки существует поддержка протоколов только для HTTP/1.1 и WebSocket. Также имеются официальные и неофициальные расширения для Gatling, которые доступны по ссылке(https://gatling.io/docs/2.3/extensions/).


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


И так, нам необходимо реализовать вот такую возможность:


val scn = scenario("BasicSimulation")
 .exec(
   new ExampleActionBuilder("MyAction")
 )

Так как функция exec() может принимать тип ActionBuilder необходимо написать свой класс и расширить его типом ActionBuilder.


class ExampleActionBuilder(myNameAction: String) extends ActionBuilder {

 override def build(ctx: ScenarioContext, next: Action): Action = {

   new ExampleChainableAction(myNameAction, next, ctx)
 }
}

В переопределенной функции build нужно создать экземпляр класса, который будет реализовывать необходимый код. Данный класс необходимо расширить от ChainableAction.


class ExampleChainableAction(myNameAction: String, myNextAction: Action, ctx: ScenarioContext) extends ChainableAction {

 override def next: Action = myNextAction

 override def name: String = myNameAction

 override def execute(mySession: Session): Unit = {
   /*
    Здесь реализуем наш код
    */
 }
}

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


Код расширения
package ru.tcsbank.load

/*
Необходимые библиотеки для работы:
io.gatling.core.Predef._ - функции ядра
import io.gatling.http.Predef._ - функции HTTP
import scala.concurrent.duration._ - функции для временных интервалов, чтобы можно было писать `4 minutes`, `15 seconds`
*/
import io.gatling.commons.stats.{KO, OK}
import io.gatling.core.Predef._
import io.gatling.core.action.builder.ActionBuilder
import io.gatling.core.action.{Action, ChainableAction}
import io.gatling.core.stats.message.ResponseTimings
import io.gatling.core.structure.ScenarioContext
import io.gatling.http.Predef._

import scala.concurrent.duration._

/*
Основной класс теста.
Именно этот класс, расширяемый от Simulation, ищет фреймворк во время запуска.
*/
class ExampleProtocolScript extends Simulation {

  /*
  Настройки для HTTP
  */
  val httpConf = http
    .baseURL("http://computer-database.gatling.io")
    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
    .doNotTrackHeader("1")
    .acceptLanguageHeader("en-US,en;q=0.5")
    .acceptEncodingHeader("gzip, deflate")
    .userAgentHeader("Mozilla/5.0 (Windows NT 5.1; rv:31.0) Gecko/20100101 Firefox/31.0")

  /*
  Сценарий скриптов. Здесь в виде "цепочки" пишем запросы, таймеры, отчеты(pacing) и все остальное, связанное со сценарием скриптов.
  */
  val scn = scenario("BasicSimulation")
    .exec(
      /*
      Выполняем свою релизацию Action
      */
      new ExampleActionBuilder("MyAction")
    )

  /*
  Сценарий нагрузки. Здесь указываем характер генерируемой нагрузки, модель поднятия пользователей, их количество и длительность нагрузки.
  */
  setUp(
    scn.inject(atOnceUsers(1)).protocols(httpConf)
  )
}

/*
  Точка входа для exec().
  Парметров ExampleActionBuilder() может быть любое количество,
  перебрасывать их можно напряму в ExampleChainableAction()
 */
class ExampleActionBuilder(myNameAction: String) extends ActionBuilder {

  override def build(ctx: ScenarioContext, next: Action): Action = {

    new ExampleChainableAction(myNameAction, next, ctx)
  }
}

/*
  Здесь выполняется наш Action.
  Парметров ExampleChainableAction() также может быть любое количетво.
 */
class ExampleChainableAction(myNameAction: String, myNextAction: Action, ctx: ScenarioContext) extends ChainableAction {

  /*
  Отдаем управление следующему актору.
  В нашей реализации просто прокидываем параметр.
  */
  override def next: Action = myNextAction

  /*
  Наменование Action.
  То, что будет отбражаться в консоли и в отчетах.
  Можно сравнить с http(<Наименование>).get(...)
  */
  override def name: String = myNameAction

  /*
  В этом методе происходит вся работа нашего Action.
  */
  override def execute(mySession: Session): Unit = {
    /*
    Время начала выполнения нашего Action
     */
    val startTime = System.currentTimeMillis()

    try {
      /*
        НАЧАЛО кода Action
       */

      System.out.println(myNameAction+" Hello world!")

      /*
        КОНЕЦ кода Action
       */

      /*
      Время завершения выполнения нашего Action
      */
      val stopTime = System.currentTimeMillis()

      /*
       Заполняем метрические данные при успешном выполнении Action
       */
      ctx.coreComponents.statsEngine.logResponse(
        session = mySession,
        requestName = name,
        timings = new ResponseTimings(startTime, stopTime),
        status = OK,
        None,
        None,
        Nil
      )

      /*
       При успешном выполнении передаем актору сообщением нашу сессию.
       Изменять этот код не требуется.
      */
      myNextAction ! mySession

    } catch {
      /*
        Если при выполнении кода произошла ошибка, обрабатываем ее здесь.
       */
      case e: Exception => {
        /*
        Время завершения выполнения нашего Action
        */
        val stopTime = System.currentTimeMillis()

        /*
        Заполняем метрические данные при ошибке в выполнении Action
       */
        ctx.coreComponents.statsEngine.logResponse(
          session = mySession,
          requestName = name,
          timings = new ResponseTimings(startTime, stopTime),
          status = KO,
          None,
          Some(e.getMessage), //отправляем сообщение об ошибке, появится в логе /target/.../simulation.log
          Nil
        )

        /*
         При получении ошибки также передаем актору сообщением нашу сессию.
         Изменять этот код не требуется.
        */
        myNextAction ! mySession
      }
    }
  }
}

В этой статье были описаны основные моменты, которые помогут самостоятельно разработать скрипты для проведения нагрузки на фреймворке Gatling. Если возникли вопросы, то с удовольствием ответим на них в комментариях.

Tags:
Hubs:
0
Comments 7
Comments Comments 7

Articles

Information

Website
www.tinkoff.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия