Добрый день.
Недавно передо мной встала задача запуска 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 можно собрать собственный рантайм что позволит значительно сократить размер образа приложения.