
Введение
Основной тренд в мире IT сейчас — экономия ресурсов и легковесные решения. В облаке мы платим только за те ресурсы, которые действительно используем. И чем эффективнее мы их используем, тем меньше денег тратится впустую: уменьшая издержки, увеличиваем маржинальность.
Java долгое время была и остается фаворитом в enterprise решениях, но все чаще в высоконагруженных проектах предпочтение отдается более «производительным» языкам, таким как Go, а порой даже C++. Но вдруг Java тоже может быть быстрой?
Вечная весна
Стандартным и наиболее часто используемым фреймворком в Java-community является Spring . Когда-то Spring был глотком свежего воздуха по сравнению с монструозными enterprise-серверами. Spring предоставляет огромное количество библиотек и интеграций, объединенных в единую экосистему.
Однако, несмотря на удобство, у Spring есть ряд недостатков:
-
Скорость погружения. Разработчику недостаточно просто знать Java, чтобы понимать и писать код на Spring. Необходимо учить особенности фреймворка, его абстракции и правила работы с ними. Это увеличивает время вхождения для новых разработчиков, не знакомых с фреймворком.
-
Изоляция разработчиков от низкоуровнего API. При работе с БД, очередями сообщений или другими системами Spring предоставляет свои готовые абстракции. Это позволяет разработчикам не углубляться в нюансы работы конкретных API. Часто разработчики Spring не умеют работать с БД или очередями в отрыве от Spring.
-
Сложность отладки. Абстракций удобны во время разработки, но в случае возникновения проблем в рантайме от разработчика требуется глубокое понимание работы фреймворка. Порой даже опытные разработчики тратят много времени, пытаясь понять, что пошло не так.
-
Производительность. За удобство мы платим производительностью. Приложения на Spring довольно тяжеловесные — они долго запускаются и потребляют много ресурсов. Изоляция разработчиков от API также плохо влияет на производительность, т.к. не позволяет использовать API на полную мощность.
В остальном Spring был, есть и долгое время будет оставаться стандартным решением для большинства проектов. Благодаря мощной экосистеме и огромному community выбрать Spring как основу для своего проекта вряд ли будет ошибкой. Но если хочется чего-то модного, молодёжного и быстрого…
Какие альтернативы?
Хоть Spring и лидирует по популярности в Java-community, существует ряд альтернативных решений. На мой взгляд, наиболее известные и перспективные:
-
Microprofile и различные его имплементации — легковеснее, чем Spring, но в основе
Java EE. По сути та же «магия», но поменьше. -
Ktor — микрофрейморк для Kotlin. Ничего лишнего, хорошая производительность, правда подходит только командам, которые любят и исповедуют Kotlin.
Но в этой статье я бы хотел рассказать о молодом и не столь известном решении — Helidon SE и Helidon Níma.
Полет ласточки
Helidon (Ласточка) — это молодой Java-фреймворк с прицелом на максимальную легковесность и работу в облаке. У него есть 2 версии:
-
MP — реализация того самого Microprofile. Надстройка поверх SE версии;
-
SE — cloud-native микрофреймворк.
Именно SE интересует нас в рамках данной статьи.
Философия Helidon SE в корне противоположна Spring и Java EE:
-
Прозрачность и полный контроль разработчика над фреймворком;
-
«No magic» подход без Inversion of Control и автоконфигураций.
Вот так выглядит создание простого приложения с веб-сервером:
public final class Main { public static void main(final String[] args) { LogConfig.configureRuntime(); //Конфигурация автоматически подтягивается из ENV и папки resources. //Поддерживаются YML, properties, json и других форматы. Config config = Config.create(); //Веб-сервер - просто Java-объект. Мы имеем над ним полный контроль. WebServer server = WebServer.builder() //Конфигурация сервера - хост, порт и т.д. .config(config.get("server")) //Правила маршрутизации запросов .routing(routing -> { routing.get("/greeting", (req, res) -> res.send("Hello World!")); routing.post("/greeting/{name}", (req, res) -> { String name = req.path().pathParameters().get("name"); res.send("Hello %s!".formatted(name)); }); }) .build() .start(); } }
Helidon предоставляет разработчику набор библиотек, которые покрывают большую часть потребностей современных микросервисных приложений. «Из коробки» доступны health-checks, метрики, трассировка. Также есть поддержка gRPC, WebSocket, OpenAPI, MQ.
Helidon Níma — виртуальные потоки вполне реальны
Helidon Níma — первый веб-фреймворк, полностью построенный вокруг Project Loom и виртуальных потоков. Но чтобы разобраться, какие преимущества это даёт, придется немного погрузиться в теорию.
Существуют 2 модели веб-серверов:
-
Блокирующие — на каждый HTTP запрос создается поток, который блокируется при любом блокирующем вводе-выводе (любое сетевое взаимодействие). Код пишется в стандартной, императивной парадигме. Среди блокирующих серверов наиболее часто используются embedded-сервера Tomcat, Jetty.
-
Неблокирующие — имеется небольшой общий пул потоков, который обрабатывает входящие запросы. Потоки обрабатывают множество запросов одновременно, получая и отправляя события. При этом любое сетевое взаимодействие должно использовать неблокирующий ввод-вывод. Для этого приходится использовать специальные библиотеки (Netty Client вместо HttpClient, R2DBC вместо JDBC). Код пишется в реактивной парадигме с использованием ProjectReactor. Самый популярный неблокирующий Java веб-сервер — Netty.
Неблокирующий подход, как правило, позволяет обслуживать большее число одновременных запросов, но при этом требует кардинального изменения подходов к разработке. Реактивная модель более сложная в разработке, отладке и тестировании.
Helidon SE версии 3 строится на базе неблокирующего веб-сервера Netty и использует реактивную модель.
Но 19 сентября 2023 вышла Java 21 и принесла Project Loom — виртуальные потоки для Java. Если кратко описать, чем так хороши виртуальные потоки — они позволяют писать неблокирующий код в императивной парадигме. Мы можем взять лучшее от двух миров: эффективность реактивного подхода и простоту императивного.
Команда Helidon решила быть «на острие технологий» и еще до выхода Project Loom в релиз начала делать веб-сервер, который бы базировался полностью на виртуальных потоках. Имя этой технологии — Helidon Níma и на момент написания статьи актуальная версия 4.0.0-M2 — release candidate, который пока не готов для production, но отлично подходит для экспериментов. Níma, работающий на виртуальных потоках, позволяет добиться производительности реактивного веб-сервера (и даже больше), используя простую и понятную императивную модель.
Helidon 4 будет использовать Níma веб-сервер как в SE, так и в MP версии.
Болтовня про производительность — это здорово, но давайте посмотрим как это будет работать в действии. Я хочу показать результаты тестирования Helidon Níma 4.0.0-M2 в сравнении с Spring WebFlux и Spring WebMVC.
Результаты тестирования
Подготовка к тестированию
Для проведения тестирования я взял простой пример: сервис должен забрать 100 строк из таблицы в БД Postgres, перевести их в Json и вернуть клиенту. Запрос к БД — самый частый тип блокирующего вызова, а нагрузку на веб-приложения обычно создают множественные запросы на чтение.
Для бенчмарка я создал 3 проекта:
-
Helidon Nima, JDBI
-
Spring WebFlux, Spring Data R2DBC
-
Spring WebMVC, Spring Data JDBC / JDBI (Spring 3.1.4)
Код доступен в репозитории.
Версии библиотек:
-
Spring 3.1.4
-
JDBI 3.41.1
-
Helidon 4.0.0-M2
Приложения запускались через docker compose на моей локальной машине с ограничением на ресурсы.
Использовалось 2 конфигурации:
-
1 CPU + 1Gb memory
-
2 CPU + 2Gb memory
В качестве базы был выбран образ container-registry.oracle.com/java/openjdk:21, JVM запускалась с
настройками -XX:InitialRAMPercentage=90.0 -XX:MaxRAMPercentage=90.0. GC по умолчанию — G1GC.
В каждом приложении использовался пул потоков (Hikari для Helidon/Spring WebMVC и R2DBC POOL для WebFlux) с фиксированным размером пула в 100 подключений.
Перед тестированием на сервисы подавалось 100к запросов для разогрева JVM. Нагрузка подавалась через AutoCannon с 3 сценариями:
-
100 потоков-пользователей — по размеру пула подключений к БД;
-
200 потоков-пользователей — в 2 раза превышающий пул;
-
1000 потоков-пользователей — пиковая аномальная нагрузка.
Число одновременных запросов равно числу потоков, а число запросов в секунду ограничено только производительностью сервиса.
Полученные данные я занес в таблицу и визуализировал ее в виде диаграмм:



По результатам тестирования Níma вышла абсолютным победителем как по RPS, так и по времени ответа.
Чтобы сделать тестирование более объективным, в одном из тестов я заменил persistence слой в MVC версии с Data Jdbc на более легковесную библиотеку JDBI (ту же, что использовал в Níma). Это дало прирост производительности, но не спасло Web MVC от разгрома.
Вместо заключения
Поговорим о деньгах.
Níma смог обрабатывать в 4.5 раз больше запросов в секунду, чем Spring WebFlux, используя те же ресурсы. Понятно, что эти результаты приблизительные и могут сильно отклоняться в реальных задачах в обе стороны, но я позволю себе достать калькулятор и пофантазировать.
Представим высоконагруженную систему из 10 микросервисов, расположенную в трёх зонах доступности. В каждой зоне находится минимум один МС. Каждый МС работает на машине с 1CPU и 1GB памяти. Тогда, чтобы решение на Spring могло обрабатывать количество запросов, сравнимое с решением на Helidon, нам понадобится в 4-5 раз больше экземпляров приложений.
Взяв за основу цены на виртуальные машины в Yandex Cloud и набросав небольшую табличку, мы можем прикинуть, что решение на Spring будет обходиться дороже на 1.5 млн рублей в год.
|
Цена |
Час |
Год |
Год 10 МС |
Год 10 МС x 3 экземпляра |
Год 10 МС x 15 экземпляров |
Разница |
|---|---|---|---|---|---|---|
|
1 CPU |
1,12 RUB |
9 811,20 RUB |
98 112,00 RUB |
294 336,00 RUB |
1 471 680,00 RUB |
1 177 344,00 RUB |
|
1 Гб ОЗУ |
0,39 RUB |
3 416,40 RUB |
34 164,00 RUB |
102 492,00 RUB |
512 460,00 RUB |
409 968,00 RUB |
|
Суммарно |
1,51 RUB |
13 227,60 RUB |
132 276,00 RUB |
396 828,00 RUB |
1 984 140,00 RUB |
1 587 312,00 RUB |
Я надеюсь, что мне удалось немного развеять миф о том, что Java — это тяжело и медленно. Java также бывает быстро и легко, если её правильно готовить.
Дополнительные материалы
-
Представление Project Loom в Java — как работает Project Loom.
-
Spring MVC vs Spring WebFlux. Что лучше? Объясняем на пингвинах — о блокирующей и неблокирующей моделях.
-
Что такого в IT, что с ним все носятся. И когда всё это закончится — про развитие IT и тенденцию на более легковесные решения.
ссылка на оригинал статьи https://habr.com/ru/articles/765050/
Добавить комментарий