Релиз Spring Native Beta

Недавно команда, занимающаяся портированием Spring для GraalVM, выпустила первый крупный релиз - Spring Native Beta. Вместе с создателями GraalVM они смогли пофиксить множество багов как в самом компиляторе так и спринге. Теперь у проекта появилась официальная поддержка, свой цикл релизов и его можно щупать.


Самым главным препятствием при переносе кода из JVM в бинарники является проблема использования фишек, присущих только java - рефлексия, работа с classpath, динамическая загрузка классов и т.д. 

Согласно документации, ключевые различия между обычным JVM и нативной реализацией заключаются в следующем:

В самом спринге рефлексия, создание прокси и ленивая инициализация встречается практически везде, поэтому все конфигурации надо было аккуратно обработать, из-за этого к релизу шли больше года.

В ходе исследований был создан новый компонент Spring AOT, который отвечает за все необходимые преобразования вашего кода в удобоваримый для Graal VM формат.

Spring AOT анализирует код и на основе него создает файлы конфигурации такие как native-image.properties, reflection-config.json, proxy-config.json или resource-config.json.

Так как Graal VM поддерживает первоначальную настройку через статические файлы, эти файлы помещаются при сборке в каталог META-INF/native-image

Для каждого сборщика выпущен свой плагин, который активирует работу Spring AOT. Для maven это spring-aot-maven-plugin, соответственно для gradle - spring-aot-gradle-plugin.Для того, чтобы добавить gradle плагин в свой проект нужна всего одна строка:

plugins {id 'org.springframework.experimental.aot' version '0.9.0'}

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

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

Например, для случаев реализации компонентов с помощью WebClient можно использовать аннотацию из пакета org.springframework.nativex.hint, чтобы указать какой тип мы будем обрабатывать:

@TypeHint(types = Data.class, typeNames = "com.example.webclient.Data$SuperHero")
@SpringBootApplication
public class WebClientApplication {
	// ...
}

Здесь мы указываем, что будем сериализовать класс Data, в котором есть подкласс SuperHero. Во время сборки для нас заранее создадут клиент, который сможет работать с этим типом данных.

Так как graavlvm не поддерживает работу с динамическими прокси, то для поддержки работы с java.lang.reflect.Proxy создана аннотация @ProxyHint.

Применять ее можно, например, так:

@ProxyHint(types = {
     org.hibernate.Session.class,
     org.springframework.orm.jpa.EntityManagerProxy.class
})

Если необходимо подтянуть какие-либо ресурсы в образ, то необходимо воспользоваться аннотацией @ResourceHint.Например, таким образом:

@ResourceHint(patterns = "com/mysql/cj/TlsSettings.properties")

Чтобы указать какие классы / пакеты должны быть инициализированы явно во время сборки или выполнения, нужно воспользоваться аннотацией @InitializationHint:

@InitializationHint(types = org.h2.util.Bits.class,
								    initTime = InitializationTime.BUILD)

Для того, чтобы компактно собрать все эти аннотации воедино создана аннотация @NativeHint:

