Введение
Когда вы знакомитесь с новой технологией, один из первых вопросов — насколько хорошо она работает. В этой статье мы рассмотрим этот аспект и постараемся ответить на все ваши вопросы, связанные с производительностью.
Для этих тестов мы используем наше демо приложение — Bookstore, которое можно найти по ссылке к репозиторию. Цель — измерить, сколько системных ресурсов типичное приложение Jmix потребляет при обработке большого количества пользователей (для Jmix и других stateful приложений).
Окружение
Настройки Bookstore
Прежде чем перейти к результатам, давайте рассмотрим изменения, которые мы внесли в приложение для настройки тестирования производительности.
1. Добавили Actuator и Prometheus Micrometer Registry
Мы добавили эти библиотеки, чтобы открыть эндпоинт Prometheus Actuator по адресу
/actuator/prometheus:
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
Остальную настройку конечных точек и другие мелкие корректировки можно посмотреть в данном коммите.
2. Отключили проверки Vaadin
Чтобы сделать тесты производительности более плавными, мы отключили некоторые проверки безопасности Vaadin, не релевантные для данного сценария тестирования. В частности, мы отключили защиту XSRF и проверку идентификатора синхронизации, изменив файл application-perf-tests.properties:
# Поддержка тестов JMeter
vaadin.disable-xsrf-protection=true
vaadin.syncIdCheck=false
3. Отключили таймер уведомлений о задачах
Мы отключили таймер уведомлений о задачах, чтобы упростить процесс записи и эмуляции HTTP-запросов в JMeter. Это позволяет сосредоточиться на тестировании основной функциональности приложения без дополнительных фоновых задач, влияющих на результаты.
Инструменты
Мы так же использовали дополнительное программное обеспечение:
- База данных PostgreSQL.
- Node Exporter для получения метрик сервера, таких как загрузка CPU.
- JMeter с Prometheus Listener для генерации тестовой нагрузки и предоставления метрик на стороне клиента.
- Prometheus для сбора и хранения результатов.
- Grafana для визуализации.
Теперь, когда мы рассмотрели программные конфигурации, перейдем к аппаратному обеспечению.
Конфигурация сервера
Что сервер приложения, что сервер базы данных — виртуальные машины со следующими характеристиками:
Характеристики:
- Виртуализация: Proxmox VE, qemu-kvm
- CPU: 8 ядер, x86-64-v3
- RAM: 16 ГБ
- HDD: 200 ГБ
- ОС: Ubuntu 22.04
Хостовое оборудование:
- CPU: Intel Xeon E5 2686 v4 2.3-3.0 ГГц
- RAM: DDR4 ECC 2400 МГц
- HDD: ZFS, Raid1 из 2-х Sata Enterprise SSD SAMSUNG MZ7L33T8HBLT-00A07
Конфигурация средняя, виртуализированная среда способна эффективно обрабатывать типичные корпоративные приложения. Для более высоких нагрузок или более сложных систем может потребоваться дополнительная оптимизация, но эта конфигурация хорошо работает для большинства внутренних приложений.
Инфраструктура
Мы используем три сервера в общей сложности, два для запуска тестов и один, посвященный сбору метрик:
- Сервер приложения содержит приложение Bookstore, запущенное как boot jar, и Node Exporter.
- Сервер базы данных содержит базу данных PostgreSQL и приложение JMeter. Этот сервер хранит постоянные данные и создает тестовую нагрузку.
- Сервер метрик включает приложения Prometheus и Grafana. Сбор и обработка метрик.
Распределяя нагрузку между выделенными серверами для приложения, базы данных и метрик, мы обеспечиваем эффективное использование ресурсов и точный сбор данных. Переходим к самому тесту.
Тест
Для создания необходимой нагрузки без использования большого количества ресурсов на веб-клиентах мы использовали JMeter HTTP(S) Test Script Recorder. Этот подход позволяет записать последовательность HTTP-запросов, а затем воспроизвести ее в большом количестве потоков одновременно, используя небольшое количество ресурсов. Однако невозможно реагировать на изменения в режиме реального времени, как это делает полноценный клиент Vaadin. Поэтому был использован простой сценарий тестирования, чтобы избежать ошибок и некорректных запросов.
План тестирования был создан согласно этой инструкции с некоторыми дополнениями и изменениями.
План теста для каждого пользователя (потока) представляет бесконечный цикл следующих действий:
- Загрузить страницу входа
- Подождать в среднем loginWait миллисекунд
- Ввести учетные данные (hikari/hikari) и войти
- Открыть Order List View
- Нажать кнопку "Create", чтобы открыть Order Details View с новым объектом Order
- Выбрать клиента (одного из первых 10 клиентов в списке, в зависимости от номера потока)
- Создать 3 строки заказа, выполняя следующие действия:
a. Нажать кнопку "Create" в блоке Order Lines
b. Выбрать продукт
c. Нажать кнопку "OK" - Подтвердить создание заказа нажатием кнопки "OK"
- Подождать в среднем logoutWaitAvg миллисекунд
- Выйти из системы
Чтобы реалистично эмулировать действия пользователя, между каждым кликом и предыдущим запросом есть среднее время ожидания clickPeriodAvg
миллисекунд. Вход и выход требуются для каждого цикла из-за ограничений подхода HTTP-запросов. Вход в систему — трудоемкая операция и требует балансировки с временем бездействия (loginWait, logoutWait
), чтобы сделать тест более похожим на действия обычного пользователя.
Параметры плана теста можно указать с помощью аргументов командной строки -J<parameterName>
во время запуска теста. Например: -JthreadCount=500
.
Доступны следующие параметры:
Имя параметра | Значение по умолчанию | Описание |
---|---|---|
host | localhost | Хост приложения Bookstore |
port | 8080 | Порт приложения Bookstore |
scheme | http | Схема приложения Bookstore |
threadCount | 1000 | Количество пользователей, одновременно выполняющих шаги теста |
rampUpPeriod | 60 | Время "разгона" до полного числа пользователей, с |
clickPeriodAvg | 2000 | Средний период между кликами, мс |
clickPeriodDev | 1000 | Минимальное и максимальное отклонение периода клика, мс |
loginWait | 10000 | Время ожидания перед входом, мс |
logoutWaitAvg | 30000 | Средний период ожидания перед выходом, мс |
logoutWaitDev | 10000 | Минимальное и максимальное отклонение периода перед выходом, мс |
Теперь, когда мы можем эмулировать взаимодействия, которые будем отслеживать, можем проверить метрики, которые будут важны на основе этих взаимодействий.
Метрики
Чтобы оценить производительность, мы отслеживали несколько ключевых метрик в течение всего теста, чтобы получить представление о том, как система ведет себя под нагрузкой:
- Время загрузки представления измерялось временем HTTP-ответа для всех запросов, связанных с загрузкой и инициализацией представления. JMeter Prometheus listener (см. план теста) предоставляет эти метрики. Следующие запросы связаны с действиями:
- Открытие List view: LoadOrdersListView-34, OrderCreation-37, OrderCreation-38
- Открытие Detail view: CLICK-create-new-order-39, OrderCreation-40, OrderCreation-41
- Сохранение сущности и возврат к списку: CLICK-save-order-70, OrderCreation-71, OrderCreation-72 - Среднее время сохранения сущности измеряется с помощью Micrometer registry timer в OrderDetailView.java#saveDelegate
- Использование CPU предоставляется Node Exporter на сервере приложения.
- Среднее использование heap-памяти предоставляется Micrometer registry.
- Выделенная heap-память устанавливается ключом -Xmx при запуске приложения.
Эти метрики были выбраны, чтобы дать нам всестороннее представление о производительности приложения и эффективности использования ресурсов.
Запуск и измерение
Настройка окружения
Сначала мы установили Node Exporter на сервер приложения, используя tarball, чтобы обеспечить мониторинг производительности в реальном времени. Тем временем на сервере базы данных мы развернули PostgreSQL 16.3 через Docker, используя официальный образ.
Далее мы добавили JMeter 5.6.3 на сервер базы данных для проведения нагрузочного тестирования. Наш сервер метрик был усилен с помощью Docker, так как мы развернули Prometheus с использованием настроенной конфигурации. В завершение мы добавили Grafana 11.1.4 на сервер метрик, следуя официальной документации для визуализации.
Сборка и запуск приложения
С настроенным окружением пришло время запустить приложение! Мы собрали его командой ./gradlew build -Pvaadin.productionMode=true bootJar
, обеспечив оптимизацию для продакшена. Затем мы скопировали полученный jar на сервер приложения и запустили его с помощью команды:
java -jar -Dspring.profiles.active=perf-tests -Xmx<N> jmix-bookstore.jar
где <N>
может быть установлено в 2g, 5g, 10g или 14g, в зависимости от того, сколько памяти мы хотели выделить.
Запуск теста
План теста запускается на сервере базы данных с помощью следующей команды:
./jmeter -n -t path/to/OrderCreation.jmx -l logs/log.txt -JthreadCount=1000 -Jhost=<app_server_ip> -JrampUpPeriod=230
Эта команда запускает план теста JMeter на сервере базы данных. Она симулирует 1000 пользователей, взаимодействующих с приложением, постепенно увеличивая нагрузку в течение 230 секунд и записывая результаты теста в log.txt.
Измерения
Давайте посмотрим на визуализацию результатов для теста с 5 ГБ размера heap и 1000 пользователями, работающими согласно описанному плану теста с параметрами по умолчанию и rampUpPeriod=230
секунд.
Память
Использование памяти можно визуализировать с помощью метрики jvm_memory_used_bytes
и выглядит это так:
Использование CPU
Использование CPU измеряется метрикой node_cpu_seconds
, суммируя все режимы, кроме "idle"
:
Как видим, все 8 ядер процессора почти полностью загружены (90%). Такая же картина наблюдается и для всех других тестов с 1000 пользователями, параметрами плана теста по умолчанию и различным объемом выделенной памяти.
Время загрузки представления
Среднее время загрузки представления измеряется путем вычисления скорости метрики jmeter_rt_summary_sum
и суммирования результатов для каждого запроса, участвующего в загрузке конкретного представления.
Для Order List View
время загрузки выглядит следующим образом:
Есть начальное увеличение нагрузки в начале теста, которое стабилизируется через несколько минут. Поэтому мы исключим первые 10 минут из анализа и сосредоточимся на среднем потреблении ресурсов в течение одного из последующих периодов, когда значение остается относительно постоянным. Обычно это происходит примерно на 40-й минуте нагрузочного теста.
Для OrderListView
среднее время загрузки составляет 2,8 с (для 1000 пользователей и 5 ГБ размера heap).
Время сохранения сущности
Мы можем наблюдать время сохранения DataContext Order Details
с помощью метрики jmix_bookstore_order_save_time_seconds
:
Среднее время сохранения сущности относительно небольшое (0,015 с) по сравнению со временем загрузки представления.
Результаты
Мы провели тесты с различным размером heap-памяти: 2, 5, 10 и 14 ГБ. В каждом случае 1000 пользователей работали согласно описанному сценарию со следующими параметрами:
- loginWait = 10 с
- logoutWaitAvg = 30 с
- logoutWaitDev = 10 с
- clickPeriodAvg = 2 с
- clickPeriodDev = 1 с
- rampUpPeriod = 230 с
Результаты измерений представлены в таблице ниже.
Выделенная heap-память, ГБ | Среднее использованное heap-памяти, ГБ (%) | Среднее время загрузки List view, с | Среднее время загрузки Detail view, с | Среднее время сохранения заказа и возврата к списку, с | Среднее время сохранения заказа, с | Использование CPU, % |
---|---|---|---|---|---|---|
2 | 1.74 (87%) | 7.2 | 3.6 | 1.5 | 0.300 | 94 |
5 | 3.4 (68%) | 2.8 | 2.5 | 1.5 | 0.015 | 90 |
10 | 5.6 (56%) | 2.3 | 2.1 | 1.3 | 0.014 | 91 |
14 | 6.6 (47%) | 2.2 | 1.9 | 1.2 | 0.017 | 89 |
Ограничения
По сравнению с реальным использованием приложения, тесты производительности имеют следующие ограничения:
- Все пользователи входят под одними и теми же учетными данными.
- Все пользователи повторяют один и тот же сценарий.
- Загрузка изображений отключена для упрощения HTTP-запросов, используемых в сценарии.
- Таймер уведомлений о задачах отключен для простоты.
Выводы
С учетом ограничений, пиковая производительность приложения соответствует выводам из Nielsen Norman Group paper, где говорится, что время отклика менее 3 секунд обычно приемлемо для удовлетворенности пользователей. На практике, чем меньше времени пользователи ждут для взаимодействия с системой, тем лучше пользовательский опыт. Однако каждый сам решает, какое оптимальное время отклика для него на основе своих требований к производительности.
Результаты показывают, что увеличение heap-памяти с 2 ГБ до 5 ГБ заметно повышает производительность, сокращая время загрузки представлений. Свыше 5 ГБ прирост минимален. Важно учитывать, что такие факторы, как задержка сети и нагрузка на сервер, могут добавить около секунды к времени отклика. Поэтому попытки оптимизировать время загрузки ниже 1,5–1,7 секунды могут не стоить дополнительных инвестиций.
С учетом сказанного, мы не рекомендуем использовать менее 2 ГБ ОЗУ, если вы создаете приложение среднего размера с ожидаемыми 1000 пользователями. Для большинства приложений с похожими конфигурациями 5 ГБ — оптимальный баланс между производительностью и использованием ресурсов.
Заключение
Эти тесты предоставляют четкое представление о том, как приложение масштабируется с различными конфигурациями памяти. Хотя результаты могут варьироваться в зависимости от вашего оборудования и рабочей нагрузки, они подчеркивают оптимальные настройки для плавной работы. Jmix может не быть лучшим выбором для приложений с огромным пользовательским трафиком, как у Amazon или Facebook, но он отлично подходит как быстрое и гибкое решение для внутренних инструментов или приложений меньшего масштаба, таких как кастомные CRM, админ-панели и другие бизнес-инструменты.