Добрый день.


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



(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)


  1. dimkrayan
    25.06.2019 11:43
    +1

    а еще можно попробовать распаковать jar. Он все равно его распакует при запуске. Это, кстати, есть в рекомендациях по ускорению спринга.


  1. apangin
    25.06.2019 12:08

    bellsoft/liberica-openjdk-alpine — полноценный docker образ OpenJDK 11 всего 131 MB.

    А в вашем получившемся образе, смотрю, отсутствуют даже самые базовые диагностические утилиты вроде jstack и jmap. Или вы не мониторите и не профилируете приложения в продакшне?


    1. gmandnepr Автор
      25.06.2019 12:40

      У образа около 5к загрузок, подскажите, как у него с поддержкой и обновлениями? Потенциально может быть хорошей альтернативой скажем adoptopenjdk/openjdk11:alpine-jre.

      Касательно мониторинга (предполагая что все это запущено в kubernetes): для метрик приложения (память, CPU) я бы вытаскивал их из подов при помощи heapster/grafana/influxdb, для бизнес метрик я бы строил дашборды на кибане используя elastic-stack. Оба подхода можно подключить используя daemonsets что позволит избежать изменений в самих сервисах и приложениях. Как вы верно заметили, ограничение этих подходов в том что JVM метрики мы не собираем.

      Во многих случаях аномольно большое использование CPU либо памяти привлечет внимание, но вы правы, может понадобится больше диагностической информации, в часности о JVM.


      1. apangin
        25.06.2019 13:10

        подскажите, как у него с поддержкой и обновлениями
        Лучше, чем у кого-либо, насколько мне известно. Впрочем, не хочу заниматься рекламой, пусть лучше alexbel сам обо всём расскажет :)


      1. gecube
        25.06.2019 18:07

        Я инжектил в jvm jmx exporter, дальше вытащить инфу на уровень кластера и в прометеус не проблема.


      1. SimSonic
        25.06.2019 19:07

        Я использую этот как базовый: https://hub.docker.com/r/bellsoft/liberica-openjdk-alpine-musl
        Ту версию, что по умолчанию (лайт, 100 мб). Spring boot 2, постгря/оракл, полёт нормальный.


        1. gecube
          26.06.2019 08:27

          musl проблемы не вызывал?
          Я как минимум хапнул несовместимость ffmpeg с ним /это вне контекста Java, если что/


          1. SimSonic
            26.06.2019 08:47

            Нет, не вызывал. Долго всматривался, как приложение работает, никаких проблем не увидел. Но, конечно же, тестировать и проверять надо внимательно.
            И я не понял отличие от просто -alpine сборки, там же тоже должен быть по идее musl.


  1. gmandnepr Автор
    25.06.2019 12:38

    del, промахнулся веткой


  1. gecube
    25.06.2019 15:54

    Просто тут я наблюдаю ту же историю, что и с Python — чем больше модулей использует приложение — тем меньше выигрыш от кастомного образа. Насколько действительно образ 422МБ хуже, чем 144МБ? Может проблема в голове разработчика? Потому что снижая размер образа он усложняет процесс сборки и поддержки (нужно внимательно следить за тем, какие зависимости нужно в образ докинуть, чтобы приложение запустилось). Т.е. все-таки каков экономический эффект от уменьшения размера? Кто сможет объяснить?


    1. gmandnepr Автор
      25.06.2019 17:16

      Я бы предложил оценивать с точки зрения, какие проблемы образ решает лучше или хуже?

      • запустить приложение: решают одинаково, при условии что мы протестировали и приложение запускается
      • простота сборки: меньший образ хуже так как его сложнее приготовить
      • колличество денег что компания заработает: вероятно тоже не влияет
      • security: возможно лучше иметь только то что необходимо и изолировать себя опасности от того что не нужно, но есть в образе
      • скорость запуска на новой ноде k8s которая была только что создана на aws spot instance: меньший образ, вероятно, загрузится быстрее


      1. gecube
        25.06.2019 17:22
        -1

        Я только с последним пунктом согласен. Но и то не факт, т.к. общие слои уже почти наверняка лежат в кэше на ноде кубера. Если, конечно, базовый образ не поменялся. И это не зависит от политики скачивания (Always Pull Policy — оно ес-но кэш не отключает).


        По безопасности — тоже как бы не сильно видно профита. Ну, будет не 20 компонентов в образе, а 10. Все равно строить пайплайн проверки образов на уязвимости. Да, конечно, в случае, если в 20-м модуле, который не используется найдена уязвимость, то мелкий образ (который на 10 нужных) без него пересобирать не придется. Но это спорное преимущество.


  1. splix
    25.06.2019 18:56
    +1

    Безотносительно размера докер образа, советую посмотреть на Jib github.com/GoogleContainerTools/jib Это инструмент упаковки Java приложений в Docker, поддерживает Spring Boot и выбор базовых образов.

    Не уверен насчет JDK custom runtime, но у него много других преимуществ. Например он складывает внешние библиотеки, код приложения и ресурсы приложения в разные слои. Поэтому размер обновления, т.е. слоя который по факту нужно скачать на сервере, обычно равен размеру вашего кода.