Всем привет, меня зовут Сергей, я занимаюсь тестированием производительности. Недавно поднялся вопрос в выборе инструмента для воспроизведения довольно интенсивной нагрузки, в основном по HTTP. Инструментов для тестирования производительности сейчас представлено довольно много, в том числе многие из них являются Open Source — проектами и доступны бесплатно. Стало интересно, какой же инструмент справится с подобной задачей лучше, сможет воспроизвести большую нагрузку затратив меньше ресурсов.

Решил поставить несколько популярных инструментов в одинаковые условия и проверить результат. Если интересно что из этого получилось, добро пожаловать под кат.

Дисклеймер

Данное тестирование не претендует на объективность, и показывает результаты только в одних конкретных условиях с конкретным подходом и только при использовании HTTP. Возможно что при изменении подхода или в результате какого-либо тюнинга результаты будут совершенно другие.

Итак, рассмотрим претендентов

Gatling (https://gatling.io/)

Open Source инструмент для тестирования производительности, написан на Scala, поэтому работает на JVM и требует установленную Java, для разработки скриптов можно использовать Scala, Java или Kotlin, но основным языком все-таки считается Scala, большинство встречающихся примеров написано именно на нем. Из коробки умеет работать с HTTP и WebSocket, но для популярных протоколов существуют не официальные плагины. Не имеет интерфейса, все скрипты пишутся при помощи кода или рекордера. Умеет строить довольно красивые краткие отчеты. Gatling не очень популярен в России, но его популярность стремительно растет в последнее время.

Пример отчета формируемый в Gatling
Пример отчета формируемый в Gatling

Apache JMeter (https://jmeter.apache.org/)

Наверно самый известный инструмент для нагрузочного тестирования. Написан на Java, для работы требуется JVM, поэтому может выполняться в любой среде где есть Java. Имеет довольно понятный интерфейс. Большую часть логики запросов можно сделать без программирования, добавляя и конфигурируя нужные блоки. Из коробки поддерживает довольно много протоколов. А так же для JMeter существует огромное количество плагинов реализующих более специфические протоколы которых нет по-умолчанию. Так же вокруг JMeter очень большое сообщество специалистов, в том числе русскоязычных.

K6 (https://k6.io/)

Подающий надежды новичок в области тестирования производительности. Распространяется бесплатно, имеет коммерческую версию, предоставляемую как сервис. Разработан на Go, скрипты пишутся на JS, из коробки умеет работать только с HTTP, но уже имеется набор плагинов для популярных протоколов. Не имеет интерфейса, все делается при помощи кода и параметров запуска.

Locust (https://locust.io/)

Open source фреймворк для тестирования производительности разработанный на Python, скрипты так же пишутся на Python. Очень прост в освоении, особенно для знакомых с Python. Имеет веб-интерфейс для запуска, конфигурирования параметров теста и просмотра результатов.

MF LoadRunner (https://www.microfocus.com/)

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

Для тестирования был написан примитивный веб-сервер на языке Go, который обрабатывает единственный GET-запрос и возвращает статический ответ. Кому интересно, код ниже.

Код
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello World!")
	})
	http.ListenAndServe(":8089", nil)
} 

Максимальная производительность сервиса достигнутая на хост-машине находится примерно на уровне 150 000 запросов в секунду.

Все инструменты, кроме LoadRunner, запускались на виртуальной машине QEMU с ОС Ubuntu Server 20.04, версия ядра 5.4.0-92-generic, конфигурация виртуальной машины 4vCPU, 4GB RAM, само тестируемое приложение размещено на хост-машине, где и развернута виртуалка. Конфигурация хост-машины: AMD Ryzen 5 5500U, 16GB RAM. Kubuntu 21.10 версия ядра 5.13.0-27-generic.

Для увеличения количества подключений, на обоих линуксах были выполнены следующие настройки:

Увеличено максимальное число открытых файлов в /etc/security/limits.conf

* hard nofile 97816
* soft nofile 97816

и количество подключений

sysctl net.ipv4.ip_local_port_range="15000 61000"
sysctl net.core.somaxconn=1024
sysctl net.ipv4.tcp_tw_reuse=1 
sysctl net.core.netdev_max_backlog=2000
sysctl net.ipv4.tcp_max_syn_backlog=2048

LoadRunner запускался на аналогичной конфигурации, но в качестве ОС уже была Windows Server 2019.

Мониторинг построен на связке Telegraf, Influx, Grafana. Telegraf установлен на обоих машинах, Influx и Grafana установлены на хост-машине.

Все инструменты, кроме LoadRunner, были настроены на отправку данных в Influx, понимаю что это создает лишнюю нагрузку, но зачем нужно тестирование без мониторинга.

Результаты тестирования

Если подробности не интересны, лучше сразу перейти к таблице в конце статьи.

Gatling

Для тестирования использовалась версия 3.7.3 (последняя на момент написания статьи)

Скрипт довольно примитивный, каждый виртуальный пользователь делает запрос, каждые 2 мс., количество пользователей возрастает от 1 до 300 за 10 минут.

Код скрипта
import io.gatling.core.scenario._
import io.gatling.http.Predef._
import io.gatling.core.Predef._
import scala.concurrent.duration.DurationInt

class GatlingTest extends Simulation{
  val httpConf = http.baseUrl("http://192.168.122.1:8088")
  val rq = http("SampleRq").get("/")
  val scn = scenario("SampleScenario").forever(
    pace(2.millis).
      exec(rq))
  setUp(
    scn.inject(rampConcurrentUsers(1).to(300).during(600)).protocols(httpConf)
  ).maxDuration(10.minutes)
}

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

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

Количество запросов в секунду
Количество запросов в секунду

Как можно заметить, рост количества запросов продолжался и дальше, и можно заметить пики на уровне 40 000 запросов в секунду, однако производительность на этом уровне была уже очень нестабильной, поэтому за максимум считаю 22 500 запросов в секунду.

Утилизация CPU на виртуальной машине
Утилизация CPU на виртуальной машине

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

Времена отклика
Времена отклика

Время отклика начало расти, как только уровень нагрузки стал нестабильным.

Количество виртуальных пользователей
Количество виртуальных пользователей

Количество виртуальных пользователей увеличивалось равномерно

Количество соединенеий tcp
Количество соединенеий tcp

Количество соединений росло пропорционально нагрузке

JMeter

Для тестирования использовалась версия 5.4.3 (последняя на момент написания статьи) Подход к формированию скрипта здесь аналогичный, для пауз использовался Constant Throughput Timer с целевой производительностью 30 000 запросов в минуту на один поток. Количество пользователей 300, запускаются за 10 минут.

Скрипт
Конфигурация запуска пользователей
Конфигурация запуска пользователей

Стабильный рост нагрузки продолжался примерно до 28 000 запросов в секунду, Jmeter создает минимальное количество подключений, что положительно сказывается на производительности.

Количество запросов в секунду
Количество запросов в секунду

Количество запросов возрастало довольно стабильно, пока не закончились ресурсы CPU.

Утилизация CPU виртуальной машины
Утилизация CPU виртуальной машины

По достижению максимальной утилизации процессора, перестала расти и производительность.

Использование оперативной памяти
Использование оперативной памяти

Оперативная память утилизировалась довольно умеренно.

Количество подключений tcp
Количество подключений tcp

Количество подключений возрастало пропорционально нагрузке.

Время отклика
Время отклика

Время отклика начало расти, как только достигли максимальной производительности.

K6

Для тестирования использовалась версия k6 v0.36.0. Использовался примерно такой же подход к формированию скрипта пауза между итерациями 10 мс. Запуск 200 виртуальных пользователей за 10 минут.

Код скрипта
import http from 'k6/http'; 

import { sleep } from 'k6'; 

export const options = { 

stages: [ 

   { duration: '600s', target: 200 }, 



   { duration: '20s', target:  0 }, 

 ], 

}; 
export default function () { 

 http.get('http://192.168.122.1:8089'); 

 sleep(0.01); 

}

Стабильный рост нагрузки продолжался примерно до 4 500 запросов в секунду.

Количество запросов в секунду
Количество запросов в секунду

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

Время отклика
Время отклика

Время отклика возрастало на протяжении всего теста.

Количество виртуальных пользователей
Количество виртуальных пользователей

Количество виртуальных пользователей возрастало равномерно.

Утилизация CPU вритуальной машины
Утилизация CPU вритуальной машины

Утилизация CPU в начале нестабильной нагрузки была на уровне около 65%.

Использование оперативной памяти
Использование оперативной памяти

Оперативная память утилизировалась существенно, наблюдался значительный рост утилизации в начале нестабильной работы.

Количество коннектов росло пропорционально нагрузке

Locust

Для тестирования использовалась версия 2.5.1. В скрипте прописаны паузы в 200 мс. Между запросами, количество пользователей 200, запускаются по 1 в секунду. Для отправки результатов в Influx использовалась библиотека InfluxDBListener.

Код скрипта
from locust import between, constant, events, tag, task, HttpUser 

from locust_influxdb_listener import InfluxDBListener, InfluxDBSettings 


@events.init.add_listener 
def on_locust_init(environment, **_kwargs): 
   """ 
   Hook event that enables starting an influxdb connection 
   """ 
   influxDBSettings = InfluxDBSettings( 
       influx_host = '192.168.122.1', 
       influx_port = '8086', 
       user = 'admin', 
       pwd = 'admin', 
      database = 'monitoring' 

   ) 
   InfluxDBListener(env=environment, influxDbSettings=influxDBSettings) 

class HelloWorldUser(HttpUser): 
   @task 
   def hello_world(self): 
     self.client.get("/") 
   wait_time = constant(0.2) 

Стабильный рост производительности наблюдался до 835 запросов в секунду, после происходит существенны спад и производительность становится нестабильной.

Количество запросов в секунду
Количество запросов в секунду

При этом утилизируется только одно ядро виртуальной машины.

Вывод команды top
Вывод команды top

Так как использовалось только одно ядро, утилизация CPU была в пределах 25%.

Утилизация CPU виртуальной машины
Утилизация CPU виртуальной машины

После начала нестабильной работы время отклика начало расти.

Время отклика
Время отклика

Оперативная память утилизировалась несущественно.

Использование оперативной памяти
Использование оперативной памяти

MF LoadRunner

Для тестов использовалась верся 2021 build 371, тип скрипта WebHTTP, к сожалению простыми путями указать интервал запуска итераций меньше секунды в LoadRunner не представляется возможным, никакой паузы не указывалось. Запускались 50 пользователей (ограничение бесплатной версии) по одному в минуту.

Код скрипта
Action()
{
	
	web_rest("GET: http://192.168.122.1",
		"URL=http://192.168.122.1:8089",
		"Method=GET",
		"Snapshot=t449913.inf",
		LAST);

	return 0;
}

В RunTimeSettings обязательно нужно снять галочку Sumulate a new user on each iteration, иначе на каждый раз создается новое подключение и подключения быстро заканчиваются.

Стабильный рост производительности продолжался примерно до 7800 запросов в секунду, пока не закончились ресурсы CPU.

Количество запросов в секунду
Количество запросов в секунду

Утилизация CPU росла пропорционально нагрузке.

Утилизация CPU виртуальной машины
Утилизация CPU виртуальной машины

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

Использование оперативной памяти
Использование оперативной памяти

Время отклика начало немного расти после достижения максимальной производительности.

Время отклика.
Время отклика.

Итоговая таблица

Название инструмента

Полученный RPS

1

Gatling

22 500

2

JMeter

28 000

3

K6

4 500

4

Locust

835

5

LoadRunner

7 800

Выскажу свое впечатление по каждому инструменту по результатам тестов.

  • Gatling — мощный инструмент, показал высокую производительность, для новичка в НТ будет сложноват из-за подхода только через код и необходимости осваивать Scala, но специалисту с опытом определенно может пригодиться, особенно если придется тестировать веб-сокеты.

  • JMeter — победитель в текущем конкурсе. Хороший инструмент как для новичка, потому что легок в освоении, так и для специалиста, имеет огромный функционал из коробки, а так же возможности для расширения.

  • K6 — возможно еще сыроват и поэтому показал не очень хорошие результаты. Может требует какого то тюнинга, но из коробки результаты получились не очень хорошие. Если кто то заметил что я сделал с ним не так, напишите пожалуйста в комментариях.

  • Locust — производительность этого инструмента оказалась, мягко говоря, «слабовата». Зато он имеет очень приятный веб-интерфейс с неплохим функционалом и позволяет писать на Python. Думаю для Python-разработчиков, которым по-быстрому нужно протестировать не очень высоко нагруженный проект идеальный вариант.

  • LoadRunner — производительность этого ветерана банковской сферы оказалась примерно по-середине, но и нужно учитывать, что запускался он под Windows. Несомненный плюс его результатов в том что нагрузка была предсказуемой, даже после исчерпания ресурсов CPU производительность не начала скакать, а находилась примерно на одном уровне.

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

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


  1. Ametrin
    02.02.2022 19:15

    Яндекс.Танк нынче уже не популярен? Было бы интересно его с пушкой BFG сравнить с тем же локустом, но у меня руки не доходят(


  1. mrdemon
    02.02.2022 19:46

    Незаслуженно забыты 2 отличных инструмента:

    1. https://github.com/artilleryio/artillery — js, конфиг через yaml. Очень шустрый. Куча возможностей из коробки.
    2. https://github.com/aliostad/SuperBenchmarker — констольный под win, mac. Простой и быстрый, строит красивые графики. Конфиг ключами.


  1. chepk
    02.02.2022 23:09
    +1

    Интересное сравнение, спасибо.

    Времена отклика на графиках в миллисекундах?

    В скрипте Gatling вижу проблему - неправильно использован pacing. Установлено значение 2мс, однако запросы выполняются дольше, что приводит к снижению интенсивности и поэтому cpu не утилизируется выше 65%. Можно попробовать провести повторный тест с pacing 40мс и до 1500 конкурентных сценариев или выше (желательно установить pacing выше максимального времени выполнения сценария и расчитать число конкурентных сценариев соответственно).

    Если нет необходимости в закрытой модели нагрузки, то можно использовать открытую модель, тогда не придётся расчитывать и использовать pacing.

    Если интересует именно RPS, можно также включить общий пул подключений, что позволит ещё немного увеличить интенсивность.

    Было бы интересно увидеть насколько изменятся результаты)


    1. su_lysov Автор
      03.02.2022 09:21

      Времена отклика в миллисекундах, по поводу увеличения конкурентных сценариев, изначально так и делал, производительность была сильно ниже, но вот про общий пул подключений интересная идея, думаю должно помочь, спасибо!


  1. Icecold
    03.02.2022 09:19

    По бест-практис всё-таки стоит разделять генератор и тестируемое приложение. CPU steal time 95+ процентов в явном виде указывает, что виртуалка находится в ожидании выполнения на хосте. Хоть тулы были примерно в равных условиях, проводить сравнение в такой конфигурации, имхо, некорректно.


    1. su_lysov Автор
      03.02.2022 09:26

      Так они же и разделены, генератор нагрузки на виртуалке, тестируемое приложение на хост машине, конечно они могут влиять друг на друга, но и в реальной жизни такое возможно, когда виртуальная машина генератора нагрузки и приложения нарезаны с одной хост-машины. По поводу CPU steal time 95+, моя вина, подложил не очень очевидный график, он показывает значение в стеке и накладывает друг на друга нулевые графики. В реальности steal time максимум 0,782%


  1. Maksimall89
    03.02.2022 10:04
    +1

    Сравнение явно полезное, но из коробки k6 поддерживает намного больше протоколов: https://k6.io/docs/using-k6/protocols/ и для k6 лучше было использовать сценарии (https://k6.io/docs/using-k6/scenarios/executors/), а не старую модель со stages/target - предполагаю, что у вас получились бы совсем другие числа.


    1. su_lysov Автор
      03.02.2022 10:55

      Спасибо! По-возможности попробую.


  1. gigimon
    03.02.2022 16:53
    +1

    Для locust не совсем верное тестирование, в виду его однопоточной python жизни, необходимо запускать slave ноды: http://docs.locust.io/en/stable/running-distributed.html по количеству CPU, как раз для их утилизации


  1. flexoid
    04.02.2022 13:57
    +1

    Для тестирования использовалась версия k6 v0.36.0. Использовался примерно такой же подход к формированию скрипта пауза между итерациями 10 мс. Запуск 200 виртуальных пользователей за 10 минут.

    А в чём смысл такого теста производительности инструмента, если она искусственно ограничивается через задержки?

    Не могу однозначно сказать за другие инструменты, т.к. плотно использовал только Tsung, который не представлен в этом сравнении. Но последнее время активно смотрю в сторону K6, периодически тестирую его и считаю очень перспективным. Поэтому такой низкий результат меня сильно удивил.

    Попробовал частично воспроизвести тест на двух облачных виртуалках. Если просто закомментировать sleep в вашем сценарии k6, получается прирост с 8.6k rps до 41K rps.


    1. su_lysov Автор
      04.02.2022 14:39

      Задержки устанавливаются не для искуственного ограничения, а для управляемого и предсказуемого роста нагрузки, все таки это важно при тестировании производительности. Честно сказать, результат K6 меня тоже удивил, возможно у меня были какие то ошибки при его использовании, но пока не понятно какие. В моем случае без задержки K6 быстро съедает все ресурсы виртуалки и начинает вести себя нестабильно, при этом результат не сильно меняется.


      1. flexoid
        04.02.2022 14:54

        Это интересно. Не очень понятно, как такой тип теста (closed-model, с лимитированным количеством юзеров) может начинать вести себя нестабильно, разве что значение "max users" выбрано слишком большим для тестового окружения.

        В любом случае, использовать искусственную задержку неправильно для такого вида теста, иначе не понятно, производительность чего в итоге тестируется? Учитывая, что в концу теста у нас 200 virtual users в цикле посылают запросы и ждут 0.01 секунду, то общее число итераций в секунду даже без отправки запросов может быть максимум

        1/0.01*200 = 20000

        Если спать 0.02 секунды, получим 10000. Вопрос - а при чём здесь k6 тогда? :)

        P.S. Полная копия вашего 10-ти минутного теста с закоментированным sleep выдает 45K reqs/s без единой ошибки.


        1. su_lysov Автор
          04.02.2022 15:06

          1/0.01*200 = 20000

          В том то и дело, что даже 20к не получилось. Да конкретный сценарий больше 20к не может выдать, но даже если указать ему не 200, а 2000 или 20000 вюзеров, результат не изменится.

          P.S. Полная копия вашего 10-ти минутного теста с закоментированным sleep выдает 45K reqs/s без единой ошибки.

          Тут уже вопрос в каком окружении и сколько ресурсов ему для этого понадобилось, у меня все инструменты запускались в одинаковой, ограниченной среде.


  1. OtocolobusManul
    04.02.2022 17:47

    Спасибо за K6, надо посмотреть на него внимательнее.
    В своей не очень широкой практике использовал еще вот такое:
    1) wrk https://github.com/wg/wrk для совсем простой нагрузки, зато выдает 100K+ RPS
    2) pandora https://github.com/yandex/pandora - пушка от яндекс-танка на Go, есть сценарии
    3) molotov https://molotov.readthedocs.io/en/stable/ - минифреймворк на Python/asyncio для сценариев со сложной логикой
    И да, как уже упоминалось, при замерах RPS имеет смысл физически разделять хосты - источники и приемники нагрузки, чтобы не возникало никакой конкуренции за ресурсы (источник на хост-системе, приемник в виртуалке - это немного не то)