@Repeatable(NativeHints.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface NativeHint

Все вместе это будет выглядеть, например, вот так:

@NativeHint(
    trigger = Driver.class,
    options = "--enable-all-security-services",
    types = @TypeHint(types = {
       FailoverConnectionUrl.class,
       FailoverDnsSrvConnectionUrl.class,
       // ...
    }), resources = {
	@ResourceHint(patterns = "com/mysql/cj/TlsSettings.properties"),
	@ResourceHint(patterns = "com.mysql.cj.LocalizedErrorMessages",
                      isBundle = true)
})

В качестве тригера, мы выбираем тот класс, присутствие которого в classpath должно вызвать построение конфигурации.

Все активные аннотации учитываются во время компиляции и преобразуются в конфигурацию Graal VM плагином Spring AOT. 

Spring Native уже включена в релизный цикл, забрать шаблон можно прямо со start.spring.io. Так как поддержка JPA и прочих spring компонентов уже реализована, то собрать простое CRUD приложение можно сразу. Если необходимо указать дополнительные параметры Graal VM при сборке, их можно добавить с помощью переменной среды BP_NATIVE_IMAGE_BUILD_ARGUMENTS в плагине Spring AOT, если сборка идет через Buildpacks, или с помощью элемента конфигурации “<buildArgs>” в pom.xml, если вы собираете через плагин native-image-maven-plugin.

Собственно, выполняем команды mvn spring-boot: build-image или gradle bootBuildImage - и начнется сборка образа. Стоит отметить, что сборщику нужно более 7 Гб памяти, для того сборка завершилась успешно. На моей машине сборка, вместе с загрузкой образов заняла не более 5 минут. При этом образ получился очень компактным, всего 60 Мб. Стартовало приложение за 0.022 секунды! Это невероятный результат. Учитывая, что все большее количество компаний переходит на K8s и старт приложения, так же как и используемые ресурсы очень важны в современном мире, то данная технология позволяет Spring сделать фреймворком номер один для всех типов микросервисов, даже для реализаций FaaS, где очень важна скорость холодного старта.

@ivanovdev
13.03.2021 20:56 UTC
Первоисточник

Комментарии

@
13.03.2021 16:03 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@ivanovdev
13.03.2021 20:11 UTC
0

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

@sshikov
13.03.2021 18:23 UTC
+3
Стартовало приложение за 0.022 секунды!


А не кажется ли вам, что это все какой-то самообман? В смысле, мы сами себя ограничиваем k8s и stateless приложениями, пытаемся оптимизировать холодный старт, чтобы микросервис можно было прибить в любой момент, и быстро рестартовать на другом узле — но это же автоматически означает, что приложение не может, например, эффективно использовать кеш, потому что при холодном старте нет прогрева этого кеша.

>даже для реализаций FaaS, где очень важна скорость холодного старта.
Я понимаю что быстрый старт — он всегда в общем не вреден, уж как минимум, вопрос скорее в том, стоит ли овчинка выделки? Как бы вы в целом оценили трудозатраты на всю работу, описанную тут? Это какой-то фиксированный объем для отдельного приложения, или будет зависеть от объема кодовой базы, или еще от чего-то?
@ivanovdev
13.03.2021 20:18 UTC
0

Добрый день! На данный момент рассматриваем возможность перевести очень маленькие(пару сотен строк кода) сервисы, которые не нужны все 100% времени. Основные сервисы останутся как есть. Но, полагаю, это позволит выиграть немного памяти и ЦПУ.

13.03.2021 20:33 UTC
0
То есть, у вас это именно мелкие, и stateless? А что насчет затрат усилий?
13.03.2021 20:53 UTC
0

По поводу затрат ещё придется разобраться. Хочется организовать некий темплейт, по которому в будущем можно будет такие микросервисы штамповать пачками.

14.03.2021 05:29 UTC
0
Там может не мучиться с натягиванием спринга на грааль и перевести их на микронавт?
14.03.2021 10:55 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@darkit
13.03.2021 20:42 UTC
+1
например, эффективно использовать кеш

он лежит в редисе и не важно сейчас у вас 2 инстанса или 20

14.03.2021 05:54 UTC
0
Логично. Т.е. в наличии один statefull сервис, который позволяет остальным быть stateless?
14.03.2021 09:05 UTC
0

а вам редис все равно нужен или вы подымаете кеш локальный в каждом сервисе, а потом думаете как его синхронизировать между всеми инстансами?

14.03.2021 11:10 UTC
0
Мне — нет. Но это специфика моих проектов, потому что они вообще не веб, а совсем другие.
@hrensgory
14.03.2021 07:30 UTC
+3

Кваркус (https://quarkus.io) компилируется в натив уже очень давно, в коде при этом ничего править или добавлять не нужно.
Для того, чтобы собрать приложение (jax-rs, cdi, jpa) в натив уходит примерно 10 минут полной загрузки четырёхядерной машины и 8 гб памяти.
Стартует оно быстро, это правда. Но в процессе старта есть задачи, не связанные с инициализацией jvm — накатить liquibase, подцепиться к кафке и т.п., по ним очевидно нет выигрыша во времени. В потреблении памяти и ЦПУ принципиальных различий не замечено, что вполне естетственно.
Вообщем мы пока решили что овчинка выделки не стоит. Хотя всё это, конечно, интересно и т.п.
Вообще, по нашим наблюдениям, в обычном jvm режиме кваркус сравнимой функциональности стартует примерно в 5 (пять) раз быстрее спринг-бута при тех же ограничениях по железу.

@darkit
14.03.2021 09:17 UTC
0
В потреблении памяти и ЦПУ принципиальных различий не замечено, что вполне естетственно.

у меня кваркус с кафкой в нативном моде комфортно работает потребляя 15-20 мб памяти, а в жвм режиме порядка 200Мб

15.03.2021 07:01 UTC
+1
15-20 Мб RSS? Чё-то с трудом верится. Хотя всякое, конечно, бывает. На том сервисе, на котором я экспериментировал, он отъел плюс/минус столько же, сколько и в JVM режиме.
15.03.2021 09:53 UTC
0

вот конфиг с кубера


resources:
  limits:
    cpu: 0.1
    memory: 40M
  requests:
    cpu: 0.05
    memory: 20M
15.03.2021 13:41 UTC
0
Красотища. А в сервисе только кафка?
15.03.2021 15:58 UTC
0

Да, прочитали с топика, сделали бизнес логику по преобразованию сущности и записали ее в другой топик.
Вот что подключено сейчас


Installed features: [cdi, config-yaml, hibernate-validator, kotlin, micrometer, mutiny, smallrye-context-propagation, smallrye-fault-tolerance, smallrye-health, smallrye-reactive-messaging, smallrye-reactive-messaging-kafka, vertx]
@PrinceKorwin
14.03.2021 10:05 UTC
0
В потреблении памяти и ЦПУ принципиальных различий не замечено, что вполне естетственно.

Не очень естественно. У GraalVM большинство оптимизаций про память. Мы у себя проводили тестирование. Выигрыш есть и местами существенный именно по памяти. Но вот скорость компиляции/линковки и требуемые под это дело ресурсы вынудили нас остаться с Кваркусом на JVM.

@vba
15.03.2021 13:29 UTC
+1

А вот оно, Annotation Oriented Programming in Action.