Ускорение Maven сборки в Docker

Kate

Administrator
Команда форума

Исходный уровень​

Чтобы оценить исходный уровень, нам нужен образец проекта. Я создал для этой цели относительно небольшой Kotlin проект.

Вот соответствующий Dockerfile:

Dockerfile
Dockerfile
  1. Начните с образа JDK для этапа упаковки
  2. Добавьте необходимые ресурсы
  3. Создайте JAR
  4. Начните с JRE для шага создания образа
  5. Скопируйте JAR с предыдущего шага
  6. Установите точку входа
Выполним сборку:

e68c42e9e920a53e071a4eb74ac320e9.png

  1. Пока забудьте пока о переменной окружения, о ней я расскажу в следующем разделе.
Вот результаты пяти прогонов:

* 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 выглядит так:

Dockerfile
Dockerfile
  1. Maven цель go-offline загружает все зависимости и плагины
  2. На данный момент доступны все зависимости
ПРИМЕЧАНИЕ: 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 позволяет монтировать тома во время сборки (а не только во время выполнения). Доступно несколько типов монтирования, но нас интересует монтирование кэша. Это экспериментальная функция, поэтому вам нужно явно ее указать:

Dockerfile
Dockerfile
  1. Подключаетесь к экспериментальным функциям
  2. Сборка с использованием кэша
Пришло время запустить сборку:

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. Давайте соответственно изменим нашу сборку:

Dockerfile
Dockerfile
  1. Загрузить последнюю версию демона Maven
  2. Обновить индекс пакета
  3. Установить unzip
  4. Создать специальную папку
  5. Распаковать архив, который мы скачали на шаге <1>.
  6. Переместить содержимое извлеченного архива в ранее созданную папку
  7. Использовать 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.Dockerfile
mvnd.Dockerfile
Dockerfile
Dockerfile
Такой подход существенно меняет результат.

Команда mvnd хороша только тогда, когда демон запущен в течение нескольких запусков. Я не нашел способа сделать это с помощью Docker. Если у вас есть идеи, как этого добиться, скажите, пожалуйста, мне.

Вот сводка всех времен выполнения:

b403dc77b193bca7af7f990ee9fb2f6c.png

Заключение​

Повышение производительности сборок Maven внутри Docker сильно отличается от обычных сборок. В Docker ограничивающим фактором является скорость загрузки зависимостей. Если вы застряли на старой версии, вам нужно использовать слои для кеширования зависимостей.

С BuildKit я рекомендую использовать новую возможность монтирования кэша, чтобы избежать загрузки всех зависимостей, если уровень недействителен.

Полный исходный код этого поста можно найти на Github.

 
Сверху