Добрый день.
Недавно передо мной встала задача запуска spring boot 2 приложения в kubernetes кластере используя docker образ. Эта проблема не является новой, достаточно быстро я нашел примеры в гугле и запаковал свое приложение. Я был очень удивлен не найдя alpine образ для jdk11 и надеялся что slim будет достаточно небольшим, но момент отправки образа на docker registry я обратил внимание что его размер составлял почти 422 мегабайт. Под катом описание того как я уменьшил docker образ с моим spring boot и java 11 до 144 мегабайт.
Приложение
Как я уже упомянул ранее, мое приложение построено используя spring boot 2 и представляет из себя REST API обертку над реляционной базой данных (используя @RepositoryRestResource). Мои зависимости включают:
org.springframework.boot:spring-boot-starter-data-rest
org.springframework.boot:spring-boot-starter-data-jpa
org.flywaydb:flyway-core
org.postgresql:postgresql
Собранный jar файл имеет размер: 37,6 мегабайт.
Dockerfile:
FROM openjdk:11-jdk-slim
WORKDIR /home/demo
ARG REVISION
COPY target/spring-boot-app-${REVISION}.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]
В результате сборки я получаю образ размером: 422 мб согласно выводу команды docker images. Интересно что при использовании устаревшего образа 8-jdk-slim, размер уменьшается до 306 мб.
Попытка 1: другой базовый образ
Первым логичным шагом была попытка найти более легковесный образ, желательно на основе alpine. Я просканировал на наиболее популярные репозитории с джавой:
- https://hub.docker.com/_/openjdk
- https://hub.docker.com/r/adoptopenjdk/openjdk11
- https://hub.docker.com/r/adoptopenjdk/openjdk11-openj9
- https://hub.docker.com/r/adoptopenjdk/openjdk8
(11 как текущий LTS релиз и 8 так как все еще есть достаточное количество приложений которые не смогли мигрировать на более современные версии)
Таблица с образами и тегами (~2700), их размерами на момент написания статьи доступна тут
Вот некоторые из них:
openjdk 8 488MB
openjdk 8-slim 269MB
openjdk 8-alpine 105MB
openjdk 8-jdk-slim 269MB
openjdk 8-jdk-alpine 105MB
openjdk 8-jre 246MB
openjdk 8-jre-slim 168MB
openjdk 8-jre-alpine 84.9MB
openjdk 11 604MB
openjdk 11-slim 384MB
openjdk 11-jdk 604MB
openjdk 11-jdk-slim 384MB
openjdk 11-jre 479MB
openjdk 11-jre-slim 273MB
adoptopenjdk/openjdk8 alpine 221MB
adoptopenjdk/openjdk8 alpine-slim 89.7MB
adoptopenjdk/openjdk8 jre 200MB
adoptopenjdk/openjdk8 alpine-jre 121MB
adoptopenjdk/openjdk11 alpine 337MB
adoptopenjdk/openjdk11 alpine-slim 246MB
adoptopenjdk/openjdk11 jre 218MB
adoptopenjdk/openjdk11 alpine-jre 140MB
Таким образом, если поменять базовый образ на adoptopenjdk/openjdk11:alpine-jre то можно уменьшить образ с приложением до 177 мб.
Попытка 2: custom runtime
С момента выпуска jdk9 и модуляризации появилась возможность собрать собственный рантайм который содержит только те модули что необходимы вашему приложению. Детальнее об этой функциональности можно прочитать тут.
Попробуем определить необходимые модули для тестового spring boot приложения:
~/app ? jdeps -s target/app-1.0.0.jar
app-1.0.0.jar -> java.base
app-1.0.0.jar -> java.logging
app-1.0.0.jar -> not found
Окей, похоже что jdeps не может справиться с fat-jar созданным при помощи spring boot, но мы можем распаковать архив и прописать classpath:
~/app ? jdeps -s -cp target/app-1.0.0/BOOT-INF/lib/*.jar target/app-1.0.0.jar.original
Error: byte-buddy-1.9.12.jar is a multi-release jar file but --multi-release option is not set
~/app ? jdeps -s --multi-release 11 -cp target/app-1.0.0/BOOT-INF/lib/*.jar target/app-1.0.0.jar.original
Error: aspectjweaver-1.9.2.jar is not a multi-release jar file but --multi-release option is set
По этому поводу на текущий момент открыт баг: https://bugs.openjdk.java.net/browse/JDK-8207162
Я попробовал скачать jdk12 чтобы получить эту информацию, но столкнулся со следующей ошибкой:
Exception in thread "main" com.sun.tools.classfile.Dependencies$ClassFileError
...
Caused by: com.sun.tools.classfile.ConstantPool$InvalidEntry: unexpected tag at #1: 53
Методом проб, ошибок и поиска модулей по ClassNotFoundException я определил что моему приложению необходимы следующие модули:
- java.base
- java.logging
- java.sql
- java.naming
- java.management
- java.instrument
- java.desktop
- java.security.jgss
Рантайм для них можно собрать используя:
jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,java.logging,java.sql,java.naming,java.management,java.instrument,java.desktop,java.security.jgss --output /usr/lib/jvm/spring-boot-runtime
Попробуем построит базовый docker образ используя эту модули:
FROM openjdk:11-jdk-slim
RUN jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,java.logging,java.sql,java.naming,java.management,java.instrument,java.desktop,java.security.jgss --output /usr/lib/jvm/spring-boot-runtime
FROM debian:stretch-slim
COPY --from=0 /usr/lib/jvm/spring-boot-runtime /usr/lib/jvm/spring-boot-runtime
RUN ln -s /usr/lib/jvm/spring-boot-runtime/bin/java /usr/bin/java
и соберем его:
docker build . -t spring-boot-runtime:openjdk-11-slim
В результате размер составил 106 мегабайт, что значительно меньше большинства найденных базовых образов с openjdk. Если использовать его для моего приложения, то результирующий размер получится 144 мегабайт.
Далее мы можем использовать spring-boot-runtime:openjdk-11-slim
как базовый образ для всех spring boot приложений если они имеют схожие зависимости. В случае различных зависимостей, возможно использовать multistage сборку образа для каждого из приложений где на первом этапе будет собираться java runtime, а на втором добавляться архив с приложением.
FROM openjdk:11-jdk-slim
RUN jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,YOUR_MODULES --output /usr/lib/jvm/spring-boot-runtime
FROM debian:stretch-slim
COPY --from=0 /usr/lib/jvm/spring-boot-runtime /usr/lib/jvm/spring-boot-runtime
WORKDIR /home/demo
ARG REVISION
COPY target/app-${REVISION}.jar app.jar
ENTRYPOINT ["/usr/lib/jvm/spring-boot-runtime/bin/java","-jar","app.jar"]
Вывод
На текущий момент большинство docker образов для java имеют достаточно большой объем, что может негативно сказаться на времени старта приложения, особенно в случае если необходимых слоев еще нет на сервере. Используя теги с jre либо воспользовавшись модуляризацией java можно собрать собственный рантайм что позволит значительно сократить размер образа приложения.
Комментарии (13)
apangin
25.06.2019 12:08bellsoft/liberica-openjdk-alpine — полноценный docker образ OpenJDK 11 всего 131 MB.
А в вашем получившемся образе, смотрю, отсутствуют даже самые базовые диагностические утилиты вродеjstack
иjmap
. Или вы не мониторите и не профилируете приложения в продакшне?gmandnepr Автор
25.06.2019 12:40У образа около 5к загрузок, подскажите, как у него с поддержкой и обновлениями? Потенциально может быть хорошей альтернативой скажем adoptopenjdk/openjdk11:alpine-jre.
Касательно мониторинга (предполагая что все это запущено в kubernetes): для метрик приложения (память, CPU) я бы вытаскивал их из подов при помощи heapster/grafana/influxdb, для бизнес метрик я бы строил дашборды на кибане используя elastic-stack. Оба подхода можно подключить используя daemonsets что позволит избежать изменений в самих сервисах и приложениях. Как вы верно заметили, ограничение этих подходов в том что JVM метрики мы не собираем.
Во многих случаях аномольно большое использование CPU либо памяти привлечет внимание, но вы правы, может понадобится больше диагностической информации, в часности о JVM.gecube
25.06.2019 18:07Я инжектил в jvm jmx exporter, дальше вытащить инфу на уровень кластера и в прометеус не проблема.
SimSonic
25.06.2019 19:07Я использую этот как базовый: https://hub.docker.com/r/bellsoft/liberica-openjdk-alpine-musl
Ту версию, что по умолчанию (лайт, 100 мб). Spring boot 2, постгря/оракл, полёт нормальный.gecube
26.06.2019 08:27musl проблемы не вызывал?
Я как минимум хапнул несовместимость ffmpeg с ним /это вне контекста Java, если что/SimSonic
26.06.2019 08:47Нет, не вызывал. Долго всматривался, как приложение работает, никаких проблем не увидел. Но, конечно же, тестировать и проверять надо внимательно.
И я не понял отличие от просто -alpine сборки, там же тоже должен быть по идее musl.
gecube
25.06.2019 15:54Просто тут я наблюдаю ту же историю, что и с Python — чем больше модулей использует приложение — тем меньше выигрыш от кастомного образа. Насколько действительно образ 422МБ хуже, чем 144МБ? Может проблема в голове разработчика? Потому что снижая размер образа он усложняет процесс сборки и поддержки (нужно внимательно следить за тем, какие зависимости нужно в образ докинуть, чтобы приложение запустилось). Т.е. все-таки каков экономический эффект от уменьшения размера? Кто сможет объяснить?
gmandnepr Автор
25.06.2019 17:16Я бы предложил оценивать с точки зрения, какие проблемы образ решает лучше или хуже?
- запустить приложение: решают одинаково, при условии что мы протестировали и приложение запускается
- простота сборки: меньший образ хуже так как его сложнее приготовить
- колличество денег что компания заработает: вероятно тоже не влияет
- security: возможно лучше иметь только то что необходимо и изолировать себя опасности от того что не нужно, но есть в образе
- скорость запуска на новой ноде k8s которая была только что создана на aws spot instance: меньший образ, вероятно, загрузится быстрее
gecube
25.06.2019 17:22-1Я только с последним пунктом согласен. Но и то не факт, т.к. общие слои уже почти наверняка лежат в кэше на ноде кубера. Если, конечно, базовый образ не поменялся. И это не зависит от политики скачивания (Always Pull Policy — оно ес-но кэш не отключает).
По безопасности — тоже как бы не сильно видно профита. Ну, будет не 20 компонентов в образе, а 10. Все равно строить пайплайн проверки образов на уязвимости. Да, конечно, в случае, если в 20-м модуле, который не используется найдена уязвимость, то мелкий образ (который на 10 нужных) без него пересобирать не придется. Но это спорное преимущество.
splix
25.06.2019 18:56+1Безотносительно размера докер образа, советую посмотреть на Jib github.com/GoogleContainerTools/jib Это инструмент упаковки Java приложений в Docker, поддерживает Spring Boot и выбор базовых образов.
Не уверен насчет JDK custom runtime, но у него много других преимуществ. Например он складывает внешние библиотеки, код приложения и ресурсы приложения в разные слои. Поэтому размер обновления, т.е. слоя который по факту нужно скачать на сервере, обычно равен размеру вашего кода.
dimkrayan
а еще можно попробовать распаковать jar. Он все равно его распакует при запуске. Это, кстати, есть в рекомендациях по ускорению спринга.