(java || kotlin) && devOps
369 subscribers
6 photos
1 video
6 files
306 links
Полезное про Java и Kotlin - фреймворки, паттерны, тесты, тонкости JVM. Немного архитектуры. И DevOps, куда без него
Download Telegram
Всем привет!

Сегодня расскажу про технологию native image.

Стандартная схема работы JVM приложения такая:
1) компилятор превращает исходники в байт-код
2) байт-код запускается на JVM
3) в процессе работы JVM анализирует использование байт-кода и при необходимости оптимизирует его, включая компиляцию в бинарное представление для конкретной процессорной архитектуры. И основные оптимизации надо отметить происходят именно здесь, а не при первичной компиляции. Еще важный момент - классы\библиотеки подгружаются в память не обязательно при старте приложения, а по мере использования. Все это называется JIT - Just in time компиляция. Влиять на нее можно с помощью ряда флагов запуска Java приложения - -server, -client.

Плюс такого подхода - JVM позволяет в 90% случаев игнорировать, на каком железе запускается Java приложение. Минус - долгий старт Java приложения плюс время для "разогрева" и выхода на рабочий режим.

Но с другой стороны с развитием Docker мы и так можем игнорировать особенности железа и ОС на хост-сервере, главное, чтобы там можно было запустить Docker. И наконец кроме долгого старта и разогрева собственно JVM у нас как правило есть Spring с кучей модулей, число которых растет, и в итоге время старта типичного Spring Boot приложения доходит до совсем неприличных величин.

Альтернатива - AOT - Ahead-of-Time compilation. В этом случае мы компилируем исходники в бинарный код в момент первичной компиляции. Причем как собственно приложение, так и JVM и все JAR. Получается такой native image монолит. Проект называется GraalVM https://www.graalvm.org/, официально поддерживается Oracle. Есть open-source версия, основанная на OpenJDK.

Плюс этого подхода - скорость запуска. Это критически важно в облаках, т.к. k8s может "случайно" рестартовать под при изменении конфигурации железа или настроек Deployment. Еще будет выигрыш в скорости обработки запросов, т.к. не тратится CPU и память в runtime на JIT компиляцию.

Какие минусы?

1) невозможна динамическая\ленивая загрузка библиотек\плагинов, classpath фиксируется в момент компиляции. К слову - у этого ограничения есть и плюсы, сложнее эксплуатировать уязвимости типа log4j injection - см. https://t.me/javaKotlinDevOps/4

2) вопрос - откуда компилятор узнает, какой код ему нужно добавить в наш native монолит? Ответ: он идет от метода main. Соответственно, код который явно не вызывается, а, например, вызывается через рефлексию, он не увидит. Соответственно, никакой рефлексии в ПРОМ коде. Что, надо сказать, в целом правильно)

3) аналогично просто так не заработает магия Spring, основанная на рефлексии и динамических прокси. Из чего следует, что мало добавить в Spring приложение AOT компилятор - нужно дорабатывать сам Spring, что и было сделано в Spring Boot 3.2. Другие фреймворки также придется дорабатывать. Например, Mockito до сих пор не работает в native image. Справедливости ради тут причина такая же, как в анекдоте про неуловимого ковбоя Джо - не нужен Mockito в native image)

4) если продолжить про Spring - загрузка бинов по условию: @ConditionalOnProperty, @Profile - тоже не заработает. Нужно указывать при сборке необходимый профиль, чтобы уже при компиляции нужные бины были обнаружены и добавлены в дистрибутив.

5) еще вопрос - но ведь среднее Java приложение + библиотеки + JVM = миллионы строк кода, что будет с компиляцией? Ответ - компиляция будет долгой, до 10 минут на spring boot hello world. Поэтому в документации Spring прямо сказано, что хотя Spring поддерживает запуск тестов в native image - делать так нужно только для интеграционных тестов, лучше на CI, а модульные запускать по старинке, т.к. тут критична скорость получения результата.

