Конфигурация и нагрузочное тестирование приложения с интенсивным использованием базы данных
Содержание:
· Введение
· Как создать приложение Spring-Boot с использованием виртуальных потоков
· Как запустить приложение Spring-Boot Java-19 на основе Gradle
· Описание и настройка нагрузочного теста
· Оценка производительности
· Заключение
Введение
В этой статье мы узнаем, как настроить приложение на основе SpringBoot Gradle для использования функции Виртуальные потоки Java 19. Впоследствии мы проверим преимущества, которые он предоставляет, путем нагрузочного тестирования тривиального приложения, интенсивно использующего базу данных, с виртуальными потоками и без них.
Приложение будет работать в контейнере Docker, организованном с помощью docker-compose. Мы будем использовать K6 для нагрузочного тестирования, Graphana для отображения метрик производительности и Postgres в качестве базы данных.
Как создать приложение Spring-Boot с использованием виртуальных потоков
Здесь https://github.com/GaetanoPiazzolla/spring-boot-virtual-threads мы можем найти тестируемый пример проекта. Следующие абзацы посвящены некоторым ключевым моментам реализации.
Предварительные требования: для сборки Java19 нам нужен Gradle 7.6, как указано в матрице совместимости. Во-первых, нам нужно использовать spring-framework-boot версии 3.0.0 или выше и настроить правильный удаленный репозиторий maven и зависимости в файле build.gradle:
plugins { java id(“org.springframework.boot”) version “3.0.1” } apply(plugin = “io.spring.dependency-management”) repositories { mavenLocal() maven { url = uri(“https://repo.spring.io/milestone") } maven { url = uri(“https://repo.maven.apache.org/maven2/") } } dependencies { implementation(“org.springframework.boot:spring-boot-starter-data-jpa”) implementation(“org.springframework.boot:spring-boot-starter”) //…
Кроме того, здесь мы можем захотеть настроить каждую «работающую» задачу, чтобы предоставить предварительный просмотр функций java 19 Enabled. Для этого достаточно настроить каждую связанную задачу Gradle следующим образом:
tasks { Val preview = “ — enable-preview” withType<org.springframework.boot.gradle.tasks.run.BootRun> { jvmArgs = mutableListOf(preview) } withType<JavaExec> { jvmArgs = mutableListOf(preview) } withType<JavaCompile> { options.encoding = “UTF-8” options.compilerArgs.add(preview) options.compilerArgs.add(“-Xlint:preview”) } }
Еще один ключевой момент — включить виртуальные потоки, сообщая базовой реализации Tomcat, что мы хотим выделить новый виртуальный поток для каждой задачи (см. вызов rest) вместо использования стандартного предопределенного пула потоков платформы:
@Bean public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler -> { protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }; }
Как запустить приложение Spring-Boot Java-19 на основе Gradle
Я столкнулся с некоторыми трудностями при запуске проекта Java 19 с весенней загрузкой, особенно с включенным флагом функции предварительного просмотра. Так что, возможно, я смогу помочь кому-то еще с этой короткой главой.
Единственным предварительным условием является наличие установленной Java 19 в качестве текущей версии Java. Установив эту настройку (убедитесь, что java — версия) указывает на правильное местоположение, разработчик должен иметь возможность запускать приложение spring-boot тремя различными способами:
1- Из встроенного исполнителя Gradle IntelliJ
Просто установите переменную среды THREAD_MODE как «виртуальную» или «стандартную» в качестве переменной среды и установите для spring.profiles.active значение «dev» (в качестве параметра виртуальной машины вам, вероятно, нужно нажать «изменить параметры», а затем добавить его ) в конфигурации приложения среды выполнения:
Также важно, чтобы JVM Gradle мы установили как OpenJDK-19, иначе сработает ряд не очень понятных ошибок:
2- Из интерфейса командной строки Gradle
Поскольку мы указали, что каждая связанная задача Gradle будет выполняться с использованием флага –enable-preview, достаточно запустить приложение, используя:
./gradlew bootRun — args=’ — spring.profiles.active=dev’
Мы можем также настроить application-dev.yaml для переключения между стандартными и виртуальными потоками.
3- Из образа Docker
Файл Dockerfile весенней загрузки Java19 очень похож на старые версии. В данном случае мы используем arm64v8/eclipse-temurin, потому что у меня M1 Mac. Если вам не так повезло, вы можете переключить изображение и использовать стандартную архитектуру затмение-темурин. (Образы OpenJDK устарели!)
# https://docs.docker.com/build/building/multi-stage/#use-multi-stage-builds # First Stage FROM arm64v8/eclipse-temurin:19-jdk-focal as builder WORKDIR /src COPY src ./src/ COPY gradle ./gradle/ COPY build.gradle.kts ./ COPY gradlew ./ COPY settings.gradle.kts ./ RUN chmod 777 ./gradlew RUN ./gradlew clean build # Second Stage FROM arm64v8/eclipse-temurin:19-jre EXPOSE 8080 COPY — from=builder /src/build/libs/spring-boot-app* /app/spring-boot.jar ENTRYPOINT [“java”,”-Duser.timezone=GMT+1",”-jar”,” — enable-preview”, “/app/spring-boot.jar”]
Мы могли бы собрать и запустить образ с помощью команды docker build + docker run, но нам нужно полагаться на docker-compose, чтобы наилучшим образом организовать нашу среду.
Описание и настройка нагрузочного теста
Чтобы запустить каждый контейнер в среде, достаточно выполнить docker-compose up:
Чтобы проверить, все ли работает должным образом, можно обратиться к конечным точкам /thread/name на портах 8080 и 8081. Первый должен ответить «VirtualThread», а второй — «Thread»:
Изначально я установил 1 процессор и 2 ГБ оперативной памяти для весенних приложений. Еще можно изменить распределение, отредактировав файл docker-compose, но я не буду менять этот параметр в следующих сценариях тестирования:
Простая база данных автоматически инициализируется в начале, и это простое представление схемы:
Как и в предыдущих статьях, мы будем измерять производительность трех конечных точек:
- GET /books → получить все большее количество предметов (книг)
- POST /books → создать новую строку в таблице базы данных books
- POST /orders → создать новый заказ, который связывает книгу и пользователя
Нагрузочные тесты будут выполняться с запуском временного контейнера K6, отличаясь от разных исполнений указанием разных параметров ENV: «THREAD», который используется для переключения между виртуальным потоком и стандартным приложением, и «USERS», который представляет собой количество одновременно работающих виртуальных пользователь зацикливается на конечных точках. Для запуска теста достаточно зайти в папку k6-testing и выполнить:
docker-compose run — rm k6 run /k6-scripts/load-test.js -e THREAD=virtual|standard -e USERS=10|20…
Кроме того, мы должны запускать следующий скрипт перед каждым тестом, чтобы сделать каждый нагрузочный тест независимым от предыдущих выполнений.
delete from books b where b.book_id <> 1; delete from orders;
Наконец, контейнер k6 будет записывать результаты в базу данных InfluxDB, которая используется в качестве источника данных для Grafana. Данные будут доступны на панели инструментов, доступной по адресу https://localhost:3000/d/k6/k6-load-testing-results?orgId=1&refresh=5s.
Оценка эффективности
1- Стандартный сценарий
Прирост производительности от внедрения виртуальных потоков в этом конкретном сценарии велик. Помните, что единственное, что меняется между виртуальным и стандартным, это планировщик потоков. Всего с 10 строками кода мы получаем увеличение пропускной способности более чем на 10%:
- Concurrency: 50 / 100 / 500 / 1000 - Resources: - CPU: 1 - RAM: 2 GB - CompletedRounds: - Virtual: 11246 / 11123 / 9445 / 9386 - Standard: 9374 / 9181 / 9243 / 8149
Здесь вы можете увидеть полный снимок экрана кибаны для двух нагрузочных тестов с 1000 пользователей.
Виртуальный:https://raw.githubusercontent.com/GaetanoPiazzolla/spring-boot-virtual-threads-test/master/img/virtual.png
Стандартный: https://raw.githubusercontent.com/GaetanoPiazzolla/spring-boot-virtual-threads-test/master/img/standard.png
2- Сон потока
Если мы добавим Thread.sleep(1000) в один из вызываемых методов, прирост производительности будет не таким впечатляющим, но он все же есть:
- Concurrency: 50 / 100 / 500 / 1000 - Resources: - CPU: 1 - RAM: 2 GB - CompletedRounds: - Virtual: 2755 / 5498 / 9656 / 8135 - Standard: 2751 / 5483 / 8475 / 8004
В этом конкретном случае, если мы увеличим параллелизм до 2 000 одновременных пользователей, в то время как стандартное приложение потока будет продолжать работать (медленно), приложение с включенным виртуальным потоком вылетит с ужасным исключением: java.lang.OutOfMemoryError — говорит нам о том, что виртуальную машину может потребоваться принудительно завершить.
Хотя создание виртуального потока, возможно, дешевле по сравнению со стандартными потоками, с действительно высоким уровнем параллелизма пул является лучшим выбором. Анализ производительности объединения виртуальных потоков может быть проведен в следующей статье.
3- Задержка сети
Что произойдет, если у нас медленное сетевое подключение к базе данных? Чтобы смоделировать это, мы можем добавить простую задержку через утилиту TC (достаточно 100 мс):
docker exec db-service apt-get update docker exec apt-get install iproute2 iputils-ping -y docker exec db-service tc qdisc add dev eth0 root netem delay 100ms
В этом случае использование виртуальных потоков даже контрпродуктивно:
- Concurrency: 50 / 100 / 500 / 1000 - Resources: - CPU: 1 - RAM: 2 GB - CompletedRounds: - Virtual: 299 / 355 / 629 / 610 - Standard: 716 / 746 / 975 / 1120
Этот результат действительно против ожиданий. Кажется, что есть дросселирование одновременных запросов. Вероятно, потому, что некоторые базовые реализации веб-сокета или уровня базы данных выполняют некую «синхронную» работу — точнее, синхронизированные блоки кода, что является одним из самых больших ограничений этой функции.
Команда Spring проделала потрясающую работу, смягчив огромное ограничение Platform-Thread-Pinning, вызванное ключевым словом synchronized, используемым в коде:
Они заявляют, что: За несколько лет до того, как виртуальные потоки стали доступны, команда Spring пересмотрела синхронизированные блоки, которые потенциально могли взаимодействовать со сторонними ресурсами, устранив конфликты блокировок в высококонкурентных приложениях. и что с самыми последними версиями Spring Framework, Spring Boot и Apache Tomcat вы можете начать экспериментировать самостоятельно.
Хотя это абсолютно верно, маловероятно, что какое-либо корпоративное приложение будет полагаться только на эти компоненты. И любое «синхронизированное» присутствие там может повлиять на наше приложение, как в этом случае. Виртуальные потоки влияют на драйверы баз данных, системы обмена сообщениями, HTTP-клиенты и многие библиотеки. Видимо, время еще не пришло.
Заключение
В этой статье мы попробовали виртуальные потоки в приложении Spring-Boot.
Хотя это все еще предварительная функция Java 19, я считаю, что ее потенциал еще предстоит раскрыть. Безусловно, есть улучшения, и мы будем готовы взять их в свои руки.
А пока вы можете скачать этот репозиторий и повеселиться с нагрузочным тестированием! Существует множество настроек, которые можно протестировать, начиная с установки большего количества ЦП или ОЗУ и заканчивая еще большим увеличением параллелизма, изменением некоторых конечных точек или добавлением метода @Async в цикл вызовов.
Как всегда, большое спасибо за то, что уделили немного времени чтению этой статьи. Я искренне надеюсь, что это кому-нибудь пригодится!