Привет! Мы — команда тестирования производительности в Тинькофф, и мы любим инструмент Gatling. В цикле статей мы расскажем об использовании Gatling и дополнительных инструментов, упрощающих разработку скриптов.

Возможно, вы уже читали наши статьи про Gatling: первую и вторую. Они успели устареть, поэтому мы решили вернуться с обновленной информацией. 

Почему стоит прочитать эту статью

Вы узнаете, как подготовить шаблонный Gatling-проект из нашего шаблона gatling-template.g8, и познакомитесь с базовыми понятиями в Gatling. А также изучите базовые возможности подключаемой библиотеки gatling-picatinny.

Кроме того, статья будет полезна новичкам в нагрузочном тестировании (НТ), которые хотят запустить его на своем проекте, но не знают, с чего начать. Мы познакомим вас с Gatling и расскажем об инфраструктуре и инструментах команды НТ Тинькофф, которые вы сможете использовать.

Дисклеймер

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

Что такое шаблон gatling-g8 и как его использовать

Шаблон gatling-g8 — это удобный инструмент, который позволяет быстро создавать SBT-проект, в нашем случае — для Gatling-проекта. Он включает в себя готовую структуру каталогов и файлов для написания скриптов, а также подключенную gatling-picatinny. Это библиотека с полезными функциями, расширяющими Gatling DSL и повышающими производительность. Шаблон g8 мы создаем с использованием инструмента giter8. Разберемся, с чего начать работу с gatling-g8.


Создание шаблона проекта. Убедитесь, что в вашей системе установлена sbt версии не ниже 1.6.2. Она понадобится для дальнейшей работы. Затем откройте консоль и выполните команду ([version] — указать актуальную версию):

sbt new TinkoffCreditSystems/gatling-template.g8 -t [version]

После выполнения команды у вас запросят несколько параметров. Укажите только имя вашего сервиса: 

Остальные параметры оставляйте как есть. Теперь у вас есть готовый scala-проект для Gatling.

Обзор структуры проекта. Рассмотрим, из чего именно состоит проект. Пойдем по каталогам сверху вниз:

  1. project — каталог содержит информацию о зависимостях, плагинах и версии sbt проекта;

  2. src — основной каталог. Содержит скрипты и конфигурации;
    2.1. resources — содержит конфигурацию Gatling, настройки симуляции и логирования;
    2.2. cases — каталог для запросов симуляции;
    2.3. scenarios — каталог для сценариев симуляции;
    2.4. test.scala — конфигурация протокола, который используется для тестирования системы;
    2.5. Debug.scala, MaxPerformance.scala, Stability.scala — готовые симуляции для отладки, теста на поиск максимальной производительности и теста стабильности соответственно.

  3. build.sbt — конфигурация sbt.

Как видно из структуры, для создания скриптов необходимо написать запросы в каталоге cases. Далее — сформировать из них сценарии в каталоге scenarios и подключить эти сценарии в симуляции Debug.scala, MaxPerformance.scala или Stability.scala. О них расскажем ниже. А затем — добавить в настройки симуляции нужные переменные. О написании скриптов мы подробнее расскажем в следующих статьях.

Запуск проекта. Чтобы запустить проект, используйте Gatling runner. Он позволяет запускать скрипты не через sbt-plugin, а напрямую из IntelliJ IDEA. В некоторых случаях это позволяет улучшить опыт отладки с помощью стандартных возможностей IDE (например, breakpoints). Откройте объект GatlingRunner по пути src/test/scala/ru/tinkoff/load/[projectName]/GatlingRunner.scala и измените класс симуляции, который хотите запустить:

object GatlingRunner {  
  def main(args: Array[String]): Unit = {
    /* 
    Здесь вы указываете класс, который хотите запустить.
    По умолчанию стоит Debug
    */     
    val simulationClass = classOf[Debug].getName    
    val props = new GatlingPropertiesBuilder    
    props.simulationClass(simulationClass)    
    Gatling.fromMap(props.build)  
  }
}

Затем нажмите на Run GatlingRunner, если работаете в IDE InteliJ IDEA: 

Если выбрали другую IDE, воспользуйтесь SBT для запуска в командной строке:

sbt "Gatling/testOnly ru.tinkoff.load.projectName.simulationName"

Теперь, когда мы рассказали про использование шаблона, предлагаем разобраться с основными понятиями.

Какие базовые понятия в Gatling важно знать