#jvm #performance #native_image #spring #docker #buildpacks #cloud #java_start_boost
Есть еще ряд интересных моментов. Я расскажу про них на примере Spring Boot native image.

Для борьбы с тем, что часть кода недостижима если идти от точки входа (метод main), есть два инструмента.
1) специальный tracing агент, который можно подключить к приложению, и он будет в runtime логировать такие скрытые вызовы. https://www.graalvm.org/22.3/reference-manual/native-image/metadata/AutomaticMetadataCollection/
2) далее можно создать т.наз. hints - подсказки AOT компилятору, что включить в native image, из того, что он не нашел сам - https://www.graalvm.org/latest/reference-manual/native-image/metadata/ Собственно, большая доля в адаптации фреймворка типа Spring для native image - подготовка таких hints, https://docs.spring.io/spring-boot/docs/3.2.1/reference/html/native-image.html

А что делать если в момент сборки еще не ясно - нужен native image или нет? Или нужны обе версии? Нет проблем - можно совместить оба режима JIT и AOT и создать артефакт, Spring Boot Executable Jar, с байткодом и всеми необходимыми для native image метаданными. И собрать из него native image позже в DevOps pipeline при необходимости.

Для Spring Boot есть два режима сборки. Основной - Native Image Using Buildpacks, в котором в итоге получается docker образ. Для него нужен только Docker на машине-сборщике. И т.наз. Native Build Tools - нужно устанавливать дистрибутив GraalVM, содержащий эти tools, в итоге получается бинарник для железа, на котором происходит сборка.

Итого - штука полезная, но только если вас категорически не устраивает время запуска приложения и все используемые вами фреймворки поддерживают native image.

#jvm #performance #native_image #spring #docker #buildpacks #cloud #startup_time
Всем привет!

Какие компиляторы есть в Java? Сколько их вообще?
Простой ответ - один, называется javac. Им мы компилируем исходники в байт-код, который потом исполняет JVM.
Исполняет и что важно оптимизирует.
Вообще говоря, основные оптимизации происходят именно runtime, а javac является довольно простым.
Идея в том, что собирается статистика использования кода во время выполнения, часто используемый код компилируется в "нативный" - ассемблер для конкретного процессора, а неиспользуемый - удаляется. Таким образом получаем плюс еще один компилятор - JIT, он же Just in Time. Только на самом деле, исторически, компиляторов два - один быстрый, но оптимизирующий не оптимально))), второй - наоборот, медленный и хорошо оптимизирующий. C1 и C2, сейчас они используются в паре, подробнее можно почитать тут https://for-each.dev/lessons/b/-jvm-tiered-compilation

А можно без байт-кода? Да, есть AOT - Ahead of Time компилятор, называется native-image, поставляется в GraalVM https://www.graalvm.org/latest/reference-manual/native-image/
Он сразу компилирует в требуемый "нативный" код.
А если поддержки требуемой процессорной архитектуры нет? Был момент доминирования x86, но сейчас растет популярность ARM архитектур, а там, я так понимаю, тот еще зоопарк.
А для этого уже существует промежуточный язык и набор компиляторов LLVM https://llvm.org/. Что-то типа Java байт-кода, только не привязанный к Java. GraalVM его тоже поддерживает https://www.graalvm.org/latest/reference-manual/native-image/LLVMBackend/
А можно его использовать как runtime компилятор? Почему нет, в Azul JDK решили отказаться от C1\C2 и сделать свой компилятор с блекджеком и LLVM - https://www.azul.com/products/components/falcon-jit-compiler/ Да, ошибся немного, блекджека там, увы, нет)

А еще есть компиляторы Kotlin, Scala, Groovy, Jython, JRuby... И Kotlin native, также использующий LLVM https://kotlinlang.org/docs/native-overview.html В общем я сбился со счета)

#java #jvm #jdk #native_image #compilers