Создание 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 соответственно, обещания мгновенного старта оказались не пустыми словами.

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

image-loader.svg

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

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

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

Итоги

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

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

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

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

© Habrahabr.ru