Создание Native Images со Spring Native и GraalVM

от автора

Привет! Меня зовут Антон Богомазов, я backend-разработчик в продуктовой команде Домклик. Наш проект представляет собой более десяти Kotlin/Spring-микросервисов, развернутых в Kubernetes, и постоянно растет, поэтому мы неизбежно сталкиваемся с растущим потреблением ресурсов кластера. Это обстоятельство и подтолкнуло меня к поиску технологий, позволяющих оптимизировать расходы на содержание наших сервисов.

В этой статье я хочу исследовать возможности технологии Java Native Image, поделиться опытом взаимодействия с ней и со средствами Spring для генерации нативных образов.

Native image — технология, позволяющая скомпилировать Java-код в исполняемый файл. Для поддержки этой функциональности существует Spring Native, использующий GraalVM для генерации образов. Главное преимущество такого подхода в том, что можно мгновенно запустить приложение без старта JVM, тратить меньше памяти и иметь меньший размер файла. Еще одним плюсом является отсутствие прогрева приложения, так как компиляция выполнялась до запуска.

Но есть и недостатки: создание native image требует значительно больше времени, чем сборка Java-приложения; отсутствие runtime-оптимизаций снижает пиковую производительность. Также не всякое приложение может быть представлено в виде native image, а использование некоторых фич потребует дополнительной конфигурации. С полным списком ограничений можно ознакомиться здесь.

Чтобы опробовать возможности технологии в деле, создадим простое приложение: контроллер с методом, возвращающим случайное число в ответ на GET-запрос.

Прежде всего установим GraalVM, который требуется Spring Native; нас интересуют его возможности AOT-компиляции. Скачайте по ссылке https://github.com/graalvm/graalvm-ce-builds/releases, а содержимое положите в JavaVirtualMachines.

Теперь можно приступить к созданию приложения; я воспользуюсь Spring Initializr. Добавим зависимости Spring Native [Experimental] и Spring Reactive Web. Последняя не является обязательной, но сделает код проще за счет Reactive Kotlin DSL:

plugins { id("org.springframework.boot") version "2.6.1" id("io.spring.dependency-management") version "1.0.11.RELEASE" kotlin("jvm") version "1.6.0" kotlin("plugin.spring") version "1.6.0" id("org.springframework.experimental.aot") version "0.11.0-RC1" }  dependencies { implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("io.projectreactor.kotlin:reactor-kotlin-extensions") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") }

Создадим класс контроллера с одним методом:

@Configuration class Controller {      @Bean     fun getRandomNumber() = router { GET("/getRandomNumber") { ServerResponse.ok() .body(Mono.just(Random.nextInt()), Int::class.java) }     }  }

Приложение готово!

Подготовительный этап закончен и можно приступить к сравнению. С помощью nativeCompile в Gradle я создал native image и JAR одного и того же приложения и сравнил различные их показатели:

  • Разница во времени создания колоссальная, компиляция нативного образа требует большого количества системных ресурсов, что отражается на длительности создания. У меня ушло около 4 минут даже на такое простое приложение.

  • С native image потребление RAM удалось сократить на 7 %.

  • Сравнить размеры файлов оказалось довольно сложно. JAR для своего запуска требует наличие JRE в окружении, а native image уже содержит все необходимые компоненты, поэтому я прибавил к размеру JAR 46 мб — размер среднего JRE. Поэтому размер образа оказался также на 7 % меньше.

  • Длительность запуска составила 5,84 и 0,72 секунды для JAR и native image соответственно, обещания мгновенного старта оказались не пустыми словами.

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

Попробуем ответить на главный вопрос: где это можно применить? Я вижу несколько сфер:

  • Прежде всего, веб-приложения и FaaS. Очень быстрый старт позволяет поднимать и гасить реплики значительно быстрее, чем если бы это были обычные Java-приложения. С другой стороны, это сработает не так хорошо с приложениями, зависимыми от инфраструктуры: подключение к БД и брокерам сообщений отнимает ценное время.

  • Десктопные приложения. Компиляция исполняемого файла позволяет запускать на машине без JVM, что снижает требования к среде выполнения и расширяет области применения Java.

Итоги

Уменьшенный размер образа и, как следствие, пода позволит оптимизировать ресурсы K8s-кластера, потенциально позволяет держать большую нагрузку за счет большего количества реплик. Для еще более радикального уменьшения размера можно сочетать native image с distroless.

Но чаще дефицитным ресурсом является RAM. Остается без ответа вопрос, будет ли полученная оптимизация масштабирована соответственно размеру приложения, и можно ли её увеличить тонкими настройками при создании образа? Это требует отдельного исследования.

Кроме того, функциональность Spring Native является экспериментальной, что может стать аргументом против её использования в вашем проекте.

Для себя я решил, что проект недостаточно зрелый, но, безусловно, интересный, буду наблюдать за его развитием.


ссылка на оригинал статьи https://habr.com/ru/company/domclick/blog/598753/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *