Інструменти тестування продуктивності. Частина 3

💡 Усі статті, обговорення, новини про тестування — в одному місці. Приєднуйтесь до QA спільноти!

Всім привіт. Я Сергій Моренець, розробник, викладач, спікер і технічний письменник, хочу поділитися з вами своїм досвідом роботи з такою цікавою темою, як тестування ефективності або продуктивності програми.

У першій частині цієї статті я зробив огляд найпопулярніших технологій, розповів про їхню історію створення та основну функціональність. У другій частині я розповів про застосування Wrk і K6, в цій частині я розповім про використання Gatling і JMeter, а також підведу підсумки тестування.

У нас за останні роки накопичилося достатньо досвіду роботи з цими системами, і ми розглядаємо ці технології на деяких наших тренінгах. Але, зрозуміло, це настільки велика тема, що з кожної технології можна написати окрему статтю. Мені було цікаво порівняти можливості всіх чотирьох технологій. Сподіваюся, що ця стаття буде корисною всім, хто планує займатися тестуванням ефективності.

Gatling

Перейдемо до Gatling. З ним все складніше у плані автоматизації. Тут є Docker image для Enterprise версії, але тільки для корпоративних клієнтів. Є сторонній image, але, на жаль, перестав оновлюватися 4 роки тому. Тому доведеться писати власний gatling.dockerfile на основі JRE 20:

