В свете недавнего выхода Spring Framework 6.1 и Spring Boot 3.2, мы хотели бы поделиться обзором тех усилий, которые предпринимает команда Spring, чтобы позволить разработчикам оптимизировать эффективность их приложений во время выполнения.

В этой статье мы рассмотрим:

  • Spring MVC вместе с Virtual Threads на JDK 21

  • Развёртывание Spring-приложений в облаке с использованием GraalVM Native Image

  • Восстановление контрольной точки

  • Масштабирование приложений до нуля с помощью Spring и Project CRaC

  • Будущее OpenJDK со Spring AOT и Project Leyden

Контекст

Начнем с самого важного вопроса: почему нам стоит заботиться об улучшении эффективности работы приложений, развёрнутых в облаке? Первая причина – оптимизация затрат. Все хотят платить меньше за хостинг. Однако выбор более дешевого хостинга обычно означает использование меньшего количества процессорного времени, памяти и ресурсов, что делает наши приложения “тяжёлыми на подъём”. Мы также живем в мире, где запуск приложений, скорее всего, будет связан с Kubernetes и контейнерами, а их использование требует с нашей стороны особого внимания к времени запуска JVM, времени прогрева и управлению памятью.

Цель команды Spring – предоставить различные варианты (некоторые из которых можно комбинировать) для оптимизации нагрузки и масштабируемости миллионов Spring-приложений. Наша цель – свести к минимуму количество изменений, которые необходимо сделать в вашем Spring-приложении, чтобы воспользоваться этими улучшениями. Но достижение этих целей, к сожалению, обычно связано с компромиссами, про которые мы постараемся рассказать максимально подробно. Надеемся, что эта статья даст вам достаточно информации, чтобы получить более четкое представление о том, как затронутые темы относятся к Вашему приложению, а также даст понять на какие компромиссы придётся пойти в том или ином случае.

Общее требование для получения преимуществ от улучшения эффективности во время выполнения — это обновиться до Spring Boot 3, который базируется на Spring Framework 6, который, в свою очередь, использует Java 17 в качестве базовой версии и требует перехода с Java EE (пакет javax) на Jakarta EE (пакет jakarta). После обновления вам становится доступен набор новых функций, улучшающих эффективность работы приложений во время выполнения.

Spring MVC вместе с Virtual Threads на JDK 21

Начнем с недавно выпущенной технологии, доступной начиная с Java 21. Технология Virtual Threads предназначена для уменьшения стоимости серверных приложений, написанных в простом и популярном стиле “поток-на-запрос”, чтобы масштабироваться с почти оптимальным использованием оборудования.

Virtual Threads делают блокирующие I/O операции не такими дорогими и идеально подходят для приложений на Spring Web MVC с использованием servlet'ов. Spring MVC может в полной мере использовать это нововведение, например, на Tomcat или Jetty, настроенных на использовании Virtual Threads. Для этого в большинстве случаев даже не потребуется изменять существующий код. Кроме того, данный способ естественно адаптируется к оптимальной производительности, не требуя тонкой настройки конфигурации пула потоков.

Мы также получили обратную связь от Spring-сообщества, в которой нас просили не ставить разработчиков перед сложным выбором между RestTemplate и реактивным WebClient. Поэтому мы приняли решение внедрить “дружественный к Virtual Threads современный HTTP-клиент” под названием RestClient (который, конечно, также является привлекательной опцией и без Virtual Threads) в Spring Framework 6.1. Spring Cloud Gateway и относящаяся к нему инфраструктура могут в той же степени получить преимущества от использования Virtual Threads, как и Spring MVC.

Итак, что это означает для WebFlux и реактивного стека?

Мы намеренно приняли решение иметь в своём арсенале различные решения как для блокирующего, так и для реактивного стека, чтобы получить максимальное преимущество от реактивного сервера WebFlux и при этом сохранить Spring Web MVC стек (наиболее часто используемый стек со стандартной архитектурой блокирующих потоков на start.spring.io) настолько компактным, насколько возможно. Virtual Threads отлично подходят для улучшения масштабируемости традиционных веб-приложений, использующих Spring MVC на базе контейнера servlet'ов. С другой стороны, сервер WebFlux предоставляет оптимальный реактивный стек, будучи безупречно совместимым с I/O настройками Netty, предоставляя аналогичные преимущества во время выполнения и используя другую программную модель.

