Статья публикуется от имени Перфильева Алексея, 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.

Напишите в комментариях, что вы хотели бы обсудить подробнее.
Поделиться с друзьями
-->

Комментарии (10)


  1. Elufimov
    07.03.2017 11:59
    +1

    Самое клёвое в gatling это то что это тот же самый код на scala и стандартный проект на sbt. Мы подключаем json4s для типизированной работы c ответами и запросами и собираем проект через sbt-native-packager. Единственный минус это отсутсвие распределённого запуска (мы решили его небольшой утилитой которая деплоит артефакт по ssh, запускает, качает логи и собирает стандартный отчёт) и привязанность к graphite (приходится держать influxdb а хотелось бы пушить логи в elastic).


  1. gigimon
    07.03.2017 13:36

    А что-то помимо http им можно тестить?


    1. tinkoff_qa
      07.03.2017 13:59

      Да. Также из коробки есть поддержка wss, jms и sse. А так в общем вы можете создать любое «brute force» приложение, в статье я написал, что внутри блока

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

      можно писать любой scala код. Так что тут ограничиваемся только своей фантазией и возможностями Scala/Java.


      1. Elufimov
        07.03.2017 14:10

        Ещё есть несколько плагинов (Cassandra, MQTT, Kafka, RabbitMQ, AMQP) http://gatling.io/docs/current/extensions/


  1. odiszapc
    07.03.2017 16:22
    +1

    выдерживает до 5000 открытых соединений без сбоев


    Почему так мало? Netty держит сотни тысяч соединений. Или у вас какой-то особенный кейс?


    1. tinkoff_qa
      07.03.2017 16:38

      Неправильно выразился. Просто у нас не было задачи тестировать бОльшее число коннектов по сокету. Так что эта цифра нас вполне удовлетворила.


  1. bamond
    07.03.2017 21:40

    val users = ssv(fileName).circular

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

    И как Вы высчитываете примерный онлайн который выдерживает сайт? Вот в вашем графике видно ~2000 запросов в HTTP Request completedtasksreport skill и 3 KO ( ошибки? )… но это же != 2000 юзерам


    1. tinkoff_qa
      07.03.2017 22:06

      Содержание файла:
      login;password
      nameasd;passasd
      asdname;sdsfjksdfk
      ...


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

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

      2000 запросов это общее число запросов для этого метода за все время нагрузки. Каждый клиент вызывал этот метод 15 раз

      .repeat(15) {
      ...
      }
      


  1. SlavikF
    10.03.2017 09:28

    У меня была задача протестировать нагрузку сервиса, в который через 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.


    1. tinkoff_qa
      10.03.2017 11:46

      Документация не поменялась. Единственный момент, не знаю было ли раньше, но сейчас можно получить конфигурационный файл со всеми параметрами и объяснениями через sbt команду:

      gatling:copyConfigFiles
      


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

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

      По поводу их модели ничего сказать не могу. Всегда хватало бесплатной версии.