Узнайте, как сделать сборки Maven более быстрыми и эффективными
Для сборки требуется несколько свойств, главное из которых - воспроизводимость.
Я считаю, что скорость должна быть ниже в порядке приоритета. Тем не менее, это также один из самых лимитирующих факторов на ваш цикл выпуска: если ваша сборка занимает T, вы не можете отпустить быстрее, чем каждый из T.
Следовательно, вы, вероятно, захотите ускорить свои сборки после достижения определенного уровня зрелости, чтобы выпускать более частые релизы.
В этом посте я хочу подробно рассказать о некоторых методах, которые вы можете использовать для ускорения Maven сборки. Следующий пост будет посвящен тому, как сделать то же самое внутри Docker.
Исходный уровень
Поскольку я хочу предложить методы и оценить их влияние, нам нужен образец репозитория.
Я выбрал примеры кода Hazelcast, потому что он обеспечивает достаточно большую многомодульную кодовую базу с множеством подмодулей; точный коммит - 448febd.
Порядок действий следующий:
Я запускаю команду пять раз, чтобы избежать временных проблем
Я выполняю
mvn clean
перед каждым запуском, чтобы начать с пустогоtarget
репозиторияВсе зависимости и плагины уже загружены
-
Я сообщаю время, которое Maven отображает в журнале консоли:
[INFO] ------------------------------------------------------- [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------- [INFO] Total time: 22.456 s (Wall Clock) [INFO] Finished at: 2021-09-24T23:20:41+02:00 [INFO] -------------------------------------------------------
Начнем с нашего исходного уровня, выполнив команду: mvn test
. Результат:
02:00 мин.
01:57 мин.
01:58 мин.
01:56 мин.
01:58 мин.
Использование всех процессоров
По умолчанию Maven использует один поток. В век многоядерных процессоров это пустая трата времени. Можно запускать параллельные сборки с использованием нескольких потоков, задав абсолютное число или число относительно количества доступных ядер. Для получения дополнительной информации, пожалуйста, посмотрите соответствующую документацию.
Чем больше у вас подмодулей, которые не зависят друг от друга, т.е. Maven может выполнять их сборку параллельно, тем лучше результат вы получите с помощью этой техники.
Это очень хорошо работает на нашей кодовой базе.
Мы собираемся использовать столько потоков, сколько доступно ядер. Соответствующая команда Maven: mvn test -T 1C
.
При запуске этой команды вы должны увидеть в консоли следующее сообщение:
Using the MultiThreadedBuilder implementation with a thread count of X
51.487 s (Wall Clock)
40.322 s (Wall Clock)
52.468 s (Wall Clock)
41.862 s (Wall Clock)
41.699 s (Wall Clock)
Цифры намного лучше, но с большей дисперсией.
Параллельное выполнение теста
Распараллеливание - отличный метод. Мы можем сделать то же самое в отношении выполнения тестов. По умолчанию плагин Maven Surefire запускает тесты последовательно, но можно настроить его для параллельного запуска тестов. Пожалуйста, обратитесь к документации за полным набором опций.
Этот подход отлично подходит, если у вас есть большое количество юнитов в каждом модуле. Обратите внимание, что ваши тесты должны быть независимыми друг от друга.
Мы вручную зададим количество потоков:
mvn test -Dparallel=all -DperCoreThreadCount=false -DthreadCount=16 #1 #2
Настройте Surefire для параллельного запуска классов и методов
Ручное изменение количества потоков до 16
Запустим и получим:
02:04 мин.
02:03 мин.
01:46 мин
01:52 мин
01:53 мин.
Похоже, что стоимость синхронизации потоков компенсирует потенциальную выгоду от выполнения параллельных тестов.
Офлайн
Maven будет проверять, есть ли у SNAPSHOT
зависимости новая «версия» при каждом запуске. Это означает дополнительные сетевые запросы. Мы можем предотвратить эту проверку с помощью опции --offline
.
Хотя вам следует избегать SNAPSHOT
зависимостей, иногда это неизбежно, особенно во время разработки.
Команда mvn test -o
, -o
является сокращением для --offline
.
01:46 мин
01:46 мин
01:47 мин
01:55 мин.
01:44 мин
Кодовая база имеет большое количество SNAPSHOT
зависимостей; следовательно, автономный режим значительно ускоряет сборку.
Параметры JVM
Сам Maven - это приложение на основе Java. Это означает, что каждый запуск запускает новую JVM. JVM сначала интерпретирует байт-код, а затем анализирует рабочий код и соответственно компилирует байт-код в нативный код: это обеспечивает максимальную производительность, но только после длительного времени сборки. Это отлично подходит для длительно выполняющихся процессов, а не для приложений командной строки.
Скорее всего, мы не достигнем максимальной производительности в контексте сборок, поскольку они относительно недолговечны, но мы по-прежнему расплачиваемся за время анализа.
Мы можем настроить Maven, чтобы отказаться от него, настроив соответствующие параметры JVM. Доступно несколько способов настройки JVM. Самый простой способ - создать специальный jvm.config
файл конфигурации во вложенной папке .mvn
проекта.
-XX: -TieredCompilation -XX: TieredStopAtLevel = 1
Давайте теперь просто запустим mvn test
:
01:44 мин
01:44 мин
01:53 мин.
01:53 мин.
01:55 мин.
Maven Daemon
Демон Maven является недавним дополнением к экосистеме Maven. Он инспирирован демоном Gradle :
Gradle работает на виртуальной машине Java (JVM) и использует несколько вспомогательных библиотек, для которых требуется значительное время инициализации.
В результате это иногда запуск может оказаться немного замедленным. Решением этой проблемы является Gradle Daemon: долгосрочный фоновый процесс, который выполняет ваши сборки намного быстрее, чем без него. Это достигается исключая дорогостоящий процесс начальной загрузки и используя кеширование для сохранения данных о вашем проекте в памяти.
Команда Gradle осознала, что инструмент командной строки - не лучшее применение JVM. Чтобы решить эту проблему, нужно постоянно поддерживать фоновый процесс JVM, используя демон. Он работает как сервер, а интерфейс командной строки играет роль клиента.
В качестве дополнительного преимущества этот длительный процесс загружает классы только один раз (если они не менялись между запусками).
После установки программного обеспечения вы можете запустить демон с помощью mvnd
команды вместо стандартной mvn
. Вот результаты выполнения команды: mvnd test
:
33.124 s (Wall Clock)
33.114 s (Wall Clock)
34.440 s (Wall Clock)
32.025 s (Wall Clock)
29.364 s (Wall Clock)
Обратите внимание, что по умолчанию демон использует несколько потоков с расширением number of cores - 1
.
Объединение методов
Мы видели несколько способов ускорить сборку. Что, если бы мы использовали их вместе?
Давайте сначала попробуем использовать все методы, которые мы видели до сих пор, в одном и том же прогоне:
mvnd test -Dparallel=all -DperCoreThreadCount=false -DthreadCount=16 -o #1 #2 #3 #4
Используем демон Maven
Запускаем тесты параллельно
Не обновляем
SNAPSHOT
зависимостиНастраиваем параметры JVM, как указано выше, через
jvm.config
файл - нет необходимости устанавливать какие-либо параметры
Команда возвращает следующие результаты:
27.061 s (Wall Clock)
24.457 s (Wall Clock)
24.853 s (Wall Clock)
25.772 s (Wall Clock)
Давайте вспомним, что демон Maven - это длительный процесс. По этой причине разумно позволить JVM проанализировать и скомпилировать байт-код в нативный код. Таким образом, мы можем удалить jvm.config
файл и повторно запустить указанную выше команду. Результаты:
23.840 s (Wall Clock)
26.589 s (Wall Clock)
22.283 s (Wall Clock)
23.788 s (Wall Clock)
22.456 s (Wall Clock)
Теперь мы можем отобразить сводные результаты:
Baseline |
Parallel Build |
Parallel Tests |
Offline |
Jvm Params |
Daemon |
Daemon + Offline + Parallel Tests + Parameters |
Daemon + Offline + Parallel Tests |
|
#1 (s) |
120.00 |
51.00 |
128.00 |
106.00 |
104.00 |
33.12 |
27.06 |
23.84 |
#2 (s) |
117.00 |
40.00 |
123.00 |
106.00 |
104.00 |
33.11 |
24.46 |
26.59 |
#3 (s) |
118.00 |
52.00 |
106.00 |
107.00 |
113.00 |
34.44 |
24.85 |
22.28 |
#4 (s) |
116.00 |
42.00 |
112.00 |
115.00 |
113.00 |
32.03 |
25.77 |
23.79 |
#5 (s) |
118.00 |
42.00 |
113.00 |
104.00 |
115.00 |
29.36 |
22.46 |
|
Average (s) |
*117.80* |
45.40 |
116.40 |
107.60 |
109.80 |
32.41 |
25.54 |
23.79 |
Deviation |
1.76 |
25.44 |
63.44 |
14.64 |
22.96 |
2.91 |
1.00 |
2.38 |
Gain from baseline (s) |
- |
72.40 |
1.40 |
10.20 |
8.00 |
85.39 |
92.26 |
94.01 |
% gain |
- |
61.46% |
1.19% |
8.66% |
6.79% |
72.48% |
78.32% |
79.80% |
Заключение
В этом посте мы увидели несколько способов ускорить сборку Maven. Вот их краткое изложение:
Демон Maven: надежная и безопасная отправная точка
Распараллеливание сборки: когда сборка содержит несколько модулей, независимых друг от друга.
Распараллеливание тестов: когда проект содержит несколько тестов.
Автономность: когда проект содержит
SNAPSHOT
зависимости и вам не нужно их обновлять.Параметры JVM: когда вы хотите сделать все возможное
Я бы посоветовал каждому пользователю начать использовать демон Maven и продолжить оптимизацию при необходимости и в зависимости от вашего проекта.
В следующем посте мы сосредоточимся на ускорении ваших сборок Maven в контейнере.
Чтобы продвинуться дальше, прочтите:
Комментарии (6)
novoselov
11.10.2021 13:05Планируется еще Incremental Build (подробнее в cache.md)
Если вкратце, то после сборки результат сохраняется в общий кеш, который можно использовать как на CI, так и локально. Если у вас большой проект, то пересобираться будут только ваши изменения.
sshikov
11.10.2021 17:50+1>Распараллеливание сборки: когда сборка содержит несколько модулей, независимых друг от друга.
К сожалению, не все плагины это поддерживают. А трудозатраты на то, чтобы сделать плагин поддерживающим, могут быть достаточно высоки.
amarkevich
11.10.2021 20:23Как я однажды неожиданно ускорил сборку мавеном на Google Cloud Build: изначально использовался образ из докер хаба. Но иногда сборка падала с ошибкой "Connection reset" от central репозитория. Так как проект содержит в основном Spring Boot микросервисы, то был сделан образ на основе официального после прогона
RUN mvn dependency:resolve compiler:help resources:help jar:help surefire:help help:help
Образ пересобирается при переходе на новую версию Spring Boot. По итогу сборка образа микросервиса с тестами занимает в районе минуты.
VasiliyKudryavtsev
12.10.2021 10:21+4Могу добавить еще пару советов:
1) Довольно очевидная вещь, но многие почему-то пренебрегают. Обновляйте maven! Он развивается не такими быстрыми темпами как gradle (может это и хорошо), но почти в каждой версии есть улучшения производительности.
На большом проекте версия 3.6.3 заметно быстрее, чем 3.6.0, а 3.8.3 - еще быстре
2) Иногда использовать --offline может быть неудобно, когда нет уверенности что нужные артефакты уже скачаны в локальный репозиторий. Но есть замечательный ключ --nsu (no snapshot update) который позволит скачать недостающие артефакты и не полезет за обновление снепшотов
3) Еще немного можно выиграть если отключить раскраску вывода (ключ -B или --batch-mode)
4) Также ускорение сборки за счет оптимизации вывода в консоль можно получить используя takari concurrent-build-logger (GitHub - takari/concurrent-build-logger: Better Maven multithreaded build logging) .
Но ускорение - это только приятный побочный эффект. На самом деле плагин решает проблему "перемешивания" вывода при параллельной сборке, так что это настоящий must have.
В отличие от других плагинов от такари он не требует переделок и тонкой настройки сборки.
5) Эффективность параллельной сборки очень сильно зависит от того, как у вас устроены зависимости между модулями. Если какой-то модуль нужен для сборки остальных, то его сборка превратиться в бутылочное горлышко.
Один из частных приемов, которые можно использовать в данном случае - вынести тесты этого модуля в отдельный модуль, чтобы, как только будет завершена компиляция, maven мог начать заниматься зависимыми модулями.
6) И, напоследок, совет, который по эффективности бывает сравним с включением maven daemon и использование параллельной сборки. Если вы пользуетесь антивирусом, то настройте в нем исключения для
Процесса Java
Папки JDK
Папки с исходниками
Папки локального репозитория maven
tsypanov
20.10.2021 10:33При сборке на Дженкинсе и локально немного помогает свойство
--no-transfer-progress
, отключающее вывод сообщение о загрузке зависимостей и уменьшающее объём логов.Что касается распараллеливания, то, ИМХО, предпочтительнее делать это с помощью JUnit 5, из коробки поддерживающего параллелизм. Нужно добавить в `test/resources` файл
junit-platform.properties
junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = same_thread junit.jupiter.execution.parallel.mode.classes.default = concurrent
Если тесты мешают друг другу, то они группируются в наборы с помощью подобного кода:
@RunWith(JUnitPlatform.class) @SelectClasses({ }) public class SystemTestSuite { }
chemtech
А если кратко, то запускайте Maven Daemon.