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

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

  • Почему Gatling?

  • Gatling в VS Code

  • Простой сценарий HTTP

  • Поддержка GRPC

  • Результаты

  • Итог

Почему Gatling?

Наверное, у большинства читателей возник вопрос, почему не JMeter? Конечно же, перед нами возник такой же вопрос. Чтобы сделать более правильные выбор предлагаю рассмотреть достоинства и недостатки каждого из инструментов.

Итак, Gatling:

  • Gatling имеет высокую интеграцию с GRPC

  • Основан на асинхронной работе, за счёт чего позволяет создавать большую нагрузку с наименьшими затратами

  • Прежде всего пишется из кода, что конечно повышает входной порог, но после изучения ускоряет разработку.

  • Есть возможность написания тестов на разных языках в рамках одного проекта: Java, Scala, Kotlin

Что же JMeter:

  • Имеет GUI, в котором в основном и идёт разработка тестов

  • Низкий порог вхождения, в основном за счёт разработки через GUI и плагины

  • Создаёт разные сценарии за счёт многопоточности, что существенно ограничивает тестирование с одного хоста и приводит к распределëнному тестированию

  • Есть теоретическая поддержка GRPC (на практике интернет пестрит гневными сообщениями, что плагин не работает, к слову, у нас так и не заработал)

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

Gatling в VS Code

В тот момент, когда мы приняли решение в пользу Gatling, мы ещё даже не подозревали, сколько всего придется перечитать и попробовать, прежде чем запустить свой первый тест.

Итак, начнем с основ. "Создаем Gatling скрипты с помощью VS Code" таким был заголовок одной из первых статей, на которую мы наткнулись (см). Следуя, статье в целом можно создать проект и даже написать первый сценарий, который запустится и выдаст результат. Кратко скажу, что для VS Code нужно поставить Metals для сборки Scala и использовать Sbt или Maven для интеграции с Gatling. Sbt более быстрый, но и использует более старую версию Scala. Maven же более современный, а что касается скорости, то разница практически не ощущается.

В целом, с момента принятия решения в пользу Gatling и написания первого рабочего сценария с одним запросом прошло примерно два дня. Тем, кто начинает, рекомендую поставить Metals и сделать выбор в пользу Maven.

Простой сценарий HTTP

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

 В своем базовом виде сценарий выглядит примерно так:

val scn = scenario(ScenarioName)
  .exec(
    http(actionName)
    .get(getUrl)
    .check(
      status.is(200),
      jsonPath("$.id").saveAs(id)
    )
  )
  .exitHereIfFailed
  .exec(
    http(actionName)
    .post(postUrl)
    .body(
      StringBody(session =>
           s"""
               {
                  "id": "${id}",
                  "name": "${name}",
               }
           """)
    )
    .check(status.is(200))
  )

Здесь создаëтся сценарий с именем “ScenarioName”, затем располагаются секции exec, в которых производится описание запроса. Первым идёт запрос http get запрос по адресу “getUrl”, по окончании запроса проверяется статус ответа и извлекается значение поля, которое используется для следующего запроса типа post. На случай падения первого запроса, чтобы второй не выполнялся добавлено “exitHereIfFailed”

Однако мало написать сценарий, ещё необходимо его запустить. Вот один из примеров запуска:

setUp(
   scn.inject(
      atOnceUsers(10)
   )
   .protocols(
      httpProtocol
   )
)

Здесь запускается сценарий по протоколу http для 10 пользователей единовременно. Стоит отметить, что как протокол, так и другие условия запуска можно варьировать и изменять, задавая скорость роста или спада. Можно также комбинировать эти состояния. За счёт изменения свойств протокола можно делать запросы по HTTP 2, задавать базовый адрес и другие параметры транспорта.

При написании сценариев на HTTP в какой-то момент можно столкнуться с проблемой отсутствия грамотной сериализации на Scala. 

Поддержка GRPC

Как бы парадоксально это не звучало, но с GRPC в Scala ситуация значительно проще. Необходим только proto файл и используя его можно сгенерировать все необходимые структуры. 

Получив нужные структуры, достаточно просто собрать сценарий. Например:

val scn = scenario(GRPCScenarioName)
	.exec(       
		grpc("GET_CONFIG")           
		.rpc(ConfigServiceGrpc.METHOD_GET_CONFIG)           
		.payload(config.Empty("test"))	   
		.check(statusCode is Status.Code.OK)	   
		.extract(_.config)(_ saveAs configKey)    
	)    
	.exitHereIfFailed    
	.exec(        
		grpc("SAVE_CONFIG")           
		.rpc(ConfigServiceGrpc.METHOD_SAVE_CONFIG)           
		.payload(session => session(configKey).as[config.SaveConfigMessage])           
		.check(statusCode is Status.Code.OK)	
	)

