Як налаштувати навантажувальне тестування на Gatling+Java. Гайд для початківців
Усім привіт, мене звати В’ячеслав. Я працюю Senior QA Automation в AMO, компанії з екосистеми Genesis. Продуктивність часто сприймається як щось само собою зрозуміле: якщо функціональні тести пройдено, а код виглядає чистим, все має працювати. Але в реальності «гранична» продуктивність системи може виявитися несподівано низькою. Тож якщо ви ще не впровадили навантажувальне тестування, саме час змінити це.
Такий вид тестування вважається складним у налаштуванні. У цій статті розповім про Gatling — інструмент, з яким можна швидко написати тест, навіть якщо ви робите це вперше, та отримати результати буквально за хвилини. Я розповім, як з нуля створити простий сценарій для навантаження. Потім ми відрефакторимо код та познайомимося з базовими методами Gatling. У кінці я поділюся, як ми працюємо з цим інструментом на практиці.
Gatling
Gatling — це інструмент для навантажувального тестування, який дозволяє симулювати велику кількість одночасних користувачів для перевірки продуктивності вебзастосунків і API. Він використовує асинхронну модель і підходить для тестування високонавантажених систем. Gatling написаний на Scala та підтримує Kotlin, Java, а починаючи з версії 3.13 — також JavaScript. Оскільки Gatling використовує DSL, структура коду майже ідентична незалежно від мови.
Як встановити Gatling
- Найпростіший спосіб — завантажити Gatling з офіційного ресурсу.
- Альтернативний спосіб — клонувати репозиторій з GitHub.
Разом із Gatling встановлюється плагін Maven. Щоби запустити тест, потрібно у вкладці з плагінами обрати Gatling Test або використати команду:
mvn gatling:test -Dgatling.simulationClass=computerdatabase.ComputerDatabaseSimulation
Результати тесту можна переглянути за посиланням або у директорії target/gatling, відкривши файл index.html у браузері.
У звіті є такі дані:
- загальна кількість запитів;
- кількість успішних та неуспішних запитів;
- середній Response Time;
- перелік ендпоінтів із зазначенням кількості запитів до кожного;
- статистика відповідей у персентилях;
- кількість активних користувачів під час симуляції.
Кожна симуляція в Gatling успадковує клас Simulation і складається з таких компонентів:
- setUp — метод, який визначає сценарії та задає параметри інʼєкції навантаження через injectOpen або injectClosed залежно від моделі користувачів.
- HttpProtocolBuilder налаштовує параметри HTTP-протоколу для всіх запитів.
- FeederBuilder забезпечує динамічні дані для тестів, які інтегруються в запити.
- Session — об’єкт для зберігання змінних користувача.
- ChainBuilder допомагає формувати логіку запитів.
- ScenarioBuilder — визначає сценарій поведінки віртуальних користувачів, описуючи послідовність дій, які вони виконують під час тестування, включно з ChainBuilder, паузами та іншими елементами.
- Throttle — використовується для контролю інтенсивності запитів.
- Logger фіксує важливу інформацію для дебагу під час виконання тестів.
- Assertions використовується для деяких перевірок (наприклад, чи кількість помилок менша за 99.99%, або чи не перевищує найдовший респонс-тайм 3 секунд тощо).
- Runtime Parameters потрібні, щоби змінювати конфігурацію тестів без редагування коду.
Побудова сценарію для навантажувального тестування
Тестовий сценарій можна створити вручну, прописуючи кожен ендпоінт та конфігурації. Однак існують способи, які значно спрощують цей процес, особливо якщо у вас немає великого досвіду з навантажувальним тестуванням.
Тестові сценарії у Gatling будуються через спеціальну утиліту — Recorder, яка дозволяє записувати їх за допомогою HAR Converter або HTTP Proxy.
HAR Converter
У вікні рекордера потрібно встановити опцію «No Static Resources», щоб виключити зайві запити до статичних ресурсів. Після цього відкриваєте сайт, увімкнувши запис мережевого трафіку, і виконуєте необхідні дії. Для прикладу у статті використаємо офіційний ресурс від Gatling. У ньому спробуємо зайти у розділ, обрати категорію, потім продукт, додати у кошик і здійснити логін у систему.
Після завершення дій зберігаєте HAR-файл та імпортуєте його у Gatling, де автоматично створиться файл RecorderSimulation із готовою конфігурацією HTTP-протоколу.
HTTP Proxy
Інший спосіб передбачає використання HTTP Proxy. Спершу потрібно налаштувати проксі, вказавши порт для запису трафіку, після чого можна використовувати Postman для виконання запитів. Gatling Proxy перехоплює ці запити, створюючи сценарій APISimulation.
Після запису сценарій потребує рефакторингу: необхідно видалити зайві хедери, прибрати паузи та перейменувати запити, які за замовчуванням мають назви на кшталт Request 0, Request 1.
Щоб зробити сценарій більш зручним, його можна оптимізувати. Наприклад, ви можете вимкнути кешування, вказати використання HTTP/2, а також налаштувати хедери, додавши Content-Type: application/json. Також можна додати теги для організації коду, хоча назви запитів усе одно доведеться змінювати вручну.
Для покращення структури коду доцільно розділити сценарій на окремі блоки за допомогою ChainBuilder. Кожен ChainBuilder містить логіку для певного ендпоінта й обгорнутий у метод exec. Сценарій будується поступово з декількох таких блоків, що полегшує його читання та підтримку. За потреби можна винести частини коду в окремі класи для кращої структуризації.
Щоб перевірити правильність виконання запитів, рекомендується додавати перевірки для кожного ендпоінта. Наприклад, статус-коду, щоби переконатися, що відповідь має статус 200. У разі помилки Gatling сповістить про невдалий запит.
private ChainBuilder getCategories = exec( http("Get categories") .get("/api/category") .check(status().is(200)) ); private ChainBuilder getProducts = exec( http("Get category by id") .get("/api/product?category=7") .check(status().is(200)) );
Також можна додати перевірки наявності потрібного контенту, наприклад, за допомогою CSS-селекторів.
Дебаг у Gatling
Дебаг у Gatling може бути дещо складнішим, ніж у звичних середовищах розробки, оскільки тут немає можливості запускати симуляцію крок за кроком. Натомість для аналізу помилок можна використовувати вивід змінних сесії через System.out або логування.
Щоби налаштувати логування, потрібен файл logback.xml, де визначаються рівні логування: Debug, Info, Error, Trace. Наприклад, якщо встановити рівень Debug, виводитиметься інформація про помилки у запитах. Якщо обрати Trace, фіксуватимуться всі події, включно з успішними запитами.
Для демонстрації дебагу спробуємо навмисно змінити очікуваний код відповіді, наприклад, вказати 201 замість 200. У консолі буде показано помилку із зазначенням ендпоінта, заголовків і тіла відповіді. Це дозволяє швидко ідентифікувати проблемні місця в сценарії.
Якщо симуляції запускаються через CI/CD або окремі джоби, логування можна налаштувати на запис у файл. Для цього потрібно додати відповідний блок у logback.xml, вказавши шлях до файлу, наприклад simulation-error.log. У цьому файлі зберігатиметься інформація про помилки, що допомагає зручніше аналізувати результати тестування.
Що таке сесія і як зберігати змінні
Сесія в Gatling — це структура, яка зберігає дані, пов’язані з віртуальним користувачем під час виконання тесту. Використовується для збереження результатів запитів, параметрів, змінних, які можуть бути використані в подальших діях сценарію.
Кожен віртуальний користувач у симуляції має свою окрему сесію, і ці дані не перетинаються з іншими користувачами. Це означає, що якщо ми збережемо змінну в сесії, то протягом тесту ми зможемо використовувати її в подальших запитах.
Основні методи роботи з сесією:
- set(key, value) — додає або оновлює значення змінної в сесії.
- get(key) — отримує значення змінної з сесії.
- remove(key) — видаляє змінну з сесії.
- contains(key) — перевіряє, чи містить сесія певний ключ.
Приклади використання:
1. Збереження змінних у сесію:
exec(session -> session .set("username", "john.doe") // Зберігаємо змінну "username" в сесію .set("age", 25) // Зберігаємо змінну "age" в сесію )
2. Використання змінних із сесії:
exec(http"Get User Page»>) .get("/user") .queryParam("username", "#{username}") // Використовуємо змінну "username" з сесії .check(status().is(200)))
3. Використання сесії для генерації динамічних URLexec(http(«Get User Page»
):
exec(session -> session.set("userId", "12345")) .exec(http("Get User Profile") .get("/user/#{userId}") .check(status().is(200)));
4. Збереження відповіді в сесію:
exec(http("Login") .post("/login") .formParam("username", "john.doe") .formParam("password", "password123") .check(jsonPath("$.token").saveAs("authToken"))) // Зберігаємо токен аутентифікації в сесію
При отриманні аутентифікаційного токену через ендпойнт ми можемо зберегти його значення за допомогою JSON Path. Наприклад, якщо токен знаходиться в полі token у відповіді, можна використати запит $.token для отримання значення і зберегти його в сесії, забезпечивши доступ до нього протягом усього сценарію. Так немає потреби отримувати новий токен на кожен запит.
private ChainBuilder authenticate = exec( doIf(session -> !session.getBoolean("isAuth")).then( exec( http("Authenticate") .post("/api/authenticate") .body(StringBody(""" { "username": "admin", "password": "admin" } """)) .check(status().is(200)) .check(jsonPath("$.token").exists().saveAs("jwt")) ) .exec(session -> session.set("isAuth", true)) ) );
Ми можемо не тільки зберігати значення з відповіді, а й контролювати стан авторизації через змінні сесії. Наприклад, перед виконанням запиту перевіряємо, чи збережена змінна isAUTH. Якщо ні — виконуємо аутентифікацію, зберігаємо отриманий токен і оновлюємо isAUTH, щоб уникнути повторних запитів на авторизацію.
Після того, як зробили окремий метод для авторизації, можемо його використовувати в кожному з ендпоінтів.
private ChainBuilder createProduct = exec(authenticate) .exec( http("Create product") .post("/api/product") .headers(Map.of("authorization")) .body(RawFileBody("data/0005_request.json")), http("Create product") .post("/api/product") .headers(Map.of("authorization")) .body(RawFileBody("data/0006_request.json")), http("Create product") .post("/api/product") .headers(Map.of("authorization")) .body(RawFileBody("data/0007_request.json")) ); private ChainBuilder updateProduct = exec(authenticate) .exec( http("Update product") .put("/api/product/34") .headers(Map.of("authorization")) .body(RawFileBody("data/0004_request.json")) );
Як працює фідер
Ще одним способом зберігання даних у сесії є використання фідера, який виступає постачальником даних. Як це працює? Уявімо, що у нас є певний ендпойнт для оновлення категорії, наприклад, UpdateCategory, і нам потрібно постійно створювати запити для оновлення даних категорії. Але ми хочемо використовувати різні дані для кожного запиту. Для цього можна створити фідер, наприклад, categoryFeeder.
Фідери можуть бути різними — дані можна брати з файлів CSV, JSON, з баз даних (наприклад, JDBC) або з Redis. У нашому проєкті ми зазвичай використовуємо CSV або JSON-файли.
Як створити та підключити фідер
Створюємо файл, наприклад, categoryFeeder.csv, і заповнюємо його даними для оновлення категорій. Файл містить такі колонки, як CategoryID та CategoryName, де вказується ID категорії та її назва. Після створення вказуємо шлях до нього.
Фідер надає різні методи для вибору даних:
Найбільша технічна конфа ТУТ!🤌
- .circular — коли потрібно безперервно подавати дані, навіть повторюючи їх;
- .queue — коли кожен запис має бути використаний лише один раз, і фідер має завершити роботу після використання всіх даних;
- .random — для випадкових даних без повторів;
- .shuffle — для перемішаних, але унікальних даних;
- .batch — для великих наборів даних, коли важливо знизити навантаження на пам’ять;
- .eager — для максимальної швидкості доступу до даних, коли їхня кількість невелика.
Як правило, для тестування використовують метод Random, але можна обирати метод залежно від потреб.
Після того, як фідер створено, ми можемо використати його у нашому тесті. Для цього вказуємо в тесті, який фідер буде використовуватись для запиту.
- У запиті на оновлення категорії вказуємо фідер як джерело даних для змінних, наприклад, для CategoryID та CategoryName.
- Так само, як раніше використовували змінні в заголовку для JWT-токену, тепер ми можемо використовувати дані з фідера, наприклад, categoryID та categoryName.
Завдяки цьому запити будуть виконуватись з різними даними, і тести не будуть повторюватися з однаковими параметрами.
Приклад використання фідера:
public static ChainBuilder updateCategory = exec(AuthApi.authenticate) .feed(categoryFeeder) .exec(http("Update category") .put("/api/category/#{categoryId}") .headers(authorization) .body(StringBody(""" { "name": "#{categoryName}" } """)) .check(jsonPath("$.name").isEL("#{categoryName}")) );
Так виглядає файл з даними:
Контроль навантаження через Injection Open та Injection Close
У Gatling існують дві основні моделі навантаження: Open Model та Closed Model.
Open Model симулює постійний потік нових користувачів, які приєднуються незалежно від поточної кількості активних користувачів. Використовується для тестування пропускної здатності системи.
Методи:
- atOnceUsers — запускає вказану кількість користувачів одночасно.
- rampUsers — поступово додає користувачів протягом заданого періоду.
- constantUsersPerSec — генерує постійну кількість користувачів за секунду.
- rampUsersPerSec — поступово збільшує кількість користувачів за секунду від початкового до кінцевого значення.
- heavisideUsers — імітує різке навантаження у вигляді стрибка.
- incrementUsersPerSec — поступово збільшує інтенсивність навантаження, додаючи користувачів.
- splitUsers — розділяє користувачів на рівні групи для більш контрольованого навантаження.
Closed Model підтримує фіксовану або контрольовану кількість одночасно активних користувачів. Тестує, як система поводиться під постійним навантаженням.
Методи:
- constantConcurrentUsers — підтримує сталу кількість одночасних користувачів у системі. Наприклад, якщо вказано 100 користувачів, цей метод забезпечить, щоб завжди було рівно 100 активних сесій.
- rampConcurrentUsers — поступово збільшує кількість одночасних користувачів від початкового значення до кінцевого протягом заданого часу. Це дозволяє імітувати збільшення навантаження на систему з часом.
Зазвичай ми використовуємо InjectClosed, коли нам потрібно підтримувати постійну кількість запитів на секунду (RPS) до певних ендпоінтів. Це дозволяє зберігати стабільне навантаження, не даючи користувачам «відпасти» під час тестування.
{ setUp( admin.injectClosed(constantConcurrentUsers(5) .during(Duration.ofSeconds(15))) ) .protocols(httpProtocol); }
Throttle
Throttle в Gatling — це механізм обмеження швидкості відправки запитів під час тестування продуктивності. Він дозволяє моделювати поведінку системи під певним навантаженням, контролюючи кількість запитів, які можуть бути надіслані за певний період часу. Це корисно для перевірки того, як система витримує обмежені потоки трафіку або під час поступового збільшення навантаження. Наприклад, ми можемо почати з 10 RPS і поступово збільшувати навантаження до 20 RPS протягом 10 секунд. Це дозволяє нам коригувати навантаження на систему без різких стрибків, що особливо важливо для реалістичних сценаріїв тестування.
Throttle використовується для таких завдань:
- Імітація реальних умов. Якщо є обмеження за кількістю запитів на одиницю часу, throttle допоможе зімітувати таке навантаження.
- Контрольоване збільшення навантаження. Можна поступово збільшувати кількість запитів, щоб визначити максимальну місткість системи.
- Запобігання перенавантаженню сервера. Використовується для запобігання створенню надмірного навантаження на тестовану систему.
setUp( scenario("Test Scenario") .exec(http("Get Request").get("/endpoint")) .inject(atOnceUsers(100)) ).throttle( reachRps(50).in(10.seconds), // Досягти 50 запитів за 10 секунд holdFor(1.minute), // Утримувати цей рівень протягом хвилини jumpToRps(100), // Швидкий перехід до 100 запитів на секунду holdFor(1.minute) // Утримувати 100 запитів на секунду ще хвилину )
Асершени та їхнє застосування
У навантажувальних тестах важливо не тільки створити запити, а й виконати перевірки, щоб оцінити, чи відповідають результати очікуванням. Для цього в Gatling використовуються асершени. Вони дозволяють перевіряти різні аспекти тесту, наприклад, час відповіді, кількість успішних/невдалих запитів, кількість запитів за одиницю часу, значення користувацьких змінних або отриманих даних.
Існує два типи асершенів:
- Глобальні — застосовуються до всіх запитів або ендпоінтів одразу. Дозволяють задати загальні критерії для всіх тестів.
- Локальні — можуть застосовуватися до конкретного ендпоінту. Наприклад, ми можемо перевіряти, чи час відгуку конкретного ендпоінту не перевищує 300 мс.
У разі, якщо асершени не проходять, запит вважатиметься успішним і продовжить виконуватись, але у звіті буде вказано, що тест не пройшов. Це дозволяє побачити проблеми навіть у тих випадках, коли запит технічно був успішним, але не відповідав заданим критеріям.
Методи асершенів
responseTime():
- min(): мінімальний час відповіді
- max(): максимальний час відповіді
- mean(): середній час відповіді
- percentile(n): час відповіді на n-му персентилі
requestsPerSec(): кількість запитів за секунду
failedRequests():
- count(): кількість невдалих запитів
- percent(): відсоток невдалих запитів
successfulRequests():
- count(): кількість успішних запитів
- percent(): відсоток успішних запитів
Приклад асершенів у тестах
setUp(scn.injectOpen(atOnceUsers(10))) .protocols(httpProtocol) .assertions( global().responseTime().max().lt(500), // Максимальний час відповіді менш як 500 мс global().responseTime().mean().lt(200) // Середній час відповіді менш як 200 мс ); setUp(scn.injectOpen(atOnceUsers(10))) .protocols(httpProtocol) .assertions( global().successfulRequests().percent().is(100) // 100% запитів повинні бути успішними ); setUp(scn.injectOpen(atOnceUsers(10))) .protocols(httpProtocol) .assertions( global().requestsPerSec().gt(100) // Кількість запитів повинна перевищувати 100 за секунду ); setUp(scn.injectOpen(atOnceUsers(10))) .protocols(httpProtocol) .assertions( global().failedRequests().percent().lt(5) // Відсоток невдалих запитів менший за 5% ); setUp(scn.injectOpen(atOnceUsers(10))) .protocols(httpProtocol) .assertions( details("Get Home Page").responseTime().max().lt(300), // Максимальний час відповіді для конкретного запиту details("Get Home Page").successfulRequests().percent().is(100) // 100% успішних запитів для конкретного запиту );
Runtime Parameters
Важливою частиною налаштування є можливість змінювати кількість користувачів під час виконання тесту навантаження, що можна зробити через параметри рантайму. Замість того, щоби вказувати значення кількості користувачів у коді, можна створити змінну, яка буде отримувати своє значення з параметра, переданого під час запуску тесту.
Прописуємо змінну
protected static int USER_COUNT = Integer.parseInt(System.getProperty("USERS"));
Потім її використовуємо в коді:
{ setUp(admin.injectOpen(atOnceUsers(USER_COUNT)) .protocols(httpProtocol) ); }
І запускаємо команду в терміналі:
mvn gatling:test -Dgatling.simulationClass=computerdatabase.ComputerDatabaseSimulation -DUSERS=100
Цей підхід дозволяє змінювати кількість користувачів без необхідності редагувати код тесту та додатково комітити зміни у репозиторій. Це дуже корисно, коли ви автоматизуєте тести у CI/CD системі або коли хочете запускати різні варіанти тестів із різними параметрами, не змінюючи код.
Як ми працюємо з Gatling на практиці у себе на проєкті
Коли зʼявляється завдання провести навантажувальне тестування, важливо чітко розуміти, що саме ми перевіряємо. Часто замовники (менеджери, продакти) просять провести навантажувальний тест, але не завжди можуть точно пояснити, на що саме потрібно звертати увагу. Перед початком тестування важливо визначити, що саме ми хочемо отримати з тесту і які параметри будемо моніторити.
Навантаження тестується на окремому кластері, створеному для тестування, з реальними даними з продакшен БД, при цьому дані змінюються (наприклад, імейл-адреси), щоб уникнути дублювання реальних користувачів. Це дозволяє створити максимально наближене середовище до продакшену, що забезпечить точність результатів.
Ми запускаємо тести ітеративно. Спочатку застосовуємо навантаження, яке відображає реальні показники продакшн-середовища (наприклад, X1), і спостерігаємо за тим, як система реагує на це навантаження. Проводимо тест на поточному навантаженні в реальному продакшн-середовищі. Тут важливо перевірити, чи система витримує навантаження без деградації. Перевіряються такі показники, як response time, стабільність роботи API та правильність налаштування сценаріїв. Використовуємо графіки для моніторингу продуктивності.
Далі збільшуємо навантаження до X2 і спостерігаємо, як система масштабується. Важливо перевірити, чи адекватно реагує Kubernetes-кластер, чи масштабуються поди, чи достатньо ресурсів для обробки навантаження (як консюмери обробляють події, як працюють Redis і Memcache). Якщо спостерігається лаг у системі або консюмери не встигають обробляти дані, додаємо ресурси або збільшуємо кількість консюмерів для покращення пропускної здатності.
За потреби поступово збільшуємо RPS (requests per second). Наприклад, ми можемо навантажити систему на 800 RPS, хоча в реальному продакшен-середовищі пік навантаження не перевищує 200 RPS. Такі тести дозволяють зрозуміти, як система справляється з високим навантаженням. Паралельно ми моніторимо роботу БД, зокрема slow query логи, щоби виявити довгі запити, які потребують оптимізації.
Крім поступового збільшення навантаження, ми також проводимо спайк-тест, який допомагає оцінити реакцію системи на різке, миттєве підвищення навантаження. Наприклад, якщо ми одразу збільшуємо навантаження до 600 RPS, система може не встигати масштабуватися або з’являються інші вузькі місця, які стають очевидними лише під час такого тесту. Спайк-тести також корисні для оцінки того, як система справляється з раптовими змінами навантаження, наприклад, коли навантаження різко падає на нуль, а потім знову збільшується.
Зазвичай ми збільшуємо RPS поступово, протягом 40 хвилин, до досягнення максимальної кількості, яку задаємо для тестування. Потім тримаємо цей рівень RPS протягом 2 годин, щоб перевірити, як система працює під тривалим навантаженням. Після цього запускаємо фінальний тест на максимальній кількості RPS (наприклад, X8) і спостерігаємо за стабільністю системи протягом 6 годин.
Крім основних навантажувальних тестів, ми також проводимо тести, що стосуються специфічних операцій, наприклад, видалення користувачів з БД. Це тестування необхідне для перевірки, як система працює в умовах, коли користувачі видаляються з бази даних через крон джобу. Цей процес може впливати на систему, тому важливо протестувати та упевнитися, що він не викликає проблем у роботі інших частин системи.
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів