AOT-кеш в JVM: минус половина времени старта без GraalVM
Пока сервис деплоили раз в неделю, никого не волновало, что JVM стартует пять секунд. В контейнерах эти секунды всплывают постоянно: при каждом рестарте пода, при каждой новой реплике под нагрузкой, при каждом rolling update. Инстанс уже съедает память и процессор, но запросы ещё не принимает, а под всплеск трафика он доезжает до готовности как раз к тому моменту, когда пик прошёл и p99 просел.
Обиднее всего, что работа каждый раз повторяется впустую. JVM читает байт-код, парсит, верифицирует, загружает и связывает все классы, до которых дотягивается код. Для типичного Spring Boot это тысячи классов, и разбирает она их совершенно одинаково — просто нигде не запоминает.
Классическое решение — GraalVM Native Image. Проблему он вроде как закрывает, но просит взамен closed-world анализ, конфиги под каждую рефлексию, отдельный профиль сборки и отказ от JIT ради статического бинарника. На проекте с историей такой переезд легко растягивается на квартал — и не факт, что заканчивается успехом.
Project Leyden берёт дешевле: JVM остаётся обычной, а разбор классов уезжает в разогревочный прогон, например, на этап сборки образа. Результат ложится в файл, и в проде JVM просто читает готовое. Старт сокращается примерно вдвое, в коде не меняется ни строки.
В JDK 25 всё делается двумя командами. Раньше, когда механизм только появился в JDK 24, шагов было три — сначала записать конфигурацию, потом собрать из неё кеш, но эти два действия объединили:
# разогревочный прогон, на выходе -- app.aot
java -XX:AOTCacheOutput=app.aot -jar app.jar
# продакшен
java -XX:AOTCache=app.aot -jar app.jar
Со Spring Boot есть нюанс: обучающий прогон должен чем-то закончиться, иначе приложение просто останется работать и кеш никогда не соберётся. Поэтому его поднимают до готовности контекста и сразу гасят флагом -Dspring.context.exit=onRefresh.
Классами дело не ограничивается. В кеш попадают ещё и профили методов: на обучении JVM запоминает, что вызывалось чаще всего, а в проде JIT читает эту статистику сразу и берётся за горячие места, вместо того чтобы собирать её самому в первые минуты работы. Сервис быстрее выходит на полную скорость.
Цифры получаются такие: на живом приложении Spring Boot старт сократился с 4,9 до 2,4 секунды, на PetClinic ранние сборки давали 40–41%, разогрев ускоряется на 15–25%.
Взамен придётся следить за одной вещью: обучение и продакшен должны совпадать — тот же classpath, те же флаги, та же версия приложения. Разойдутся — JVM остановится с ошибкой, а не станет работать на устаревшем кеше. Поэтому сборку кеша обычно вешают на тот же шаг пайплайна, где собирается образ.
У тех, кто сидит на ZGC, всё это до недавнего времени не работало вовсе: сборщик прячет служебные данные прямо внутри ссылок на объекты, и кеш такого не переносил. Починили только в JDK 26.
Главное же отличие от Native Image в том, что Leyden ничего не забирает взамен. Рефлексия, динамическая загрузка классов и JIT остаются на месте. Если в проде что-то пойдёт не так, как на обучении, JVM спокойно доработает обычным способом — без падений и без необходимости заранее описывать в конфигах каждый возможный случай.
#backendvkhub #graalvm #jvm
Пока сервис деплоили раз в неделю, никого не волновало, что JVM стартует пять секунд. В контейнерах эти секунды всплывают постоянно: при каждом рестарте пода, при каждой новой реплике под нагрузкой, при каждом rolling update. Инстанс уже съедает память и процессор, но запросы ещё не принимает, а под всплеск трафика он доезжает до готовности как раз к тому моменту, когда пик прошёл и p99 просел.
Обиднее всего, что работа каждый раз повторяется впустую. JVM читает байт-код, парсит, верифицирует, загружает и связывает все классы, до которых дотягивается код. Для типичного Spring Boot это тысячи классов, и разбирает она их совершенно одинаково — просто нигде не запоминает.
Классическое решение — GraalVM Native Image. Проблему он вроде как закрывает, но просит взамен closed-world анализ, конфиги под каждую рефлексию, отдельный профиль сборки и отказ от JIT ради статического бинарника. На проекте с историей такой переезд легко растягивается на квартал — и не факт, что заканчивается успехом.
Project Leyden берёт дешевле: JVM остаётся обычной, а разбор классов уезжает в разогревочный прогон, например, на этап сборки образа. Результат ложится в файл, и в проде JVM просто читает готовое. Старт сокращается примерно вдвое, в коде не меняется ни строки.
В JDK 25 всё делается двумя командами. Раньше, когда механизм только появился в JDK 24, шагов было три — сначала записать конфигурацию, потом собрать из неё кеш, но эти два действия объединили:
# разогревочный прогон, на выходе -- app.aot
java -XX:AOTCacheOutput=app.aot -jar app.jar
# продакшен
java -XX:AOTCache=app.aot -jar app.jar
Со Spring Boot есть нюанс: обучающий прогон должен чем-то закончиться, иначе приложение просто останется работать и кеш никогда не соберётся. Поэтому его поднимают до готовности контекста и сразу гасят флагом -Dspring.context.exit=onRefresh.
Классами дело не ограничивается. В кеш попадают ещё и профили методов: на обучении JVM запоминает, что вызывалось чаще всего, а в проде JIT читает эту статистику сразу и берётся за горячие места, вместо того чтобы собирать её самому в первые минуты работы. Сервис быстрее выходит на полную скорость.
Цифры получаются такие: на живом приложении Spring Boot старт сократился с 4,9 до 2,4 секунды, на PetClinic ранние сборки давали 40–41%, разогрев ускоряется на 15–25%.
Взамен придётся следить за одной вещью: обучение и продакшен должны совпадать — тот же classpath, те же флаги, та же версия приложения. Разойдутся — JVM остановится с ошибкой, а не станет работать на устаревшем кеше. Поэтому сборку кеша обычно вешают на тот же шаг пайплайна, где собирается образ.
У тех, кто сидит на ZGC, всё это до недавнего времени не работало вовсе: сборщик прячет служебные данные прямо внутри ссылок на объекты, и кеш такого не переносил. Починили только в JDK 26.
Главное же отличие от Native Image в том, что Leyden ничего не забирает взамен. Рефлексия, динамическая загрузка классов и JIT остаются на месте. Если в проде что-то пойдёт не так, как на обучении, JVM спокойно доработает обычным способом — без падений и без необходимости заранее описывать в конфигах каждый возможный случай.
#backendvkhub #graalvm #jvm