Предыстория
Уже довольно давно я работаю в проекте, в котором используется система сборки Maven. По началу, когда проект был ещё не таких размеров как сейчас, время его полной компиляции было относительно разумным и не вызывало нареканий. Но со временем код разросся, количество подпроектов резко увеличилось и среднее время полной компиляции выросло до 6 — 10 минут. Что служило постоянным источником упрёков со стороны разработчиков.
Следует так же отметить. что мы не использовали параллельную сборку, т.к. это регулярно вызывало различные проблемы. То артефакты в локальном хранилище побьются, то просто соберёт не в том порядке и в финальный WAR артефакт попадёт старый, неперекомпилированный код. Конечно некоторые разработчики использовали параллельную сборку на свой страх и риск. Но рано или поздно попадали в ситуацию, когда не могли разобраться что происходит. И простая перекомпиляция в один поток сразу помогала.
Так продолжалось довольно долго, пока я не наткнулся на довольно любопытный сайт компании Takari, на котором предлагаются способы по усовершенствованию методов работы с Maven.
Самыми интересными там являются три вещи:
Так же на GitHub у них выложен Maven Wrapper (аналог враппера из Gradle).
Забегая вперёд отмечу, что описываемые здесь инструменты не только решают проблему некорректной работы Maven, но и дают существенный прирост скорости сборки.
Concurrent Safe Local Repository
Данное усовершенствование предназначено для решения проблемы битых артефактов в локальном репозитарии.
Дело в том, что в Maven работа с локальным хранилищем (по сути каталогом на файловой системе) реализована непотокобезопасным способом. Т.е. если параллельно собирающиеся проекты начинают одновременно выкачивать одну и ту же зависимость, то в результате получается битый файл. Вот именно эту проблему и решает данное дополнение.
Для того, чтобы им воспользоваться, необходимо модифицировать непосредственно сам установленный Maven:
curl -O https://repo1.maven.org/maven2/io/takari/aether/takari-local-repository/0.10.4/takari-local-repository-0.10.4.jar
mv takari-local-repository-0.10.4.jar $M2_HOME/lib/ext
curl -O https://repo1.maven.org/maven2/io/takari/takari-filemanager/0.8.2/takari-filemanager-0.8.2.jar
mv takari-filemanager-0.8.2.jar $M2_HOME/lib/ext
Всё. Больше никаких дополнительный действий не требуется. Теперь все операции с локальным репозитарием будут безопасными. Само по себе это расширение может использоваться разве что на серверах CI, когда множество сборок происходит одновременно и хочется использовать один репозитарий для экономии места. Но для рядового разработчика более интересно использование совместно со Smart Builder, который работает исходя из предположения, что данное расширение уже установлено.
Как показал опыт, при использовании этого решения сборка начинает работать чуть медленнее, но зато более надёжно.
Takari Smart Builder
Это расширение устанавливается аналогично предыдущему:
curl -O https://repo1.maven.org/maven2/io/takari/maven/takari-smart-builder/0.4.0/takari-smart-builder-0.4.0.jar
mv takari-smart-builder-0.4.0.jar $M2_HOME/lib/ext
И обеспечивает более продвинутый алгоритм распараллеливания сборки проектов Maven. Разница в работе стандартного планировщика сборки Maven и Smart Builder иллюстрирует диаграмма ниже:
Стандартная стратегия распараллеливания Maven проста и наивна. Она базируется на вычислении глубины зависимостей. Maven запускает параллельную сборку всех проектов одного уровня, пока они не закончатся и только потом переходит на следующий уровень.
Takari Smart Builder, в свою очередь, использует более продвинутую стратегию. Он вычисляет цепочки зависимостей, производит топологическую сортировку и только после этого принимает решение о том, в какой последовательности необходимо производить сборку проектов.
Более того. В процессе компиляции, он запоминает время компиляции каждого проекта в файл .mvn/timing.properties и использует его как дополнительную информацию для того, чтобы в следующий раз завершить компиляцию как можно быстрее.
Для того, чтобы воспользоваться этим функционалом, необходимо при запуске Maven указать дополнительный ключ. Например:
mvn clean install --builder smart -T1.0C
Всё становится проще с Maven 3.3.1
В версии Maven 3.3.1 было реализовано несколько нововведений. Первое и самое главное — возможность объявлять расширения ядра Maven прямо в проекте. Для этого нужно добавить файл .mvn/extensions.xml. В приложении к описанному раньше, этот файл может иметь следующий вид:
<?xml version="1.0" encoding="UTF-8"?>
<extensions>
<extension>
<groupId>io.takari.maven</groupId>
<artifactId>takari-smart-builder</artifactId>
<version>0.4.1</version>
</extension>
<extension>
<groupId>io.takari.aether</groupId>
<artifactId>takari-local-repository</artifactId>
<version>0.11.2</version>
</extension>
</extensions>
Теперь нам не нужно докладывать библиотеки непосредственно в дистрибутив Maven. При этом мы получаем тот же результат.
Файл extensions.xml — не единственный возможный в каталоге .mvn. Тут могут располагаться ещё два файла: jvm.config и maven.config.
jvm.config содержит параметры JVM для запуска компиляции текущего проекта. Например этот файл может выглядеть следующим образом:
-Xmx2g -XX:+TieredCompilation -XX:TieredStopAtLevel=1
Первая опция задаёт размер heap равный 2 Гб, а следующие две — оптимизируют работу JVM под нужды Maven (подсмотрено тут).
maven.config — ещё один файл с параметрами, но на этот раз для самого Maven. Например:
--builder smart -T1.0C -e
Таким образом мы можем задать, что по умолчанию используется smart builder с количеством потоков равному количеству логических ядер. Т.е., если мы просто выполним
mvn clean install
то сборка будет выполняться в несколько потоков и с использование всех расширений и оптимизаций. Причём, если даже мы будем выполнять сборку вложенного модуля, то эти настройки всё-равно будут применены, т.к. Maven производит поиск каталога .mvn не только в текущем каталоге, но и в родительских.
Тут, правда есть один нюанс. Т.к. сборка идёт в несколько потоков, то и лог сборки выводится этими потоками конкурентно. В результате, при возникновении проблем, не всегда бывает понятно что происходит, в следствии того, что строчки оказываются перемешанными. В этом случае, если хочется запустить сборку в один поток и разобраться в причинах неприятностей, то приходится уже вручную переключать сборку в однопоточный режим:
mvn -T1 clean install
The Takari Lifecycle
The Takari Lifecycle — альтернатива жизненному циклу Maven по умолчанию (сборка JAR файлов). Его отличительная особенность — вместо пяти отдельных плагинов для одного стандартного жизненного цикла используется один универсальный с тем же функционалом, но с гораздо меньшим количеством зависимостей. Как следствие — гораздо более быстрый старт, более оптимальная работа и меньшее потребление ресурсов. Что даёт существенный прирост производительности при компиляции сложных проектов с большим количеством модулей.
Для активации модернизированного жизненного цикла необходимо добавить takari-lifecycle-plugin в качестве расширения сборки:
<build>
<plugins>
<plugin>
<groupId>io.takari.maven.plugins</groupId>
<artifactId>takari-lifecycle-plugin</artifactId>
<extensions>true</extensions>
</plugin>
</plugins>
</build>
А так же переопределить сборку JAR модулей как takari-jar:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>io.takari.lifecycle.its.basic</groupId>
<artifactId>basic</artifactId>
<version>1.0</version>
<packaging>takari-jar</packaging>
После этого все проекты типа POM, а так же takari-jar проекты будут собираться с использование нового жизненного цикла.
Так же можно включить данный жизненный цикл для всех JAR модулей (см. документацию), в нашем случае это начало приводить к конфликтам с различными плагинами Maven. В результате было принято решение просто переопределить packaging модулей, где это можно сделать без ущерба для сборки. Как показала практика этого оказалось более чем достаточно.
Следует так же отметить, что при использование расширения takari-lifecycle-plugin, изменяется расположение различных настроек сборки. Они перемещаются в секцию configuration этого плагина. Например:
<plugins>
<plugin>
<groupId>io.takari.maven.plugins</groupId>
<artifactId>takari-lifecycle-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
Более подробную информацию можно посмотреть в документации..
Takari Maven Wrapper
У Takari есть ещё одна приятная вещь — Maven Wrapper. По аналогии с Gradle Wrapper, он позволяет запустить сборку проекта сразу после клонирования. Без необходимости установки и настройки Maven у себя на компьютере. К тому же, это позволяет закрепить необходимую версию Maven за проектом.
Самый простой способ добавить в свой проект враппер — воспользоваться архетипом. Выполним в корне проекта:
mvn -N io.takari:maven:wrapper
После этого в текущем каталоге у нас появятся два скрипта:
- mvnw.bat — для Windows
- mvnw — для *nix систем
А так же в каталоге .mvn/wrapper появится сам враппер и файл его конфигурации.
Всё. После этого можно вызывать:
./mvnw clean install
А если требуется другая версия Maven, то можно установить необходимый URL в конфигурации .mvn/wrapper/maven-wrapper.properties.
И опять-таки тут не обходится без нюансов. Так в организациях с закрытой сетью часто используют проксирующие репозитарии Maven такие как Nexus или Artifactory. В таком случае каждый разработчик вынужден отдельно настраивать у себя зеркало (mirror) Maven на этот репозитарий. Что немного противоречит идеологии враппера — отсутствие необходимости каких либо настроек.
Выйти из положения можно следующим образом: создадим в нашем проекте файл .mvn/settings.xml вида
<?xml version="1.0" encoding="UTF-8"?>
<settings>
<mirrors>
<mirror>
<id>nexus-m2</id>
<mirrorOf>*</mirrorOf>
<url>http://repo.org.ru/nexus/content/groups/repo-all-m2</url>
<name>Nexus M2</name>
</mirror>
</mirrors>
</settings>
и добавим в файл .mvn/maven.config строчку
--global-settings .mvn/settings.xml
В результате чего зеркало начнёт подхватываться автоматически.
Тестирование и результаты
Всё вышеописанное не имело бы смысла, если бы не давало впечатляющих результатов по ускорению сборки проекта. И чтобы не быть голословным — приведу результаты, которые были получены на одном из наших рабочих проектов.
Итак имеем:
- Рабочий компьютер: Intel® Core(TM) i5-3470 CPU @ 3.20GHz
- Linux Kubuntu 14.04
- Java 8b60
- Maven 3.3.3
- Мультипроект в котором насчитывается 234 pom.xml файлов. Большинство из них собирают различные jar артефакты, но есть и некоторое количество ejb, war, ear и т.п.
Т.к. в «ванильном» Maven с многопоточной сборкой наблюдались проблемы, то (почти)всегда использовался только один поток, что в итоге приводило к времени сборки 5:32 (5 минут 32 секунды) и выше. После всех оптимизаций (параллельная сборка + takari lifecycle) время сборки оказалось равным 1:33. Практически в 4 раза!
Все промежуточные результаты сведены в таблицу ниже:
Как собиралось | Количество потоков | Затраченное время |
---|---|---|
default | 1 | 5:32 |
default | 4 | 3:25 |
smart build | 4 | 3:18 |
smart build + takari-jar | 1 | 3:23 |
smart build + takari-jar | 4 | 1:33 |
Smart Build запускался по два раза, и фиксировался второй результат, т.к. после первого запуска может происходить оптимизация порядка выполнения сборки (см. документацию).
Что любопытно, добавление Takari Lifecycle в однопоточном режиме даёт такой же прирост производительности, как и выполнение сборки в 4-е потока но на «ванильном» Maven.
В качестве заключения
Я только недавно обнаружил инструменты описанные в данной статье. Так что практика их использования пока ещё очень скромная. Возможно со временем ещё вылезут какие-либо подводные камни. Но в любом случае такого радикального ускорения сборки оказалось достаточным, чтобы рискнуть использовать эти возможности в нашем тех процессе. Время покажет что из этого получится.
Так же хочу обратить внимание, что в github репозитарии компании Takari есть ещё некоторое количество любопытных проектов. Их описание выходит за рамки данной статьи, но, возможно, кого-то что-то ещё и заинтересует.
UPD
Как уже отметил в комментариях, начал приходить фидбэк от разработчиков. Оказалось, что файл mvnw.bat не выполняет свои функции. Была внесена правка на скорую руку, которая привела функционал в надлежащий вид:
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto chkMHome
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:chkMHome
if not "%M2_HOME%"=="" goto valMHome
SET "M2_HOME=%~dp0.."
if not "%M2_HOME%"=="" goto valMHome
echo.
echo Error: M2_HOME not found in your environment. >&2
echo Please set the M2_HOME variable in your environment to match the >&2
echo location of the Maven installation. >&2
echo.
goto error
:valMHome
:stripMHome
if not "_%M2_HOME:~-1%"=="_\" goto checkMCmd
set "M2_HOME=%M2_HOME:~0,-1%"
goto stripMHome
:checkMCmd
@rem if exist "%M2_HOME%\bin\mvn.cmd"
goto init
echo.
echo Error: M2_HOME is set to an invalid directory. >&2
echo M2_HOME = "%M2_HOME%" >&2
echo Please set the M2_HOME variable in your environment to match the >&2
echo location of the Maven installation >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
set MAVEN_CMD_LINE_ARGS=%*
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
@rem for %%i in ("%M2_HOME%"\boot\plexus-classworlds-*) do set CLASSWORLDS_JAR="%%i"
set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar""
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.home=%M2_HOME%" "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS%
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%
Так же оказалось, что в целом сборка под Windows значительно медленнее, чем под Linux. Счем это связано пока не понятно.
UPD2
Всплыл ещё один тонкий момент. Сборка для SonarQube конфликтует со Smart Builder'ом. Т.к. по умолчанию включена опция --builder smart, то для сборки под SonarQube не достаточно выполнить
mvn sonar:sonar
Нужно ещё и переключиться обратно на стандартную стратегию сборки:
mvn --builder multithreaded sonar:sonar
или
mvn --builder singlethreaded sonar:sonar
в зависимости от ситуации.
Комментарии (10)
AlexZaharow
03.09.2015 14:51Вы JRebel не пробовали? Может ли он вам подойти?
vektory79
03.09.2015 15:02К сожалению у нас слишком много собственной динамики и так просто JRebel не прикрутить. Да и не все случаи он решает. У людей больше всего ломки именно при необходимости синхронизации с апстримом, т.к. при это в любом случае нужно пересобрать всё целиком.
А для повседневной работы мы пробовали приспособить, но не вышло. В нашем случае только если собственное дополнение к JRebel писать, но на такое никто не сподвигся. К тому же сейчас планируем уход от такой динамики. Возможно тогда станет легче и появится возможность использовать JRebel.
igor_suhorukov
03.09.2015 23:45Спасибо! Как раз недавно смотрел на Maven Wrapper. Очень удобная вещь — проект из scm становится самодостаточным для сборки
vektory79
04.09.2015 10:25Тут начал фидбэк от разработчиков приходить. Оказалось, что в mvnw.bat какая-то ересь написана. Не работает как надо. И по первым прикидкам и не должно. Немного подправили и всё стало как надо. Как смогу — добавлю к статье.
MaximChistov
Какая клевая идея курлом исполняемые файлы по http грузить )) Может хотя бы на https поправите?)
vektory79
Если честно, то это взято с сайта Takari, но мне не сложно и улучшить оригинал.
vayho
А что конкретно в этом случае может произойти плохого?
vektory79
Ну, теоретически — MitM
vayho
https не спасает от mitm
MaximChistov
curl по умолчанию неправильные(=подмененные, самоподписанные) сертификаты не принимает, насколько я знаю