company_banner

Нагрузочное тестирование на Gatling

  • Tutorial
Статья публикуется от имени Перфильева Алексея, akaaxel

image altGatling – это framework для проведения нагрузочного тестирования. Он основан на трех технологиях: Scala, Akka и Netty.
В этой статье мы:
  1. Посмотрим, как установить и начать использовать Gatling.
  2. Разберем синтаксис скриптов Gatling на языке Scala.
  3. Напишем небольшой тест, где используем основные функции Gatling. Запустим тестовый скрипт при помощи sbt и сохраним отчет.

Почему Gatling


Большинство специалистов для нагрузки используют Jmeter — до тех пор, пока не понадобится нагружать сокеты.

Мы нашли плагин для Jmeter. Плагин показал плохую производительность: программа работала нестабильно уже при ста открытых коннектах. Gatling стал хорошей заменой: он содержит программный интерфейс нагрузки сокетов и выдерживает до 5000 открытых соединений без сбоев.

Когда мы познакомились с Gatling — его синтаксисом и возможностями — стали переводить все скрипты с Jmeter на Gatling.

Подготовка к работе с Gatling


Устанавливаем Scala SDK и SBT, чтобы создавать скрипты и запускать их в IDE — например, в IntelliJ IDEA с поддержкой SBT проектов.

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



Скрипт размещаем в /src/test/scala/. Чтобы запустить симуляцию из-под sbt, добавляем в plugins.sbt строчку:

addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.0")

В build.sbt добавляем:

enablePlugins(GatlingPlugin)

libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "2.2.2" % "test"

libraryDependencies += "io.gatling" % "gatling-test-framework" % "2.2.2" % "test"

Idea выдаст ошибку на строку enablePlugins(GatlingPlugin), но эта проблема IDE.

Теперь мы готовы разработать скрипт нагрузки.

Синтаксис


Любой скрипт на Gatling состоит из двух частей: конфигурации и самого профиля.

Конфигурация:


Задаем файл с данными о пользователях, которые нагрузят систему:

val users = ssv(fileName).circular

ssv (semicolon separated values ) — формат файла. Ему не обязательно совпадать с расширением файла. В документации можно посмотреть другие поддерживаемые форматы файлов.
fileName — строка с абсолютным именем файла ( C:\data\users.csv )
circular — метод обхода значений в файле. У нас: когда доходим до последней строки с пользователем, возвращаемся в начало.

Дальше задаем конфиг http — он будет работать для всех запросов:

val httpConf = http
           	.baseURL("https://www.tinkoff.ru/ ")
.acceptHeader("*/*")
.acceptEncodingHeader("gzip, deflate, br")
.acceptLanguageHeader("ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3")
.userAgentHeader("Mozilla/5.0 (Windows NT 6.1; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0")
.check(status is 200)

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

Создаем сценарий:

val basicLoad = scenario("BASIC_LOAD").feed(users).during(20 minutes) {
	exec(BasicLoad.start)
  }
setUp(
	basicLoad.inject(rampUsers(1000) over (20 minutes))
  	.protocols(httpConf))
	.maxDuration(21 minutes)

Конфигурация должна содержаться в классе, расширяющий класс Simulation

package load
import io.gatling.core.scenario.Simulation
class LoadScript extends Simulation{
// Здесь наш конфиг
}

Посмотрите на полный проект. Мы создаем сценарий, где используем наших пользователей и конфиг http. За 20 минут скрипт прогонит профиль BasicLoad.start. Если сервер повиснет, на 21-й минуте прогон принудительно завершится. Мы получим отчет по всем данным, которые успели попасть в лог.

Профиль нагрузки:


