(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
Всем привет!

В развитие прошлого поста предлагаю поговорить про большие компании и новые языки программирования.
С одной стороны тут нет противоречия разработка-сопровождение.
Если говорить про JVM или .NET языки - т.е. случай, когда есть виртуальная машина между приложением и ОС - то при смене языка ничего в плане сопровождения не меняется. Случаи, когда новый язык сгенерирует какой-то неработающий или неоптимальный байт-код я рассмотрю далее.
А если даже язык не JVM\.NET - мы живем в мире победившего Docker и тут все еще лучше: сопровождению остается провалидировать базовый образ, который к нам пришел от создателей языка, и\или выставить требования, что там должно быть.

Может возникнуть вопрос - а что делать, если готового базового образа нет?
Я бы его перефразировал - а что, если язык настолько редкий или новый, что его создатели забыли\не доросли до Docker.
Это вопрос к тем, кто занимается наймом. HR или Tech Lead\ИТ лидер. От них нужен анализ рынка для того, чтобы ответить на вопрос: что будет, если разработчик, предложивший новый перспективный язык, уйдет из компании? Насколько такие разработчики дороже?
Кроме анализа рынка тут может помочь https://www.tiobe.com/tiobe-index/, в частности подсказать тренд - растет или падает популярность языка.
И третий критерий фильтрации языков - насколько язык сложен для обучения. Тут поможет экспертное мнение Team Lead\Tech Lead.

По первому критерию для сферической компании в вакууме я бы отмел, к примеру, Scala. И как ни обидно для Scala - по-третьему тоже. Все же Scala - это немного brainfuck)
Почему для сферической компании в вакууме - есть компании в РФ, где Scala живет давно, для них очевидно этот аргумент не актуален.
Да и по сложности обучения все не так однозначно - если в компании принцип: "Отбираем лучших", - то Scala как раз может быть позитивным фильтром)
По второму критерию можно отфильтровать Ruby, пик его популярности пройден.

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

Да, возвращаясь к вопросу про неработающий байт-код. Если язык на рынке давно, то вероятность такого события пренебрежимо мала. А чтобы ее уменьшить еще сильнее есть древнее правило - не надо обновляться на новые версии в первых рядах, стоит подождать хотя бы месяц-другой. И чтобы добить эту тему - возражение "мы не сможем оказывать вам поддержку (консультации) для языка xyz" - некорректно. Т.к. если язык распространен - поддержку окажет документация, stackoverflow, habr, Yandex\google, или даже ChatGPT

#jvm #language #scala
Всем привет!

Я уже рассказывал про один из вариантов ускорения запуска JVM приложений - использование native image https://t.me/javaKotlinDevOps/242
Напомню, основная идея была в том, что на этапе компиляции мы превращаем байт-код в нативный код. Можно рассматривать этот процесс как некий дамп универсального кода в конкретный, предназначенный для определенной процессорной архитектуры.

Похожий принцип используется и в случае JVM checkpoint/restore https://openjdk.org/projects/crac/ - проект CRaC.
Проект использует функционал Linux checkpoint/restore для Docker образов https://criu.org/Main_Page.
Т.е. в данном случае мы дампим все содержимое памяти JVM приложения на диск.
Работает, соответственно только для Docker и только в Linux, но кажется это не критическое ограничение.
Вот как это можно сделать на чистом Java приложении https://habr.com/ru/articles/719522/
Есть поддержка на всех основных платформах - Spring Boot, Micronaut, Quarqus, см. https://github.com/CRaC/docs
Проблему долгого первого запуска можно обойти либо сделав дамп до выхода на ПРОМ на идентичном Linux-е, либо разворачивая новые версии как канарейку или в моменты минимальной нагрузки, т.е. когда долгий старт не критичен.

Плюсом этого решения перед native image является то, что нет никаких ограничений на динамическую загрузки библиотек и рефлексию.

Кажется, одним из выгодоприобитетелей будут облачные провайдеры FaaS - Function as a Service, а если быть точным - их пользователи. И, собственно, так и есть - Amazon Lambda уже https://github.com/CRaC/aws-lambda-java-libs подддерживает

#crac #startup_time #jvm #performance #java_start_boost
Всем привет!

Какие компиляторы есть в 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
Всем привет!

Ну и еще одна оптимизация времени старта Java приложения. Самые внимательные уже могли ее заметить пройдя по ссылкам из предыдущего поста.

С момента появления Spring Boot упаковка приложения в fat jar - jar содержащий все зависимости и Tomcat в придачу (или другой контейнер сервлетов) - стала неким стандартом.
Но fat jar при исполнении требуется распаковать. А разархивация всегда требовала времени, не зря архиваторы используются как бенчмарки для процессорных тестов.

Соответственно, можно заранее разложить зависимости по отдельным файлам для ускорения старта. Вот как рекомендует это делать Spring https://docs.spring.io/spring-boot/reference/packaging/efficient.html
Судя по данным статьи из вчерашнего поста это даст еще 25% ускорения при старте https://spring.io/blog/2023/12/04/cds-with-spring-framework-6-1

#performance #spring #jvm #java_start_boost