Толстые и тонкие дистрибутивы

На этой неделе я окончательно перевёл все мои проекты на 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 сборок!