object BasicLoad {
  val start =	
  exec(
  	http("HTTP Request auth")
    	.post("/rest/session-start")
    	.formParam("login", "${login}")
    	.formParam("password", "${password}")
    	)
	.exec(
  	http("HTTP Request getSkills")
    	.get("/rest/skills")
    	.check(jsonPath("$.id").saveAs("idSkill"))
	)
	.exec(
  	http("HTTP Request getResults")
    	.get("/rest/results")
    	.check(jsonPath("$.id").saveAs("idResult"))
	)
	.repeat(15) {
  	exec(session => {
    	println("Some Log")
    	val tmp = getTen()
    	session.set("ten",tmp)  	
   	})
    	.exec(
      	http("HTTP Request completedtasksreport skill")
        	.get("/rest/v2/completedtasksreport/")
        	.queryParam("dateFrom", "${data}")
        	.queryParam("excludeNoAnswer", "false")
        	.queryParam("orderBy", "ResultDate")
        	.queryParam("orderDesc", "true")
        	.queryParam("skip", "0")
        	.queryParam("take",_.attributes.getOrElse("ten",None))
        	.queryParam("skillIds", "${idSkill}")
           	)
    	.exec(
      	http("HTTP Request completedtasksreport result")
        	.get("/rest/v2/completedtasksreport/")
        	.queryParam("dateFrom", "${data}")
        	.queryParam("excludeNoAnswer", "false")
        	.queryParam("orderBy", "ResultDate")
        	.queryParam("orderDesc", "true")
        	.queryParam("skip", "0")
        	.queryParam("take", _.attributes.getOrElse("idSkill",None))
        	.queryParam("resultId", "${idResult}")
           	)
    	.exec(
      	http("HTTP Request completedtasksreport skill and result")
        	.get("/rest/v2/completedtasksreport/")
        	.queryParam("dateFrom", "${data}")
        	.queryParam("excludeNoAnswer", "false")
        	.queryParam("orderBy", "ResultDate")
        	.queryParam("orderDesc", "true")
        	.queryParam("skip", "0")
        	.queryParam("take", _.attributes.getOrElse("idSkill",None))
        	.queryParam("skillIds", "${idSkill}")
        	.queryParam("resultId", "${idResult}")
            )
    }
}

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

http(samplerName: String).(get|post|put…) отправляет необходимый запрос http. В функции метода http указываем относительный путь. Базовый url мы уже указали при настройке конфига http. Далее указываем параметры запроса — queryParam | formParam.

check проверяет ответ. Можно проверить заголовок ответа. Мы также используем check, когда хотим проверить и сохранить тело ответа или его отдельные элементы.

Любые действия можно выполнить с помощью конструкции:

exec( session => {
// ваш код
})

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

"${idSkill}"

или

_.attributes.getOrElse("idSkill",None)

Запуск и отчет


Запускаем Gatling с помощью sbt.

> sbt
> gatling:testOnly load.LoadScript


Во время запуска в консоль будут поступать данные в формате:

2017-02-02 10:49:27 20s elapsed
---- BASIC_LOAD --------------------------------------------------------------------
[--------------------------------------------------------------------------] 0%
waiting: 0 / active: 10 / done:0
---- Requests ------------------------------------------------------------------
> Global (OK=5155 KO=0 )
> HTTP Request auth (OK=111 KO=0 )
> HTTP Request getSkills (OK=111 KO=0 )
> HTTP Request getResults (OK=111 KO=0 )
> HTTP Request completedtasksreport skill (OK=1610 KO=0 )
> HTTP Request completedtasksreport result (OK=1607 KO=0 )
> HTTP Request completedtasksreport skill and result (OK=1605 KO=0 )


Если какие-нибудь методы упадут, мы сразу увидим ошибку:

status.find.is(200), but actually found 500 1 (100,0%) и запись в KO.

После прогона отчет попадет в папку /target/gatling/SCRIPT_NAME-TIMESTAMP.

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



Если нас интересует конкретный метод, отдельно смотрим статистику по нему:



Нагрузочное тестирование с чужой машины


Если запуск скрипта и анализ результатов проводит ваш коллега, подготовьте его машину:

  1. Скачайте архив. Распакуйте и скопируйте свой скрипт в папку /user-files/simulations/.
  2. Откройте папку /bin и запустите gatling.<bat|sh>.
  3. Выберите свой скрипт в командной строке, нажмите нужную цифру.

После этого начнется нагрузка. Результаты попадут в папку /results. Чтобы посмотреть их, откройте index.html в любом браузере.

В архиве вы найдете утилиту recorder. С ее помощью можно сгенерировать скрипт двумя способами:

  • на основе архива HAR — дамп-вкладки network в окне разработчика в браузере
  • используя утилиту в качестве прокси между браузером и веб-сервером

Генерация скрипта с использованием рекордера не идеальна — в скрипте много «воды» и нет функций проверки ответов. Отчет трудно читать, методы в нем называются request_0, request_1 и т. д.

