Узнайте, как сделать сборки 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
  1. Настройте Surefire для параллельного запуска классов и методов

  2. Ручное изменение количества потоков до 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
  1. Используем демон Maven

  2. Запускаем тесты параллельно

  3. Не обновляем SNAPSHOT зависимости

  4. Настраиваем параметры 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)


  1. chemtech
    11.10.2021 13:01
    +1

    А если кратко, то запускайте Maven Daemon.


  1. novoselov
    11.10.2021 13:05

    Планируется еще Incremental Build (подробнее в cache.md)

    Если вкратце, то после сборки результат сохраняется в общий кеш, который можно использовать как на CI, так и локально. Если у вас большой проект, то пересобираться будут только ваши изменения.


  1. sshikov
    11.10.2021 17:50
    +1

    >Распараллеливание сборки: когда сборка содержит несколько модулей, независимых друг от друга.
    К сожалению, не все плагины это поддерживают. А трудозатраты на то, чтобы сделать плагин поддерживающим, могут быть достаточно высоки.


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


  1. 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


  1. 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 {
    
    }