Привет! Мы — команда тестирования производительности в Тинькофф, и мы любим инструмент 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.
Обзор структуры проекта. Рассмотрим, из чего именно состоит проект. Пойдем по каталогам сверху вниз:
project — каталог содержит информацию о зависимостях, плагинах и версии sbt проекта;
src — основной каталог. Содержит скрипты и конфигурации;
2.1. resources — содержит конфигурацию Gatling, настройки симуляции и логирования;
2.2. cases — каталог для запросов симуляции;
2.3. scenarios — каталог для сценариев симуляции;
2.4. test.scala — конфигурация протокола, который используется для тестирования системы;
2.5. Debug.scala, MaxPerformance.scala, Stability.scala — готовые симуляции для отладки, теста на поиск максимальной производительности и теста стабильности соответственно.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, который состоит из различных сценариев и инжекторов (способов моделирования нагрузки) для этих сценариев. Симуляцию можно разделить на следующие части:
Конфигурация протокола (в примере — HTTP).
Определение запросов.
Определение сценария.
Вызов основного метода симуляции 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 есть три типа предустановленных способов подачи нагрузки:
Debug. Используется для отладки и проверки работоспособности логики самих скриптов.
class Debug extends Simulation {
setUp(
// Сценарий запустится на одном пользователе.
// Фактически мы получим одну итерацию сценария CommonScenario()
CommonScenario().inject(atOnceUsers(1)),
).protocols(
httpProtocol,
).maxDuration(testDuration)
}
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)
}
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:
Разберем, что происходит в этом коде:
Мы пишем http-запрос с некоторым параметром, значение которого мы хотим параметризовать. Для этого мы используем специальную конструкцию #{value}.
Создаем файловый фидер.
Файл содержит заголовок и значения. Заголовок должен называться так же, как и параметр.
Добавляем фидер в сценарий. Теперь вместо #{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.
Полезные ссылки
Подробнее о сессии в Gatling.
Подробнее о составлении http запросов в Gatling.
Подробнее о фидерах в Gatling.