Симуляция. В общем виде симуляция в Gatling — это класс Scala, который состоит из различных сценариев и инжекторов (способов моделирования нагрузки) для этих сценариев. Симуляцию можно разделить на следующие части:

  1. Конфигурация протокола (в примере — HTTP).

  2. Определение запросов.

  3. Определение сценария.

  4. Вызов основного метода симуляции setUp.

В примере ниже мы показываем общий вид симуляции без использования нашего шаблона gatling-template.g8. 

import io.gatling.core.Predef.
import io.gatling.core.structure.ScenarioBuilder
import io.gatling.http.Predef.
import io.gatling.http.protocol.HttpProtocolBuilder
import io.gatling.http.request.builder.HttpRequestBuilder

import scala.concurrent.duration.DurationInt

// Создаем класс симуляции, который наследуется от класса Simulation
class BasicSimulation extends Simulation { 
  
  // Определяем http протокол
  val httpProtocol: HttpProtocolBuilder = http
  	// Базовый URL относительно которого будут строиться все запросы
    .baseUrl("http://computer-database.gatling.io")
  	// Добавляем необходимые хедеры
    .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8")
    .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")

  // Создаем запрос
  val request: HttpRequestBuilder = http("request_1")
    .get("/myRequest")

  // Создаем сценарий, в который добавлен наш запрос
  val scn: ScenarioBuilder = scenario("BasicSimulation")
    .exec(request)

    // Основной метод setUp, в который добавляем сценарии
    setUp(
    // Моделируем нагрузку, используя специальные inject-методы
    scn.inject(
      // Ничего не делаем 4 секунды
      nothingFor(4.seconds),
      // Поднимает сразу заданное количество пользователей
      atOnceUsers(10),
      // Поднимает заданное количество пользователей, 
      // равномерно распределенных в течении заданного периода
      rampUsers(10).during(5.seconds),
      // В течение заданного времени будет поддерживать определенное 
      // кол-во пользователей в секунду 
      constantUsersPerSec(20).during(15.seconds),
      // Тоже самое, что и выше, но пользователи будут вводиться через 
      // рандомизированные интервалы
      constantUsersPerSec(20).during(15.seconds).randomized,
      // Поднимает пользователей от начального до целевого значения
      // равномерно в течении заданного периода
      rampUsersPerSec(10).to(20).during(10.minutes),
      // Тоже самое, что и выше, но пользователи будут вводиться через 
      // рандомизированные интервалы
      rampUsersPerSec(10).to(20).during(10.minutes).randomized, 
    )
    // Подключаем http протокол
  ).protocols(httpProtocol)
}

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

  1. Debug. Используется для отладки и проверки работоспособности логики самих скриптов.

class Debug extends Simulation {

  setUp(
    // Сценарий запустится на одном пользователе. 
    // Фактически мы получим одну итерацию сценария CommonScenario()
    CommonScenario().inject(atOnceUsers(1)),
  ).protocols(
    httpProtocol,
  ).maxDuration(testDuration)

}
  1. MaxPerformance. Используется для запуска тестов со ступенчато-подаваемой нагрузкой. С его помощью мы определяем точки максимальной производительности. Ниже — график и пример его реализации в Gatling. 

На графике можно увидеть ступенчато-подаваемую нагрузку. Выход на очередную ступень нагрузки происходит в течение ramp duration. Сама ступень длится в течении stage duration, интенсивность на ступень рассчитывается по формуле intensity / stage numbers, где stage numbers — это количество ступеней в тесте.

class MaxPerformance extends Simulation with Annotations {

  setUp(
    CommonScenario().inject(
      // Интенсивность на ступень
      incrementUsersPerSec((intensity / stagesNumber).toInt) 
        // Количество ступеней
        .times(stagesNumber)
        // Длительность полки
        .eachLevelLasting(stageDuration)    
      	// Длительность разгона
        .separatedByRampsLasting(rampDuration)  
      	// Начало нагрузки с 0 rps
        .startingFrom(0),                                    
    ),
  ).protocols(httpProtocol)
    // Общая длительность теста
    .maxDuration(testDuration)
}
  1. Stability. Используется для запуска тестов со стабильно подаваемой нагрузкой с предварительным постепенным разгоном. Такой тест может быть полезен для подтверждения стабильной работы приложения и проведения регрессионных тестов. Ниже — пример теста со стабильно подаваемой нагрузкой и код в Gatling.

На графике можно увидеть стабильно подаваемую нагрузку. Выход на стабильную нагрузку происходит в течение ramp duration, стабильная нагрузка длится в течение stage duration с интенсивностью intensity.

class Stability extends Simulation with Annotations {

