Ранее я описал различные методы ускорения ваших Maven сборок.
Сегодня я хотел бы расширить их область применения и сделать то же самое для сборок Maven внутри Docker.
Между каждым запуском мы меняем исходный код, добавляя одну пустую строку; между каждым разделом мы удаляем все построенные образы, в том числе промежуточные, являющиеся результатами многоступенчатой сборки. Идея состоит в том, чтобы избежать повторного использования ранее созданного образа.
Исходный уровень
Чтобы оценить исходный уровень, нам нужен образец проекта. Я создал для этой цели относительно небольшой Kotlin проект.
Вот соответствующий Dockerfile
:
Начните с образа JDK для этапа упаковки
Добавьте необходимые ресурсы
Создайте JAR
Начните с JRE для шага создания образа
Скопируйте JAR с предыдущего шага
Установите точку входа
Выполним сборку:
Пока забудьте пока о переменной окружения, о ней я расскажу в следующем разделе.
Вот результаты пяти прогонов:
* 0.36s user 0.53s system 0% cpu 1:53.06 total
* 0.36s user 0.56s system 0% cpu 1:52.50 total
* 0.35s user 0.55s system 0% cpu 1:56.92 total
* 0.36s user 0.56s system 0% cpu 2:04.55 total
* 0.38s user 0.61s system 0% cpu 2:04.68 total
Buildkit для победы
В последней командной строке использовалась переменная среды DOCKER_BUILDKIT
. Это способ указать Docker использовать устаревший движок. Если вы какое-то время не обновляли Docker, значит, вы используете этот движок. В настоящее время BuildKit заменил его и стал новым по умолчанию.
BuildKit обеспечивает несколько улучшений производительности:
Автоматический сбор мусора
Разрешение параллельных зависимостей
Эффективное кеширование инструкций
Импорт/экспорт кэша сборки
и т.п.
Повторим предыдущую команду на новом движке:
time docker build -t fast-maven:1.1 .
Вот выдержка из вывода журнала после первого запуска:
...
=> => transferring context: 4.35kB
=> [build 2/6] COPY .mvn .mvn
=> [build 3/6] COPY mvnw .
=> [build 4/6] COPY pom.xml .
=> [build 5/6] COPY src src
=> [build 6/6] RUN ./mvnw -B package
...
0.68s user 1.04s system 1% cpu 2:06.33 total
Следующие выполнения той же команды имеют несколько другой результат:
...
=> => transferring context: 1.82kB
=> CACHED [build 2/6] COPY .mvn .mvn
=> CACHED [build 3/6] COPY mvnw .
=> CACHED [build 4/6] COPY pom.xml .
=> [build 5/6] COPY src src
=> [build 6/6] RUN ./mvnw -B package
...
Напоминаю, что мы меняем исходный код между запусками. Файлы, которые мы не меняем, а именно .mvn
, mvnw
и pom.xml
, кэшируются BuildKit. Но эти ресурсы невелики, поэтому кеширование не приводит к значительному сокращению времени сборки.
* 0.69s user 1.01s system 1% cpu 2:05.08 total
* 0.65s user 0.95s system 1% cpu 1:58.51 total
* 0.68s user 0.99s system 1% cpu 1:59.31 total
* 0.64s user 0.95s system 1% cpu 1:59.82 total
Быстрый просмотр журналов показывает, что самым узким местом в сборке является загрузка всех зависимостей (включая плагины). Это происходит каждый раз, когда мы меняем исходный код. Вот почему BuildKit не улучшает производительность.
Слои, слои, слои
Мы должны сосредоточить наши усилия на зависимостях. Для этого мы можем использовать слои и разделить сборку на два этапа:
На первом этапе скачиваем зависимости
Во втором мы создаем нужные пакеты
Каждый шаг создает слой, второй зависит от первого.
При использовании слоев, если мы изменим исходный код во втором слое, это не повлияет на первый слой, и его можно будет использовать повторно. Нам больше не нужно загружать зависимости. Новый Dockerfile
выглядит так:
Maven цель
go-offline
загружает все зависимости и плагиныНа данный момент доступны все зависимости
ПРИМЕЧАНИЕ: go-offline
не загружает все. Команда не запустится успешно, если вы попытаетесь использовать опцию -o
(в автономном режиме). Это известный старый баг. Во всех случаях это «достаточно хорошо».
Запустим сборку:
time docker build -t fast-maven:1.2 .
Первый запуск занимает значительно больше времени, чем базовый:
0.84s user 1.21s system 1% cpu 2:35.47 total
Однако последующие сборки выполняются намного быстрее. Изменение исходного кода влияет только на второй уровень и не запускает загрузку (большинства) зависимостей:
* 0.23s user 0.36s system 5% cpu 9.913 total
* 0.21s user 0.33s system 5% cpu 9.923 total
* 0.22s user 0.38s system 6% cpu 9.990 total
* 0.21s user 0.34s system 5% cpu 9.814 total
* 0.22s user 0.37s system 5% cpu 10.454 total
Монтирование тома в сборке
Слои сборки значительно сократили время сборки. Мы можем изменить исходный код и сохранить его на низком уровне. Однако остается одна проблема. Изменение одной зависимости делает слой не валидным, поэтому нам нужно снова загрузить все зависимости.
К счастью, BuildKit позволяет монтировать тома во время сборки (а не только во время выполнения). Доступно несколько типов монтирования, но нас интересует монтирование кэша. Это экспериментальная функция, поэтому вам нужно явно ее указать:
Подключаетесь к экспериментальным функциям
Сборка с использованием кэша
Пришло время запустить сборку:
time docker build -t fast-maven:1.3 .
Время сборки выше, чем для обычной сборки, но все же ниже, чем для сборки слоев:
0.71s user 1.01s system 1% cpu 1:50.50 total
Следующие сборки соответствуют слоям:
* 0.22s user 0.33s system 5% cpu 9.677 total
* 0.30s user 0.36s system 6% cpu 10.603 total
* 0.24s user 0.37s system 5% cpu 10.461 total
* 0.24s user 0.39s system 6% cpu 10.178 total
* 0.24s user 0.35s system 5% cpu 10.283 total
Однако, в отличие от слоев, нам нужно загрузить только обновленные зависимости. Здесь давайте изменим версию Kotlin с 1.5.30
на 1.5.31
:
pom.xml
<properties>
<kotlin.version>1.5.31</kotlin.version>
</properties>
Это огромное улучшение относительно времени сборки:
* 0.41s user 0.57s system 2% cpu 44.710 total
Рассмотрим использование демона Maven
В предыдущем посте, касающемся обычных сборок Maven, я упомянул демон Maven. Давайте соответственно изменим нашу сборку:
Загрузить последнюю версию демона Maven
Обновить индекс пакета
Установить unzip
Создать специальную папку
Распаковать архив, который мы скачали на шаге <1>.
Переместить содержимое извлеченного архива в ранее созданную папку
Использовать mvnd вместо оболочки Maven
Теперь запустим сборку:
docker build -t fast-maven:1.4 .
Журнал выводит следующее:
* 0.70s user 1.01s system 1% cpu 1:51.96 total
* 0.72s user 0.98s system 1% cpu 1:47.93 total
* 0.66s user 0.93s system 1% cpu 1:46.07 total
* 0.76s user 1.04s system 1% cpu 1:50.35 total
* 0.80s user 1.18s system 1% cpu 2:01.45 total
Существенного улучшения по сравнению с исходным уровнем нет.
Я попытался создать специальный mvnd
образ и использовать его как родительский:
Такой подход существенно меняет результат.
Команда mvnd
хороша только тогда, когда демон запущен в течение нескольких запусков. Я не нашел способа сделать это с помощью Docker. Если у вас есть идеи, как этого добиться, скажите, пожалуйста, мне.
Вот сводка всех времен выполнения:
Заключение
Повышение производительности сборок Maven внутри Docker сильно отличается от обычных сборок. В Docker ограничивающим фактором является скорость загрузки зависимостей. Если вы застряли на старой версии, вам нужно использовать слои для кеширования зависимостей.
С BuildKit я рекомендую использовать новую возможность монтирования кэша, чтобы избежать загрузки всех зависимостей, если уровень недействителен.
Полный исходный код этого поста можно найти на Github.
ggo
Чтобы эмулировать поведение maven-демона в докере надо запустить контейнер и не останавливать его. И затем при билде подсовывать ему исходники и вызывать в нем package. Имхо, лучше просто тупо в шеле звать mvnw.
зы, параллельные билды опять же не запустишь, имхо, докер лучше не использовать для этой задачи