Когда вам необходимо параллельное выполнение потоков на уровне приложения, (как, например, при отправке многочисленных удаленных HTTP-запросов, потенциально в потоковом режиме с последующим комбинированием результатов), подход Structured Concurrency в Project Loom может предоставить вам интересный низкоуровневый блок для построения приложения в будущем, но это не тот тип API, который может понадобиться разработчику типичного Spring-приложения (к тому же, этот проект все еще находится в стадии preview). Для таких случаев WebFlux и реактивные API типа Reactor предоставляют беспрецедентное на данный момент преимущество наряду с Kotlin Coroutines и их подходом Flow, который предоставляет нам интересную комбинацию императивного и декларативного подходов к разработке. RSocket является еще одним примером серьезного преимущества модели реактивного взаимодействия.

Заметим, что вы не обязаны выбрать что-то одно, поскольку Spring MVC также предоставляет опциональную поддержку реактивной модели. Таким образом, если вам необходимо параллельное выполнение потоков лишь в некоторых случаях, вы можете просто использовать Spring MVC стек с настроенными Virtual Threads и бесшовно включить, к примеру, взаимодействие с реактивным WebClient в свои веб-контроллеры, а Spring MVC будет адаптировать реактивные возвращаемые значения к асинхронным Servlet-ответам. Эта поддержка реактивности в Spring MVC полностью опциональна, а Reactor и Reactive Streams нужны только тогда, когда действительно используются реактивные эндпоинты, а также когда HTTP-стек базируется на Servlet-контейнере, таком как Tomcat или Jetty (но не на Netty).

Мы ожидаем, что именно Virtual Threads станут наиболее популярным выбором для тех, кто использует Spring MVC и Java 21+ с достаточно типовыми веб сценариями. В целом же экосистема Java все еще должна более полно адаптироваться к Virtual Threads, чтобы избежать привязки к процессору (например, в распространенных реализациях JDBC драйверов), однако ожидается, что даже эта проблема скоро будет решена. Чтобы оценить работу Virtual Threads убедитесь в том, что вы используете Spring Boot версии 3.2 или выше, установите свойство spring.threads.virtual.enabled в true, а также используйте последние доступные версии библиотек и драйверов.

Оптимизированное развертывание контейнеров со Spring и GraalVM Native Image

Мы продолжаем улучшать встроенную поддержку GraalVM, впервые появившуюся в Spring Boot 3. Основной случай использования — это построение оптимизированного образа контейнера при помощи Buildpacks, который содержит минимальный базовый слой операционной системы и ваше приложение, скомпилированное в платформенно-ориентированный исполняемый файл благодаря Spring AOT трансформациям и компилятору платформенно-ориентированных образов GraalVM. Наличие дистрибутива JVM не требуется.

Такой подход позволяет разворачивать контейнеры с минимальным размером, которые стартуют с пиковыми показателями производительности за несколько десятков миллисекунд (обычно в 50 раз быстрее по времени запуска, чем стандартная JVM), а также с меньшим потреблением памяти.

GraalVM следует по пятам за внедрением новых функций в Java и, к примеру, предоставляет первоклассную поддержку Virtual Threads: см. недавний пост в блоге Джоша Лонга, All together now.

Отличные характеристики GraalVM во время выполнения стали возможными благодаря принятию некоторых компромиссов. Компиляция платформенно-ориентированного образа занимает минуты, а не секунды. Чтобы правильно управлять рефлексиями, прокси и другим динамическим поведением JVM, требуются дополнительные метаданные. Spring включает в себя значительную часть этих метаданных, но любой реальный проект наверняка потребует дополнительных сведений, чтобы работать правильно (например, о зависимостях, специфических для вашей организации). Наконец, комбинация трансформаций Spring AOT и платформенно-ориентированного образа GraalVM требует от нас заморозки параметра classpath и состояний Spring Boot бина во время сборки. У вас будет возможность поменять URL или пароль к базе данных в рантайме через конфиги, но не получится сменить тип базы данных или сделать что-то, что поменяет структуру Spring-бинов.

Исторически, другим недостатком была ограниченная пиковая производительность по причине отсутствия своевременной компиляции, но появление Oracle GraalVM под лицензией GraalVM Free Terms and Conditions (см. налагаемые ограничения) ставит это под сомнение. Вы можете подписаться на Buildpacks RFC #294 чтобы отслеживать потенциальную будущую поддержку данного продукта, а также попробовать его уже сейчас с вашими рабочими Spring Boot приложениями, используя этот простой Dockerfile в качестве стартовой точки.

Имея в своем арсенале мгновенный старт и немедленный доступ к пиковой производительности, платформенно-ориентированные приложения на Spring Boot могут масштабироваться до нуля (scale-to-zero). Давайте разберемся, что это значит.

Масштабирование до нуля