  setUp(
    CommonScenario().inject(
      // Длительность разгона
      rampUsersPerSec(0) to intensity.toInt during rampDuration, 
      // Длительность полки
      constantUsersPerSec(intensity.toInt) during stageDuration, 
    ),
  ).protocols(httpProtocol)
  	// Длительность теста = разгон + полка
    .maxDuration(testDuration)

}

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

Gatling предлагает несколько стратегий для встроенных фидеров:

  • queue — очередь, используется по умолчанию, читает записи последовательно;

  • random — случайным образом выбирает запись из последовательности;

  • shuffle — перемешивает записи, а затем ведет себя как queue;

  • circular — читает записи последовательно, но при достижении конца последовательности начинает заново.

При использовании queue или shuffle убедитесь, что ваш набор данных содержит достаточно записей. Если у фидера заканчивается запись, Gatling принудительно закончит тест.

Также фидеры делятся на:

  • файловые — читают данные из заранее подготовленных файлов;

  • JDBC — читает данные из базы данных;

  • Sitemap — читает данные из специального формата XML-файлов;

  • Redis — читает данные из Redis.

  • Custom — читает данные из произвольной функции, поставляется с gatling-picatinny.

Пример использования файлового фидера:

...

  val someRequest: HttpRequestBuilder = 
    http("some request")
	.get("/")
      .queryParam("someParam", "#{value}")   // 1
	
  val csvFeeder: BatchableFeederBuilder[String] = 
     csv("value.csv").random  // 2

  /* Пример того, как выглядит файл value.csv   // 3
   value								  
   1
   2
   ...
   n
   */

  val scn: ScenarioBuilder = scenario("BasicSimulation")
    .feed(csvFeeder)					   // 4		
    .exec(someRequest)

...

Пример файла value.csv:

Разберем, что происходит в этом коде:

  1. Мы пишем http-запрос с некоторым параметром, значение которого мы хотим параметризовать. Для этого мы используем специальную конструкцию #{value}.

  2. Создаем файловый фидер.

  3. Файл содержит заголовок и значения. Заголовок должен называться так же, как и параметр.

  4. Добавляем фидер в сценарий. Теперь вместо #{value} там окажется случайное значение из файла. Убедиться, что значения из фидера попали в запросы, можно запустив отладочный сценарий (Debug.scala).

Так мы можем «на лету» создавать разные запросы.

Сессия.
Это состояние виртуального пользователя. В сессии хранятся данные только для этого пользователя. Передача информации между сессиями по умолчанию невозможна, но можно использовать workaround — например, redis. По сути, это пара «ключ-значение», где ключ — это атрибут сессии, или параметр, а значение — это содержимое этого атрибута. В сессии хранятся как различные переменные, например текущее значение фидера, так и название сценария, id виртуального пользователя и другие параметры.

Вы можете читать данные из сессии и добавлять их туда. Вот так можно положить значение в сессию:

class CommonScenario {  
  val scn: ScenarioBuilder = scenario("Common Scenario")	
   // Положили в сессию параметр 'key' с значением 'value'
  .exec(session => session.set("key", "value"))    
  .exec { session =>
    /* 
    Обратите внимание, если вы хотите положить несколько значений 
    в сессию, то такой вариант не сработает и в сессии будет только 
    параметр 'm' 
    */   
    session.set("k", "v")        
    session.set("m", "d") 
  }    
  .exec { session =>   
    // Правильное использование
    session.set("k", "v").set("m", "d")     
  }    
  .exec(GetMainPage.getMainPage)
}

А так — получить из сессии значение:

class CommonScenario {  
  val scn: ScenarioBuilder = scenario("Common Scenario")    
  .exec { session =>    
    // Взяли значение из сессии, обязательно нужно указать as[type]
    val someValue = session("someValue").as[Int]    
    // Изменили значение
    val newValue  = someValue + 1
    // Положили новое значение обратно в сессию
    session.set("key", newValue)				
  }
  .exec(GetMainPage.getMainPage)
}

Еще можно вывести сессию в консоль. Например, с целью отладки:

class CommonScenario {  
  val scn: ScenarioBuilder = scenario("Common Scenario")    
  .exec { session => 
    // Вывели в консоль всю сессию
    println(session)						 		  
    session
  }
  .exec { session => 
    // Вывели в консоль только необходимый атрибут  
    println(session("addComputer").as[String])    
    /* 
    Можно так же поставить breakpoint в этом месте 
    для достижения такого же результата 
    */
    session 
  }
}

Вывод в консоли выглядит так:

