Java
3 September 2015

Takari: Maven на стероидах

From Sandbox

Предыстория


Уже довольно давно я работаю в проекте, в котором используется система сборки 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

в зависимости от ситуации.

+19
15.4k 99
Comments 12
Top of the day