Здесь первым запросом получаем конфиг, отправляя простое сообщение, в формате сгенерированной ранее структуры из proto файла ConfigServiceGrpc и config.Empty. Проверяем статус и сохраняем в сессии ответ. Затем конфиг идёт на сохранение, также используя структуры, полученные из proto файла. Останется только проверить статусы запросов.

Запуск GRPC сценария не сильно отличается от запуска HTTP. 

setUp(
   scn.inject(
      rampUsers(100).during(1.seconds)
   )
   .protocols(
      grpc(
				 managedChannelBuilder(target=grpcUrl)
				 .usePlaintext()
	 		)
			.shareChannel
   )
)

Здесь запускается сценарий для 100 пользователей в рамках одной секунды. Также стоит заметить использование GRPC канала вместо HTTP.

В случае, если обращение идет по внешнему каналу через 443 порт нужно убрать usePlaintext.

Результаты

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

---- Requests ------------------------------------------------------------------
> Global                                                   (OK=50     KO=0     )
> GetPosts                                                 (OK=10     KO=0     )
> GetPost 1                                                (OK=10     KO=0     )
> GetPost 2                                                (OK=10     KO=0     )
> GetPost 3                                                (OK=10     KO=0     )
> GetPost 4                                                (OK=10     KO=0     )

Также в консоли есть статистические параметры выполненных запросов.

---- Global Information --------------------------------------------------------
> request count                                         50 (OK=50     KO=0     )
> min response time                                     43 (OK=43     KO=-     )
> max response time                                    390 (OK=390    KO=-     )
> mean response time                                   107 (OK=107    KO=-     )
> std deviation                                        103 (OK=103    KO=-     )
> response time 50th percentile                         48 (OK=48     KO=-     )
> response time 75th percentile                        184 (OK=184    KO=-     )
> response time 95th percentile                        285 (OK=285    KO=-     )
> response time 99th percentile                        380 (OK=380    KO=-     )
> mean requests/sec                                     50 (OK=50     KO=-     )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms                                            50 (100%)
> 800 ms < t < 1200 ms                                   0 (  0%)
> t > 1200 ms                                            0 (  0%)
> failed                                                 0 (  0%)

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

Помимо консольных результатов в директории target/gatling можно найти графические результаты по каждому из действия или по всему сценарию и лог выполнения сценария. Расцветку и вывод графиков также можно настраивать через уже упомянутый конфиг файл.  

Итог

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

В дальнейшем, же написание нагрузочных тестов на Gatling занимало достаточно мало времени 10-30 минут в зависимости от сложности теста.

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


  1. shushu
    05.09.2022 01:52

    Интересно, вы k6 рассматривали?
    https://k6.io/


    1. Luchnik22
      05.09.2022 02:12
      +1

      Я рассматривал, там есть большой минус в единицах измерения - virtual users, которые не дают реального понимая сколько запросов может выдержать сервис (хотя это можно настроить).

      В плане нагрузочного тестирования мне больше всего зашёл Яндекс Танк, но для gRPC нужно будет немного дописать код (форк)


      1. Luchnik22
        05.09.2022 15:40

        На всякий случай добавлю, что проблема с k6 и VUs легко решается переходом на старый добрый RPS

        А про причины почему k6 использует именно VUs, можно почитать здесь


  1. primko
    05.09.2022 16:23
    +1

    jsonPath("$.id").saveAs(id) - сохранит значение в сессию, но дальше в примере стоит строковый интерполятор и вызов сессии, но переменные используются не из сессии, оно так не будет работать. Можно же просто указать переменные из сессии и все

     .body(
          StringBody(
               """
                   {
                      "id": "${id}",
                      "name": "${name}",
                   }
               """)
        )

    .check(status.is(200)) - можно не указывать, так как входит в дефолтную проверку

    200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 304

    При написании сценариев на HTTP в какой-то момент можно столкнуться с проблемой отсутствия грамотной сериализации на Scala

    хотелось бы видеть более развернутое описание проблемы, я если честно не понял совсем, с чем у вас проблема


    1. alex_smite Автор
      06.09.2022 08:53

      Спасибо, за найденную ошибку!

      • При написании скриптов для http мы не нашли способа сериализации ответа в объект Scala.

      • Ещё есть проблема с увеличением буфера памяти, поскольку при массовой загрузки файлов в какой-то момент можно напороться на нехватку памяти, при том, что ресурсы машины используются далеко не полностью. (параметры -Xms, -Xmx пробовали менять)


  1. Des96
    05.09.2022 16:23
    +1

    А зачем вручную json создавать? Используйте существующие либы, circe или zio-json там

    Также можно extension написать, чтобы каждый раз не писать asJson