Session(CommonScenario,1,HashMap(gatling.http.cache.baseUrl -> 
http://computer-database.gatling.io, randomComputerName -> JLSZBCAOWBZNI, 
company -> 40, gatling.http.ssl.sslContexts -> io.gatling.http.util.SslContexts@5993df64, 
gatling.http.referer -> http://computer-database.gatling.io/computers, 
gatling.http.cookies -> CookieJar(Map()), introduced -> 2022-01-26, 
gatling.http.cache.dns -> io.gatling.http.resolver.ShufflingNameResolver@5d64e0f7, discontinued -> 2022-01-27),OK,List(),
io.gatling.core.protocol.ProtocolComponentsRegistry$$Lambda$698/0x00000008007a0840@ae111d9,
io.netty.channel.nio.NioEventLoop@13047d7d)

JLSZBCAOWBZNI

Gatling Picatinny. Это библиотека с множеством полезных функций. Она расширяет Gatling DSL и повышает производительность при написании скриптов. Мы уже писали об этой библиотеке в тексте об арсенале Gatling. Полное описание всех функций с примерами вы можете найти в README. А о новых функциях библиотеки мы расскажем ниже.

Какие фидеры появились в Picatinny
HashiCorp Vault. Фидер, способный извлекать секретные данные из HC Vault. Авторизация проходит через approle, при этом используется v1 API. Фидер работает с kv Secret Engine. Он не перебирает ключи, а возвращает полную карту с ключами, найденными при каждом вызове. Параметры фидера:

  • vaultUrl — URL хранилища, например https://vault.ru;

  • secretPath — путь к секретным данным в вашем хранилище. Например, testing/data;

  • roleId — approle логин;

  • secretId — approle пароль;

  • keys — список ключей, которые вы хотите получить из хранилища.

private val vaultUrl: String    = System.getenv("vaultUrl")
private val secretPath: String  = System.getenv("secretPath")
private val roleId: String      = System.getenv("roleId")
private val secretId: String    = System.getenv("secretId")
private val keys: List[String]  = List("k1", "k2", "k3")
val vaultFeeder: Feeder[String] = 
	VaultFeeder(vaultUrl, secretPath, roleId, secretId, keys)

Utils. Создает токен JWT с использованием шаблона json и сохраняет его в сессии Gatling. После этого вы можете использовать его для подписи запросов. Пример объемный, его лучше посмотреть в README.

Random method. Содержит множество различных методов для генерации рандомных значений.

Assertion. Загружает конфигурации assertion из файлов YAML. Пример файла nfr.yml:

nfr:
  - key: '99 перцентиль времени выполнения'
    value:
      GET /: '500'
      MyGroup / MyRequest: '900'
      request_1: '700' 
      all: '1000'
  - key: 'Процент ошибок'
    value:
      all: '5'
  - key: 'Максимальное время выполнения'
    value:
      GET /: '1000'
      all: '2000'

Пример использования:

import ru.tinkoff.gatling.assertions.AssertionsBuilder.assertionFromYaml  

class test extends Simulation {            
  setUp(          
    scn.inject(            
      atOnceUsers(10)          
    ).protocols(httpProtocol)        
  ).maxDuration(10)
  // Подключили конфигурации assertion из файла
  .assertions(assertionFromYaml("src/test/resources/nfr.yml"))  
}

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

import io.gatling.core.Predef._
import io.gatling.core.structure.ScenarioBuilder
// Добавляем импорт
import ru.tinkoff.gatling.transactions.Predef._ 

object DebugScenario {
  val scn: ScenarioBuilder = scenario("Debug")
    .exec(Actions.createEntity())
  	// Начало транзакции
    .startTransaction("transaction1") 
    .doWhile(_ ("i").as[Int] < 10)(
      feed(feeder)
        .exec(Actions.insertTest())
        .pause(2)
        .exec(Actions.selectTest)
    )
  	// Конец транзакции
    .endTransaction("transaction1") 
    .exec(Actions.batchTest)
    .exec(Actions.selectAfterBatch)
}

// Наследуемся от SimulationWithTransactions
class DebugTest extends SimulationWithTransactions {
  setUp(
    DebugScenario.scn.inject(atOnceUsers(1))
  ).protocols(dataBase)

}

Заключение

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

В следующих статьях мы расскажем, как Gatling и другие инструменты помогают создавать скрипты для различных протоколов, таких как HTTP, gRPC, JDBC, AMQP и Kafka.

Полезные ссылки

  1. Основные концепции в Gatling.

  2. Подробнее о сессии в Gatling.

  3. Подробнее о составлении http запросов в Gatling.

  4. Подробнее о фидерах в Gatling.