FROM eclipse-temurin:20-jre-alpine
WORKDIR /opt
ENV GATLING_VERSION 3.9.5
RUN mkdir -p gatling
RUN apk add --update wget bash libc6-compat && \
  mkdir -p /tmp/downloads && \
  wget -q -O /tmp/downloads/gatling-$GATLING_VERSION.zip \
  https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/$GATLING_VERSION/gatling-charts-highcharts-bundle-$GATLING_VERSION-bundle.zip && \
  mkdir -p /tmp/archive && cd /tmp/archive && \
  unzip /tmp/downloads/gatling-$GATLING_VERSION.zip && \
  mv /tmp/archive/gatling-charts-highcharts-bundle-$GATLING_VERSION/* /opt/gatling/ && \
  rm -rf /tmp/*
 
WORKDIR /opt/gatling
ENV PATH /opt/gatling/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV GATLING_HOME /opt/gatling
ENTRYPOINT ["gatling.sh"]

Скрипти Gatling можна написати на Java/Scala/Kotlin. Скористаємося Java-варіантом і створимо клас UserSimulation:

public class UserSimulation extends Simulation {
 
  HttpProtocolBuilder httpProtocol = http
    .baseUrl("http://user:8080");
 
  ScenarioBuilder scn = scenario("UserSimulation")
    .exec(http("request")
      .get("/benchmark"))
    .pause(0, 3);
 
    {
    setUp(
      scn.injectClosed(constantConcurrentUsers(Integer.getInteger("users")).during(Integer.getInteger("duration", 20)))
    ).protocols(httpProtocol);
  }
}

Як ви бачите, єдина вимога — використання Simulation як базового класу. Всередині класу ми створюємо об’єкт HttpProtocolBuilder і вказуємо базовий URL для наших запитів, параметри запиту, різні необхідні заголовки і т.д.

Потім ми вказуємо відносний шлях (/benchmark) та знову паузу між запитами. Якщо вказати два граничні значення (0 і 3), то це означатиме, що Gatling буде щоразу використовувати для паузи випадкове число від 0 до 3 секунд.

У методі setup() ми вказуємо (впроваджуємо) наших користувачів, причому викликаємо метод injectOpen(). У Gatling є дві моделі контролю за створенням користувачів:

  • Closed — коли кількість користувачів фіксована.
  • Open — коли кількість користувачів може змінюватися згодом за різними правилами (throttle, ram-up, stress testing і т.д.).

Виберемо закриту модель з фіксованою кількістю користувачів (constantConcurrentUsers). І у методі during вкажемо тривалість тесту. Кількість одночасних користувачів та тривалість тестування беруться з системних змінних («users» та «duration»). Але як ми їх передаватимемо до цього класу?

На відміну від K6, де використовується JavaScript, в Gatling усі скрипти повинні бути відкомпільовані. Тому якби ми зараз запустили наш скрипт, то отримали б помилку:

germes-gatling-1 | Wrapped by: java.io.IOException: Cannot run program «javac» (in directory «/opt/gatling»): error=2, No such file or directory

Тому як базовий образ для Gatling контейнера потрібно вказувати JDK, а не JRE:

FROM eclipse-temurin:20-jdk-alpine

Головний скрипт в Gatling — gatling.sh (або gatling.bat). Саме йому ми і передаємо всі параметри:

gatling.sh -erjo «-Dusers=100 -Dduration=20» --batch-mode --run-mode local --simulations-folder /opt/gatling/scripts/ --simulation UserSimulation

Тут:

  1. -erjo — додаткові параметри для JVM (наші системні змінні);
  2. --batch-mode означає, що ми вимикаємо інтерактивний режим з різними питаннями та підтвердженнями для користувача;
  3. --run-mode дозволяє вказати тип запуску (локальний, розподілений чи хмарний);
  4. --simulations-folder вказує папку зі скриптами;
  5. --simulation містить назву класу-скрипту, який ми хочемо запустити.

Шоста таблиця показує роботу Gatling для Spring MVC застосунку після прогріву.


Users


Latency (ms)


Req/sec


CPU, Gatling


CPU, App


Mem, Gatling


Mem, App


100


2


60


45


32


374


331


1000


1


591


96


78


431


338


5000


11


2936


1548


18


566


550

Заради інтересу приберемо паузу з тесту та подивимося, як це позначиться на результатах. Це відразу призвело до великої кількості ConnectException на сервері (68% всіх запитів) і latency злетіло до 660 мс.

JMeter

Тепер перейдемо до останньої технології у цьому огляді — JMeter. Це єдина тулза, яка містить повноцінний UI на базі Java Swing, і це є істотною перевагою, тому що дає можливість використовувати її, не вдаючись до програмування. А це знижує поріг входження для застосування цієї технології.

Для роботи з JMeter потрібно спочатку запустити виконуваний файл (jmeter.bat/jmeter.sh) в режимі GUI (дефолтному), а потім створити свій тестовий план (Test Plan), тобто сценарій тестування вашого застосунку і потім зберегти в XML-форматі (з розширенням. jmx). Тестовий план в JMeter — це контейнер для всіх інших елементів конфігурації.

Перший елемент — це Thread Group, де потрібно вказати два найважливіші параметри тесту — кількість користувачів і тривалість виконання тесту (duration). Але кількість користувачів змінюється від тесту до тесту ,та використовувати константу тут не вийде.

На щастя, у JMeter є properties. Ми можемо вказати значення ${__P(users)}, а потім передати значення цього property через опцію -J при старті JMeter (-Jusers=100). Ще один важливий параметр — ramp-up чи прогрів. Справа в тому, що JMeter запускається за допомогою JVM і їй самій потрібен деякий час, щоб запрацював JIT-компілятор, включилися певні оптимізації коду і т.д.

Наступний елемент — HTTP Request, який містить всю конфігурацію для того HTTP запиту, який JMeter надсилатиме на сервер:

  • Host name/port.
  • Відносний шлях та HTTP-метод.
  • Тіло запиту (для запитів POST/PUT) та багато іншого.

Щоб переглянути результати тесту, додамо елемент Aggregate Graph. Тут можна або дивитися на результати тестування в режимі реального часу (якщо ви запустите тести через GUI), або завантажити файл результатів після.

А де ж паузи, які ми застосовували у K6/Gatling? Тут вони теж є, але у вигляді елемента Constant Timer, де ви вказуєте затримку в мілісекундах:

Ми не робитимемо цього, щоб не перевантажувати цей блок інформацією.

Тепер створимо Dockerfile для нового контейнера. На щастя, вже є базовий образ так, що потрібно лише його використовувати:

FROM justb4/jmeter
ADD docker-scripts/benchmark/jmeter/test.jmx /opt/

Команда для запуску тесту виглядає так:

jmeter -n -t /opt/test.jmx -l /opt/res.csv -e -Jusers=100

Тут ми вказали:

  • опцію -n (batch режим без GUI). Такий режим рекомендований авторами JMeter, щоб GUI не впливала на продуктивність при тестуванні;
  • -t (назва файлу);
  • -e (генерувати report dashboard після тестування);
  • — l (файл із результатами у форматі CSV);
  • -J (properties).

Така команда використовується для локального запуску JMeter. Нам же потрібно прибрати jmeter з початку, тому що він і так автоматично додається до Docker image. Сьома таблиця показує роботу JMeter для Spring MVC-застосунку після прогріву.


Users


Latency (ms)


Req/sec


CPU, JMeter


CPU, App


Mem, JMeter


Mem, App


100


2


42310


570


941


5030


573


1000


13


44681


586


928


5210


756


5000


27


41849


597


840


5730


871

Для запуску зі 100 користувачами файл результатів становив 86 мегабайт.

Висновки

Усі performance tools відпрацювали на відмінно, без помилок, але, зрозуміло, лише на рівні своїх можливостей. Приз за найпростіший інтерфейс і мінімальний поріг входження заслуговує Wrk. Це, по суті, утиліта командного рядка, яка дозволяє однією командою запустити load/ stress testing. Ще один її плюс — мінімальне споживання ресурсів:

  • від 7 до 70 Мб пам’яті в самому піковому випадку;
  • 300-330% завантаження CPU.

Її мінус — необхідність писати скрипти для налаштування запитів мовою Lua. Для найпростіших прикладів є готові скрипти, але для складніших доведеться хоч трохи знати Lua. Якогось просунутого готового DSL тут немає. У принципі, хоча Wrk і створили у нашому столітті, його мінімалізм дозволяє назвати його дитиною 20 століття. Він не підтримує ні розподіленого, ні хмарного, а лише локальне використання.

Тепер поговоримо про найненажерливішого учасника нашого експерименту — JMeter. Його споживання ресурсів зашкалює:

  • мінімум 5 Гб пам’яті;
  • завантаження CPU 570-600%.

Головний плюс JMeter — наявність GUI, де немає необхідності застосовувати навички програмування. А ще безліч підтримуваних протоколів і налаштувань, які помітно полегшують його використання. Це дуже стабільна технологія, думаю, що конфігураційні файли, створені 15-20 років тому, чудово працюватимуть з поточною версією JMeter.

Завдяки своїй плагінній архітектурі він не застаріває, а навпаки, обростає все новими розширеннями. На поточний день ви можете скористатися понад 100 плагінами.

K6 та Gatling — два учасники, які, навпаки, інтенсивно розвиваються, і де ми писали скрипти для тестування. І в тому, і в іншому випадку у вас буде багатий DSL для налаштування ваших скриптів.

K6 використовує JavaScript, тому тут особливо немає необхідності в якомусь спеціальному IDE для роботи, тоді як Gatling скрипти вимагають Maven залежностей та IDE для того, щоб перевірити, щоб вони хоча б компілюються. Обидва вони підтримують хмарний режим роботи (у Gatling є Enterprise версія), а K6 — ще й розподілене тестування.

K6 завдяки розширенням може відправляти результати своєї роботи практично в будь-яку систему візуалізації, таку як Prometheus, Grafana, InfluxDB або New Relic. Gatling також має можливість інтеграції з деякими системами моніторингу, такими як Graphite, але тільки в платній версії Gatling Enterprise.

Але Gatling має зручний спосіб запуску через Maven/Gradle плагіни, які не вимагають його локальної інсталяції або Docker контейнера.

Що стосується споживання ресурсів, то у Gatling воно на диво помірне — 400-550 Мб пам’яті, тоді як K6 здивував тим, що для варіанта з 1000 користувачами використовував 440 Мб, а 5000 — вже 1800 Мб. Така поведінка заслуговує на додаткове дослідження.

Ще одна важлива характеристика будь-якої технології — підтримка спільноти, наявність великої бази запитань та відповідей і документації. Якщо взяти Stackoverflow, то тут безумовним лідером є JMeter:

  • JMeter — 18854 питання;
  • Gatling — 1674;
  • K6 — 305;
  • Wrk — 41.

В цілому можна сказати, що написання performance-тестів не є тривіальним процесом і вимагає знання не тільки можливостей performance tools, але і сценаріїв роботи користувачів з вашим застосунком.

Підсумковий docker-compose-benchmark.yml виглядає так:

version : '3.8'
services:
  wrk:
    build:
      context: .
      dockerfile: docker-scripts/benchmark/wrk.dockerfile
    command: wrk -t4 -c100 -d20s http://user:8080/benchmark
  k6:
    build:
      context: .
      dockerfile: docker-scripts/benchmark/k6/k6.dockerfile
    command: run --vus 5000 --duration 20s /opt/test.js
  gatling:
    build:
      context: .
      dockerfile: docker-scripts/benchmark/gatling/gatling.dockerfile
    command: gatling.sh -erjo "-Dusers=5000 -Dduration=20" --batch-mode --run-mode local --simulations-folder /opt/gatling/scripts/ --simulation UserSimulation
  jmeter:
    build:
      context: .
      dockerfile: docker-scripts/benchmark/jmeter/jmeter.dockerfile
    command: -n -t /opt/test.jmx -l /opt/res.csv -e -Jusers=100

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

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

Scala выглядит угрожающе, но на деле Gatling DSL простой (так, я без знания Scala начал писать сценарии в первый же рабочий день)

«Тепер поговоримо про найненажерливішого учасника нашого експерименту — JMeter. Його споживання ресурсів зашкалює:
мінімум 5 Гб пам’яті;»
Для сценарію з 1 реквестом, рілі?))

1) проблема в тому, що автор дивиться моніторинг докер контейнера і робить висновок, що саме стільки джиметр і використовує. Але реальність така: те що ми бачимо в таск менеджері/актівіті моніторі це зарезервована память. Її джава просто зайняла, оскільки ви їй дозволили це в параметрах -Xms -Xmx.
Якщо поставимо -Xms=4g -Xmx=4g, то бачитимемо 4 гіга в моніторингу, але насправді використовуватися може 300mb

Після таких статей якраз і з’являються питання «я бачив в статті, шо джиметр використовує х100 більше памяті ніж інші, порадьте альтернативу»

2) jmeter, або точніше jvm буде використовувати ту память, яку ви їй виділите. А garbage collection, очистка/вивільнення пам’яті, буде запускатися, коли eden space в heap заповнюється до певного рівня.
В залежності від загального розміру heap, розмір eden space буде відрізнятись, тобто якщо ви виділили 4 гіга пам’яті, джиметр використовуватиме більше пам’яті, перед тим як запустити очистку, ніж якщо б ви виділили джиметру 500 мегабайт.
Для того щоб зрозуміти скільки насправді використовується памяті під час вашого тесту, можна підключитись профайлером до джиметра, або налаштувати моніторинг heap, наприклад використовуючи jolokia2 в telegraf.

Візьмемо для прикладу сценарій з статті, з одним реквестом і 100 потоками, без затримок.
Підняв локально мок, зробив аналогічний сценарій і запустив тест на 3х конфігураціях джиметру:
1) -Xms=4g -Xmx=4g heap. Реальне використання heap було max 2.5gb, хоча в актівіті моніторі було 4gb.
2) -Xms=500mb -Xmx=500mb heap. Реальне використання heap max 375mb.
3) -Xms=200mb -Xmx=200mb heap. Реальне використання heap max 170mb.

У всіх трьох випадках throughput був ~12000rps, тобто просадки по навантаженню, при зменшенні виділеної пам’яті, не було. Чому так вийшло, що зменшивши кількість пам’яті, всеодно все ок? Тому що, в залежності від загалього розміру пам’яті, розмір eden space також різний: 120mb, 316mb і 2.5gb відповідно. І насправді для такого простого скрипта джиметру норм працювати і з 200mb пам’яті, але якщо ви щедро виділили більше, він не буде часто витрачати ресурси процесора на garbage connection, а буде використовувати весь доступний об’єм eden space.

Підсумовуючи, використання ресурсів напряму залежить від складності вашого сценарію, наскільки він оптимізований, конфігурації інструмента, яке навантаження ви генеруєте ТА ЧИ ПРАВИЛЬНО ВИ АНАЛІЗУЄТЕ ВИКОРИСТАННЯ РЕСУРСІВ.
Ну і у випадку з джиметром, ствердження, що вам необхіодно мінімум 5гігів пам’яті для сценарію з одним реквестом, та навіть не з одним, точно не відповідає дійсності :)

Підписатись на коментарі