Масштабирование до нуля — это своего рода обобщение понятия бессерверности. Рабочие нагрузки могут запускаться только на бессерверных облачных платформах, но также и на любом Kubernetes кластере или облачной платформе, предоставляющей функциональность масштабирования до нуля при отсутствии запроса, подлежащего обработке. Благодаря Kubernetes, вы можете использовать такие решения как Knative или KEDA для масштабирования до нуля. При этом вы можете масштабироваться до нуля с любым типом приложения, включая традиционное веб-приложение. Самая важная характеристика бессерверной архитектуры — это не техническая реализация, а модель оплаты по мере использования, которую она позволяет реализовать.

Существуют различные случаи использования, при которых масштабирование до нуля может быть интересным. JVM великолепна, когда необходимо разрабатывать веб приложения с высоким трафиком, но давайте будем честны, нам также приходится разрабатывать множество маленьких приложений для решения офисных задач, которые, как правило, используются не 24/7. Почему мы должны платить за то время, когда их никто не использует? Существуют также промежуточные окружения, которые обычно используются лишь в течение очень небольшого периода времени, а также микросервисы, для которых кэширование обеспечивает возможность отключать часть из них на продолжительный период. И давайте также не забывать о высокой доступности, которая заставляет нас держать включёнными по два экземпляра каждого сервиса на случай внештатной ситуации, поскольку стартовое время наших приложений слишком большое, чтобы быстро восстановиться после сбоя.

Но как выполнить масштабирование до нуля для проектов, которые не могут принять компромиссы, необходимые для платформенно-ориентированного образа GraalVM?

Восстановление контрольной точки JVM: как масштабировать до нуля с помощью Spring и Project CRaC

CRaC — это OpenJDK проект, который определяет новый Java API, позволяющий вам сохранить контрольную точку и восстановить приложение на HotSpot JVM, разработанной командой Azul Systems, a также поддерживаемой командами AWS Lambda и IBM OpenLiberty. Он базируется на CRIU, проекте, который реализует сохранение и восстановление контрольных точек в Linux.

Принцип следующий: вы запускаете свое приложение почти как обычно, но на версии JDK, где включена функциональность CRaC. Затем в какой-то момент, потенциально после того, как под рабочей нагрузкой JVM “прогреется”, вы инициируете сохранение контрольной точки через вызов API, выполнение jcmd команды, обращение к HTTP-эндпоинту и т.д.

После этого образ работающей, прогретой JVM в оперативной памяти сериализуется на диск, позволяя очень быстро восстановить ее впоследствии, потенциально на другой машине с похожей операционной системой и архитектурой CPU. Восстановленный процесс сохраняет все возможности HotSpot JVM, включая дальнейшую оптимизацию JIT во время выполнения.

Стоит отметить, что сохранение и восстановление контрольной точки очень хорошо сочетаются с жизненным циклом контекста приложения на Spring в фазах остановки и запуска. Поддержка CRaC в Spring Framework 6.1 в основном сводится к маппингу жизненных циклов CRaC и Spring друг на друга, остальная часть поддержки не привязана к CRaC и по большей части относится к улучшению жизненного цикла Spring, нацеленному на более корректное закрытие и открытие сокетов, файлов и пулов потоков. Здесь цель состоит в том, чтобы помимо обычных запуска и остановки поддерживать также многочисленные циклы остановки и запуска.

Как и GraalVM, Project CRaC позволяет приложению масштабироваться до нуля с моментальным запуском в несколько десятков миллисекунд даже на малых серверах. А это в 50 раз быстрее, чем обычный “холодный” запуск JVM, (как и в случае с платформенно-ориентированным образом GraalVM). Но давайте посмотрим на связанные с этим решением компромиссы.

Первый компромисс заключается в том, что CRaC требует предварительного запуска вашего приложения перед его выводом в эксплуатацию. Должны ли вы запускать его на вашей CI/CD платформе? С продакшен-сервисами или без них? Этот компромисс вызывает множество нетривиальных вопросов.

Второй компромисс связан с необходимостью закрывать и правильно воссоздавать любые ресурсы, связанные с сокетами, файлами и пулами потоков, на основе жизненного цикла CRaC. Spring Boot заботится об этом за вас в пределах поддерживаемого набора задач. Но некоторые библиотеки это пока не поддерживают, поэтому может пройти некоторое время до того момента, когда появится поддержка всех используемых Вами технологий.

Третий компромисс, по нашему мнению, самый неприятный. Может возникнуть соблазн создать автономный образ, готовый к восстановлению. Но любая чувствительная информация, загруженная в память во время старта контрольной точки, будет сериализована в файлы снимков, что может привести к её утечке. Например, может утечь пароль от базы данных на продакшене.