Что дальше


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

В следующей статье:

  1. Рассмотрим нагрузку сокетов.
  2. Разберем рандомные ветвления.
  3. Зададим RPS.
  4. Сравним производительность Gatling с Jmeter.

Напишите в комментариях, что вы хотели бы обсудить подробнее.
Tinkoff
it’s Tinkoff — просто о сложном

Comments 15

    +1
    Самое клёвое в gatling это то что это тот же самый код на scala и стандартный проект на sbt. Мы подключаем json4s для типизированной работы c ответами и запросами и собираем проект через sbt-native-packager. Единственный минус это отсутсвие распределённого запуска (мы решили его небольшой утилитой которая деплоит артефакт по ssh, запускает, качает логи и собирает стандартный отчёт) и привязанность к graphite (приходится держать influxdb а хотелось бы пушить логи в elastic).
      0
      А что-то помимо http им можно тестить?
        0
        Да. Также из коробки есть поддержка wss, jms и sse. А так в общем вы можете создать любое «brute force» приложение, в статье я написал, что внутри блока
        exec( session => {
        // ваш код
        })
        

        можно писать любой scala код. Так что тут ограничиваемся только своей фантазией и возможностями Scala/Java.
          0
          Ещё есть несколько плагинов (Cassandra, MQTT, Kafka, RabbitMQ, AMQP) http://gatling.io/docs/current/extensions/
        +1
        выдерживает до 5000 открытых соединений без сбоев


        Почему так мало? Netty держит сотни тысяч соединений. Или у вас какой-то особенный кейс?
          0
          Неправильно выразился. Просто у нас не было задачи тестировать бОльшее число коннектов по сокету. Так что эта цифра нас вполне удовлетворила.
          0
          val users = ssv(fileName).circular

          Покажите, пожалуйста, примерное содержимое данного файла.

          И как Вы высчитываете примерный онлайн который выдерживает сайт? Вот в вашем графике видно ~2000 запросов в HTTP Request completedtasksreport skill и 3 KO ( ошибки? )… но это же != 2000 юзерам
            0
            Содержание файла:
            login;password
            nameasd;passasd
            asdname;sdsfjksdfk
            ...


            Чтобы посчитать критическую нагрузку на сайт(на самом деле на одну из бек систем), мы линейно увеличиваем число потоков, при этом мониторим, например, через jmx бек. Также на графике «число ответов в секунду», можно будет выделить момент, при каком числе потоков(клиентов) начинается увеличение числа ошибок.

            Да KO это ошибки.

            2000 запросов это общее число запросов для этого метода за все время нагрузки. Каждый клиент вызывал этот метод 15 раз
            .repeat(15) {
            ...
            }
            
            0
            У меня была задача протестировать нагрузку сервиса, в который через WebSocket отсылались задания, а сервер по частям возвращал результат.

            Я начал писать скрипт на Gatling (тогда версия 2.1.7). Потратил несколько дней, и забил на это дело — год назад Gatling был кривой, а его разработчики — немного неадекватны. Не знаю, если сейчас стало лучше.

            Первой проблемой было то, что Gatling сам закрывал соединение. Я попросил автора помочь разобраться — где искать / как настроить, чтобы посмотреть детальные логи. Он мне сказал, что это типа твой сервер закрывает соединение — сам разбирайся. Потом я нашёл (там для вебсокетов оказывается отдельный логгер), что это у Gatling есть параметр webSocketMaxFrameSize, при превышении этого размера Gatling обрывает соединение без внятного сообщения об этом.
            Как сейчас — уже сделали внятную документацию?

            Второй проблемой было то, что под нагрузкой некоторые соединения помирали (time out). Ну на то оно и нагрузочное тестирование, чтобы обнаружить такие случаи. Но Gatling в таком случае просто подвисал, и не мог распознать time out. Эта задача до сих висит открытой:
            https://github.com/gatling/gatling/issues/2601

            Ну и третьей проблемой была их модель. Я когда спрашивал про некоторые исправления, то бывало отвечали так, что они уже что-то пофиксили, но чтобы получить эти фиксы — берите платную подписку. А хотите open source — ждите у моря погоды, мы когда-нибудь выложим. Такой вот open source.
              0
              Документация не поменялась. Единственный момент, не знаю было ли раньше, но сейчас можно получить конфигурационный файл со всеми параметрами и объяснениями через sbt команду:
              gatling:copyConfigFiles
              


              Насчет второго. Мне помогла принудительная проверка таймаута в таком виде:
              check(wsAwait.within(10 second).until(1).regex(".*conversationCount.*"))
              

              В этом случае если вдруг коннект повиснет, то через 10 секунд вылетит ошибка Check failed: Timeout

              По поводу их модели ничего сказать не могу. Всегда хватало бесплатной версии.
              0
              С интересом ждём следующей статьи про Gatling…

              Мне вот на днях дали задачу нагрузить HTTP endpoint обычными GET запросами.
              Использовал Apache Bench, получается вот так:

              ab -k -n 1000000 -c 1000 "http://10.20.30.40/id1/id2?q1=v1"
              Requests per second: 5424.66 [#/sec] (mean)
              Time per request: 184.343 [ms] (mean)
              Time per request: 0.184 [ms] (mean, across all concurrent requests)

              То есть другими словами, этот скрипт в каком-то смысле эквивалентен вопросу: при нагрузке в 1000 постоянных пользователей, сколько RPS и как быстро может выдать сервер?
              И я получаю, что сервер выдаёт 5424 RPS, и выдаёт ответ в среднем за 184мс.

              Вот никак не могу разобраться, как сделать что-то похожее с Gatling.
              Смотрел все варианты injection profile:
              http://gatling.io/docs/current/cheat-sheet/
              И получается, что Gatling генерирует нагрузку только вот таким образом:
              — Отправляй по X запросов в секунду (constantUsersPerSec) или добавляй по X пользователей в течении какого времени (rampUsersPerSec) и другие комбинации, которые все крутятся вокруг того, что я изначально задаю количество запросов в секунду и посмотреть, как система живёт под такой нагрузкой.
              Но что делать, если я хочу нагрузить систему максимально (как Apache Bench) ограниченным количеством пользователей и померить, сколько RPS сервер выдаёт?
              — atOnceUsers: Отправь X запросов сразу. Но тут проблема в том, что сразу не получится отправить 100000 запросов просто потому что столько соединений сразу не установишь, да и не надо это.

              То есть как сделать запрос по типу: нагружай сервер используя 100 пользователей чтобы они слали запросы один за другим и покажи, сколько запросов (RPS) у них получится сделать?
                0
                Так, вроде бы разобрался:
                package commonCollection
                
                import io.gatling.core.Predef._
                import io.gatling.http.Predef._
                
                class ScsPerformanceSimulation extends Simulation {
                  val scsUsers = 50
                  val scsReqPerUser = 80
                
                  val scs = exec(http("Test").get("/"))
                
                  val httpConf = http
                    .baseURL("http://example.com")
                    .disableCaching
                
                  val scn = scenario("Performance Test")
                    .repeat(scsReqPerUser) {
                      exec(scs)
                  }
                
                  setUp(
                    scn.inject(atOnceUsers(scsUsers))
                  ).protocols(httpConf)
                }
                
                  +1
                  В этом случае можно делать так
                  val scn = scenario("Performance Test").forever(exec(http("Test").get("/")))
                  
                  setUp(scn.inject(atOnceUsers(numberUsers))).protocols(httpConf)
                  

                  Тогда получается что несколько потоков в количестве numberUsers будут постоянно делать get на /

                  Тогда это будет закрытый сценарий где пользователи не выходят из симуляции, у вас в примере открытый где пользователи пройдя по симуляции просто умирают и всё,
                    0
                    Спасибо, не знал про forever. Просто использовал большое число повторений.

                    Вот теперь пытаюсь понять, что Gatling делать с соединениями: в случае закрытого сценария — используется ли одно соединение на каждого пользователя, которое сохраняется на протяжении всей жизни этого пользователя? Или как-то по другому?
                    То есть работает ли в Gatling в режиме Keep Alive или нет?

                    Вот тут описана опция .sharedConnections
                    http://gatling.io/docs/current/http/http_protocol/#connection-sharing
                    Я поэкпериментировал и не заметил, чтобы это давало какую-то разницу в результатах.

            Only users with full accounts can post comments. Log in, please.