Готовим докерфайл для быстрой сборки JAR используя Maven
Рассмотрим пример maven-проекта из sping guides. Он имеет следующую структуру:
├── mvnw ├── mvnw.cmd ├── pom.xml ├── src ├── main │ └── java │ └── hello │ ├── Greeter.java │ └── HelloWorld.java ├── test ├── java ├── hello └── GreeterTest.java
Самый банальный Dockerfile «на скорую руку» может выглядеть следующим образом:
Рассмотрим способы оптимизации.
1. Шаг перый: Кешируем зависимости
Как правило, в процессе активной разработки, список зависимостей меняется реже чем бизнес-логика и тесты. Воспользуемся этим знанием: вынесем dependencies в отдельной слой и расположим его до слоя COPY src src
. Согласно идеологии docker layers, изменение слоя инвалидирует только те слои, которые расположены после данного, поэтому изменение содержимого директории src не будет инвалидировать слой зависимостей.
Осталось теперь только придумать, как выделить dependencies в отдельный слой. В качестве решения, можно сначала скормить мавену голый pom.xml без директории src, а позже скопмилировать проект, используя уже имеющиеся зависимости:
Согласно maven build lifecycle, mvn verify выполнит фазы validate, compile, test, package и сам verify. Поскольку на данном этапе в директории /build есть только pom.xml — сам исходный код скомпилирован не будет, но все зависимости из pom.xml будут установлены в локальный репозиторий ~/.m2.
Следующими двумя слоями мы копируем директорию src и компилируем jar-файл командой mvn package. Большинство зависимостей будет зачитано мавеном из локального репозитория.
Исследуем слои промежуточного stage образа командой dive:
0 B WORKDIR /build 1.6 kB COPY pom.xml . 18 MB RUN /bin/sh -c mvn --fail-never verify 715 B COPY src src 682 kB RUN /bin/sh -c mvn package
Итого, на данном этапе мы добились следующего поведения:
- изменение директории src (частое действие) приводит к инвалидации только двух последних двух слоев, «тяжелые» слои, расположенные ДО — остаются в кеше
- изменение файла pom.xml (редкое действие) приводит к инвалидации последних четырех слоев
2. Шаг второй: Исключаем тесты
Если выполнение тестов при сборке не является частью вашего CI/CD, их можно пропустить, используя флаг -DskipTests
. Наличие этого флага позволяет пропустить выполнение тестов, однако не исключает тесты из компиляции. Полный отказ от тестов реализуется при помощи двух флагов -Dmaven.test.skip -DskipTests
.
Теперь, когда тесты исключены, maven больше не нуждается в плагине Maven Surefire Plugin, который используется на этапе mvn test. Поскольку, это единственный артифакт, который добавлялся в локальный репозиторий в слое RUN mvn package (легко проверить командой dive), теперь мы можем использовать флаг -o
, который запрещает обращение к remote репозиториям и вынуждает мавен читать локальный репозиторий:
Сборка стала заметно быстрее, а размер последнего слоя stage=build снизился с 682 до 634kB (проверьте ваши слои самостоятельно командой dive или docker history). Благодаря флагу -o
, все зависимости зачитаны мавеном из локального репозитория.
3. Шаг третий: Используем параллельную сборку
Maven версии 3+ позволяет использовать parallel builds:
mvn -T 4 clean install # Builds with 4 threads mvn -T 1C clean install # 1 thread per cpu core mvn -T 1.5C clean install # 1.5 thread per cpu core
Распараллелим сборку по 10 потоков на каждое ядро, доступное докеру:
Мы рассмотрели несколько способов оптимизации maven-сборки в экосистеме докера. В промышленных масштабах, подобная оптимизация экономит уйму времени ожидания, как ускоряя CI/CD pipeline, так и уменьшая биллинг за процессорное время.
Подпишись на телеграм «чевопса»
Девопс глазами программиста 👨💻
Практические примеры работы с Docker 🐳, Kubernetes ☸️ и облаками ☁️