Потенциальным решением этой проблемы может стать выполнение запуска контрольной точки без конфигурации промышленного окружения с последующим обновлением конфигурации приложения во время восстановления. Это можно сделать, с использованием Spring Cloud Context и аннотации @RefreshScope. Команда Spring, возможно, изучит эту тему в будущем, чтобы понять, имеет ли смысл добавить больше встроенной поддержки. Вы также можете использовать стратегию создания и хранения файлов образа как часть зашифрованного содержимого напрямую в вашей платформе Kubernetes, даже если это потребует более глубокой интеграции платформы.

Последняя ключевая характеристика состоит в том, что CRaC работает только на Linux и требует тонкой настройки функциональности Linux, для работы без привилегированного режима.

Не забывайте и о том, что мы находимся в самом начале истории проекта CRaC и что Spring Boot 3.2 — это самая первая версия, которая его поддерживает. Некоторые из этих ограничений могут быть сняты по мере того, как технология восстановления контрольной точки эволюционирует вместе с ее поддержкой со стороны Spring. Обратитесь также к документации по Spring Framework и https://github.com/sdeleuze/spring-boot-crac-demo, если хотите опробовать эту технологию самостоятельно.

Беглый взгляд на будущее OpenJDK со Spring AOT и Project Leyden

Мы увидели два способа масштабировать до нуля наши приложения, базирующиеся на Spring, используя GraalVM или CRaC. Каждый из них заставляет нас пойти на некоторые компромиссы. Что если существует другой способ улучшить характеристики времени выполнения Spring Boot приложений с меньшим количеством ограничений?

Возможно, вы слышали о Project Leyden, новом проекте OpenJDK, который нацелен на улучшение времени запуска, времени достижения пиковой производительности и уменьшения занимаемого места Java программами.

Мы рекомендуем посмотреть этот доклад от Брайана Гетца, если вы хотите узнать больше.

Project Leyden недавно ввел понятие “premain” оптимизаций (по сути Class Data Sharing + AOT на стероидах) и, что интересно, команда Java Platform обнаружила значительную синергию с Spring Ahead-Of-Time оптимизациями, которые изначально были созданы для обеспечения поддержки платформенно-ориентированных образов GraalVM, но уже способны обеспечивать на 15% более быстрый запуск и на JVM.

В то время как “premain” оптимизации в большой степени экспериментальны (в настоящее время это экспериментальная ветка в репозитории Leyden на GitHub), команда Spring недавно смогла зафиксировать более быстрый запуск демонстрационного проекта Spring Petclinic (от 2-х до 4-х раз), скомбинировав Spring AOT на JVM и оптимизации из Project Leyden, а также более быстрый прогрев, и практически без каких-либо ограничений.

В своей текущей форме, в отличие от GraalVM и CRaC, эти оптимизации не позволяют масштабироваться до нуля, поскольку не дают возможности приложениям запускаться за десятки миллисекунд. Однако, если мы получаем значительные улучшения времени запуска на JVM и времени разогрева без практически каких-либо ограничений, это решение имеет потенциал для повсеместного использования и может комбинироваться с другими находящимися в разработке возможностями Leyden, которые можно будет выбрать по своему усмотрению. Мы рады сообщить, что начали сотрудничество между Java Platform Group и командой Spring, чтобы увидеть, насколько далеко мы сможем отодвинуть границы возможного, используя подход premain из Project Leyden. В сочетании с улучшениями Spring AOT, специально предназначенными для JVM, мы ожидаем дальнейших оптимизаций, применимых к широкому диапазону приложений на Spring. В ближайшие месяцы мы опубликуем больше информации.

Ознакомьтесь с репозиторием, если хотите попробовать сами.

Заключение

Обратная связь от Spring-сообщества во всем мире стала ключевым источником вдохновения для команды Spring наряду с прагматичным сотрудничеством с такими компаниями как Oracle, Bellsoft, Azul и многими другими.

Мы много работаем над поддержкой новых возможностей, одновременно минимизируя воздействие на разработку приложений на Spring, предоставляя четкие способы обновления на новые версии для множества видов приложений. Это самый большой вызов, но и самый приятный аспект наших стратегических усилий в области инфраструктуры.

И последнее. Мы ждем обратной связи о том, что заинтересовало вас больше всего в плане использования в вашей организации и для ваших проектов. Считаете ли вы, что масштабирование до нуля и модель pay-as-you-use стоят тех ограничений, которые требуются при использовании GraalVM или CRaC? Является ли уменьшение использования памяти, обеспечиваемое платформенно-ориентированным образом GraalVM ключевым преимуществом для вас? Считаете ли вы, что Spring AOT на JVM в сочетании с Project Leyden имеет высокий потенциал? Какова ваша точка зрения на Virtual Threads? Пожалуйста расскажите нам об этом!

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех, присоединяйтесь

Комментарии (0)