Толстые и тонкие дистрибутивы
На этой неделе я окончательно перевёл все мои проекты на Ubuntu 18.04 и тонкие .deb дистрибутивы. Этот проект я начал в начале года и окончательно завершил только сейчас, спустя почти 8 месяцев. Сама по себе миграция заслуживает отдельной статьи с нытьём и риторическими вопросами. Здесь же, я хочу немного описать противостояние тонких и толстых дистрибутивов. То, как я видел эту историю и немного анализа. Поехали!
Дистрибутивы
Почти сразу же, как только написана первая версия любой программы, становится вопрос о том, как её распространять. В 2020 есть несколько довольно стандартных способов:
- Докер-образ. Программа компилируется, все зависимые библиотеки кладутся как в storage layer
- zip файл. Все необходимые файлы просто кладутся в архив
- debian пакет. Немного о том, как его собрать, я писал в одной из своих статей
Если с докер-образами всё понятно, то вот, что положить в zip файл или debian пакет, не всегда очевидно. И существуют 2 диаметрально противоположные стратегии.
Толстые дистрибутивы
Идея достаточно проста: давайте положим в архив все необходимые файлы и зависимые библиотеки, необходимые для работы приложения. Из плюсов данного подхода:
- почти нет зависимости на внешнюю среду. Абсолютно не важно какие библиотеки уже установлены в операционной системе, приложение принесёт свои собственные библиотеки с нужными ему версиями.
- нет зависимости от других приложений. Это следует из того, что все зависимости лежат внутри толстого дистрибутива.
- простота. Достаточно скачать один дистрибутив и запустить его
java -jar fatApp.jar
.
Из минусов:
- дистрибутив очень много весит. Из-за того, что все зависимости лежат внутри, размер дистрибутива значительно увеличивается. Это влияет на скорость установки (нужно скачать дистрибутив из apt репозитория или artifactory) и скорость сборки, загрузки в репозиторий.
- как следствие, сильно увеличивается в размерах бинарный репозиторий. Нужно продумывать стратегию очистки старых версий. Например, недавно компания Docker столкнулась с проблемой разросшегося репозитория docker hub и решила удалять неиспользуемые докер-образы. Удалять старые и не использующиеся зависимости - достаточно сложная и нетривиальная задача.
Ярким приверженцем использования толстых дистрибутивов является проект spring boot. Во время фазы package
они собирают толстый дистрибутив, который можно запустить одной строчкой:
$ mvn package
$ java -jar target/mymodule-0.0.1-SNAPSHOT.jar
Тонкие дистрибутивы
Решить проблемы толстых дистрибутивов призваны тонкие. Но и у них есть свои минусы:
- относительная сложность. Нет единого мнения о том, как правильно собирать тонкий дистрибутив и разворачивать его для запуска.
- зависимость от других приложений. В классическом тонком дистрибутиве зависимости установлены в операционной системе. Но что, если одному приложению нужна одна версия библиотеки, а другому другая? Разработчики linux-подобных операционных систем пытаются найти наименьшее общее кратное для тысяч приложений и библиотек. Это достаточно сложная и трудоёмкая процедура.
Плюсы вытекают сами собой:
- приложения очень мало весят. Их можно очень быстро деплоить и, зачастую, не нужно переживать о размере бинарного репозитория.
- зависимости переиспользуются между приложениями. Это не очень актуально для Java мира, но вот для C/C++ мира вполне востребовано. Смысл в том, что библиотека загружается в память всего один раз и потом используется разными приложениями. Это уменьшает потребление оперативной памяти и скорость загрузки приложения.
Как я уже писал, яркими сторонниками тонких дистрибутивов являются операционные системы. Несмотря на то, что команда Ubuntu решила сделать толстые дистрибутивы (snap пакеты), сообщество встретило эту идею очень прохладно и с долей скептицизма.
Тонкие дистрибутивы для Java
Во всех своих проектах я постепенно отказался от толстых дистрибутивов и перешёл на тонкие. Для меня это было важно из-за нескольких причин:
- большие бинарные репозитории дорого держать. Для хобби проектов, которые не приносят деньги, платить за гигабайты дистрибутивов - дорого.
- скорость сборки и загрузки в репозитории. Я очень часто работаю в поезде, самолёте, гостинице, где широкие каналы большая редкость. Загружать 100 мегабайтные толстые дистрибутивы можно часами. А вот загрузка ~200кб занимает секунды. Это очень удобно и увеличивает продуктивность.
Поскольку не существует единого мнения о том, как делать тонкие дистрибутивы, я решил сделать свой. Для этого я написал небольшой maven plugin - deps-maven-plugin. Во время сборки он создаёт три файла:
- repositories.txt - список всех maven репозиториев, которые доступны в проекте
- dependencies.txt - список всех зависимостей проекта
- script.sh - фиксированный скрипт, лежащий внутри плагина
<plugin>
<groupId>com.aerse.maven</groupId>
<artifactId>deps-maven-plugin</artifactId>
<configuration>
<repositories>${project.build.directory}/deps/repositories.txt</repositories>
<dependencies>${project.build.directory}/deps/dependencies.txt</dependencies>
<script>${project.build.directory}/deps/script.sh</script>
<excludes>
<exclude>com.example:*:*<exclude>
</excludes>
</configuration>
</plugin>
Идея достаточно проста: после распаковки тонкого дистрибутива нужно вызвать script.sh и передать ему два сгенерированных файла. Он их скачает и положит в указанную папку. Это и будет папка со всеми зависимостями.
Тут нужно учитывать, что все зависимости должны быть доступны в открытых maven репозиториях. Если это не так, то их можно исключить в секции excludes
и положить внутрь тонкого артефакта.
Но как же быть, если артефакт поменял версию или его удалили из списка зависимостей? Всё просто: script.sh строит пересечение зависимостей, которые нужны в папочке и тех, которые там уже есть. Если зависимости уже были скачаны, то они не будут ещё раз скачиваться. А если зависимости уже не используются (лежат в папке, но отсутствуют в dependencies.txt), то они удаляются из папки.
После того как скрипт отработает, можно запускать приложение. Например, вот так выглядят пути для r2cloud:
java -cp /home/pi/r2cloud/lib/*:/usr/share/java/r2cloud/* ru.r2cloud.R2Cloud
Папка /home/pi/r2cloud/lib/
содержит само приложение. А папка /usr/share/java/r2cloud/
содержит все зависимости приложения.
Результаты
Помимо очевидных плюсов тонких дистрибутивов, есть и неочевидные. Например, они сильно экономят трафик при обновлении r2cloud через 3g модем. А так же, за всё время у меня накопилось всего 564.11Мб дистрибутивов r2cloud. А это около 600 сборок!