snap & flatpack — трагедия общин

habr.png

Лонгрид варнинг: вас предупредили, много букв.

Уже давно ведётся разработка формата дистрибуции приложений, которые были «свободны» от общесистемных зависимостей. Ubuntu очень, очень активно продвигает свой snap, gnome — flatpack. Оба обещают рай и свободу от rpm/deb. Давайте же подумаем про проблему, которую они хотят решить, и цену, которую они просят за решение этой проблемы.

Никто в современном мире не может написать приложение, не используя чужого кода. Причин несколько:


  • Многие библиотеки настолько серьёзны, что написать их функционал с нуля — это Грандиозная Задача. Примеры — поддержка unicode, рендеринга шрифтов, математики.
  • Другие библиотеки предлагают довольно скромный набор функций, но написанных так хорошо, что написать хотя бы так хорошо же, почти невозможно. Стандартные библиотеки языков программирования, различные реализации libc, etc.
  • Стоимость работы по взаимодействию с чужим кодом (чему посвящён этот раздел) чаще всего оказывается ниже, чем цена сопровождения своего кода. Плотность «багов на строк кода» с большой вероятностью будет сравнима, а «свои баги» надо самому же и ловить. Чужие (популярные) библиотеки имеют вероятность быть отлажены и исправлены чужими же руками.

Ключевым же является то, что если даже функционал одиночной библиотеки мы можем «из принципа» писать сами, то общее количество нужных функций (и зависимостей) даёт чуть ли не экспоненциальный рост числа задач, которые надо решить, отодвигая время начала работы над кодом самой программы в недостижимую даль.

Пример для осознания масштаба драмы: допустим, ваше приложение принимает на вход две строки как опциональные аргументы и выводит их вместе, после нормализации. Если вы пишите индустриальное приложение (приложение, которое похоже на «настоящее»), то:


  • Вам надо парсер командной строки
  • Который должен принимать юникод
  • И, возможно, давать пользователю подсказку, что он опечатался в имени аргумента
  • Что требует фонетического сравнения
  • И, возможно, регулярных выражений
  • В общем случае вам придётся поддерживать не только юникод, но и другие локали, что требует библиотеки поддержи локалей и ВСЁ то, что люди придумали в контексте локалей.
  • Конкатенация строк с нормализацией — ещё одно применение отдельной библиотеки юникода, сами вы такое не реализуете.
  • Вывод на экран (справки по командной строке, вашего результата) с большой вероятностью потребует поддержки ncurses — библиотеки, поддерживающей разные терминалы (можно обойтись текстовым режимом, но приложения часто используют цветовые возможности).
  • Тесты подразумевают использование тестового фреймворка, возможно, библиотеки для моков.

Понятно, что такая сложность для задачи «двух строк» это грубый overengineering, но как только вы начинаете делать что-то больше, идея «всё сам (а)» начинает выходить за пределы обозримого и реализуемого.

Как вы думаете, сколько библиотек нужно, чтобы гарантированно отработать curl http (s)://…? Много. Вы будете использовать одну, но зависимости ваших зависимостей — это ваши зависимости.

При том, что использование библиотек неизбежно, само использование может различаться по реализации. Обратите внимание, у нас появилось два важных слова «использование» и «реализация использования». Что значит «использование»? В самом грубом виде — возможность вызвать код библиотеки, когда это нужно. А вот реализации этого:


  • Мы можем скопировать код, который делает нужные нам операции. В виде куска кода (copy&paste), как отдельный модуль в языке программирования (объектный файл для компилируемых языков), либо как отдельный модуль (для интерпретируемых языков). Где-то тут же рядом идёт и «скопировать исходный файл библиотеки к себе в каталог с приложением». Какие проблемы это создаёт? Главная, основная проблема — мы теряем (навсегда) связь с оригиналом. Даже если автор оригинальной библиотеки исправит ошибку, мы не будем об этом знать. Более того, если мы просто скопировали код, то следующий человек, работающей над программой, даже не сможет узнать, что этот код «чужой». Фактически, мы срезали путь в вопросе «написать с нуля» и взяли чужое. Однако, мы срезали лишь кусочек, потому что если в этом коде будут ошибки (а они там не будут, они там есть), то их исправление потребует от исправляющего пойти и разобраться в сути проблемы до самого низа. Даже если разбирательство потребует чтения нескольких сотен тысяч строк исходного кода и сотен RFC (а так же комментариев о том, что реализации отличаются от RFC), другого пути у нас нет. Ключевой ошибкой в этом месте является то, что мы потеряли информацию о том, что это код чужой. Наличие комментариев в файле может помочь, но потребует деятельного и глубокого участия человека, потому что если мы напишем в комментарии «взято из libfoobar, src/lib/foo.c версии 364a51577f3782dbf8e5d2482ab941980357c492», то кому-то надо будет посмотреть, где libfoobar находится, какая там версия и что поменялось с предыдущей версии». Чтобы упростить этот процесс, нам нужна машиночитаемая метаинформация.
  • Если мы сопровождаем «чужой код» метаинформацией и используем программы для управления этим кодом (вместо копипаста), то это называется вендоринг, т.е. контролируемое включение чужого кода в свой код. Технически вендоринг может происходить на этапе исходного текста, линковки объектов в исполняемый файл, импорта модулей (в интерпретаторах) из состава приложения, или, даже, динамической линковки с «своей» версией библиотеки (об этом чуть позже).
  • Наконец, мы можем осуществлять динамическую линковку на этапе запуска приложения. Для компилируемых языков это обычные so’шки, для интерпретируемых — наличие модуля в общесистемном импорте. Если его могут импортировать несколько приложений, это это общая библиотека. Если приложение «принесло свой модуль», то библиотека «своя», даже если её интерфейс подразумевает «общую библиотеку». Например, если приложение использует «свою» версию so’шки, вне зависимости от того, отличается ли она от общей, или нет, то это вендоринг. А если импортируется системная, то это общая библиотека (shared library).

В чём же различие между этими методами? Я кратко приведу аргументы, они кратно обсуждались в множестве статей. Каждый из этих аргументов остаётся валидным не смотря на наличие соседних контраргументов:


  • Экономию памяти (оперативной и на диске) для so’шек, уменьшение размеров установленной системы. Чем больше приложений использует одну и ту же so’шку, тем большая экономия памяти. Соответственно, обратно, чем больше «своих» библиотек приносит приложение, тем оно «жирнее».
  • Спор о том, кто следит за уязвимостями — система (предоставляя обновления библиотеки) или автор приложения (вовремя обновляя его).
  • Разрешение конфликта зависимостей (вендоринг решает эту проблему так как общие библиотеки требуют внимания и аккуратности от всех участников процесса, иногда создавая непреодолимые трудности), тот самый легендарный dll hell.
  • Новые версии библиотек — либо они появляются по пожеланию авторов приложения, либо по решению авторов дистрибутива. В одном случае автор может принести нужную ему новую фичу, в другом случае, дистрибутив может принести улучшение существующего приложения за счёт поддержки чего-то нового в библиотеке (например, hidpi экраны начали правильно работать во всех приложениях, динамически линкующиеся с библиотеками qt/gtk).

Все эти вопросы многократно разбирались ранее. Вместо этого я хочу сфокусироваться на социальных аспектах водораздела «всё своё» и «всё общее».

Общие библиотеки — это кооперация, власть и ответственность. Люди, определяющие какие общие библиотеки доступны в операционной системе диктуют производителям ПО какие общие библиотеки они могут использовать. Многое ПО может использовать разные библиотеки, и указание на то, какую точно версию нужно использовать остаётся на усмотрение линкера (для компилируемых языков) или обработчика файла зависимостей (pip, bundler, etc). Если все приложения в дистрибутиве собраны с одинаковыми требованиями, то наступает благодать: если в какой-то библиотеке есть ошибка, мейнтейнер этой библиотеки обновляет версию, и исправление автоматически применяется ко всем приложениям. Даже если приложение релизится раз в два года, фикс в условном openssl будет применён в течение недели. Если в конкретной ОС принято решение об отказе от старого протокола, каких-то модификациях (например, интерфейса пользователя), то эти изменения так же применятся для всех. Look&feel в общем стиле, который (быть может) может быть изменён пользователем один раз и для всех. Это ли не благодать?


Власть и борьба за неё

… Эта благодать требует, чтобы все приложения могли работать с выбранной версией библиотеки. А что, если какое-то приложение хочет очень-очень новую функцию из библиотеки, а все остальные приложения не хотят его использовать, потому что это, допустим, не LTS-релиз библиотеки, т.е. она не достаточно стабильна? А ведь дистрибутив может отказываться переходить на новые версии «из принципа», ибо мы обещали пользователям только багфиксы, а новые версии — только в следующей версии ОС, которая (вроде бы), выйдет через пол-года. И это вызывает сопротивление со стороны авторов приложения. Да кто вы такие, чтобы мне рассказывать с какими версиями мне работать? Я автор, я так вижу. Мне нужна libfoobar 3.14-pre2 или старше, а не ваша древняя унылая libfoobar 3.10.

… В этот момент автор просто пишет в требованиях к приложению libfoobar>=3.14-pre2. Мейнтейнер берёт и патчит требование, плюс удаляет код, который от этой библиотеки зависел. Может быть. Или просто отказывается принимать новую версию с такой зависимостью, пока эта зависимость (libfoobar 3.16) не окажется в новой версии дистрибутива.

Если автору очень нужно, чтобы пользователи пользовались новой версией (например, потому что автор не хочет поддерживать старую версию), то он ищет обходные пути для отправки приложения пользователю.

То же самое происходит в ситуации, когда есть несколько дистрибутивов, некоторые новее, некоторые старее. Поддерживать более старые дистрибутивы, тестировать с разными библиотеками — это сложно. Так что вариант «отгрузить со своими библиотеками» возникает почти сразу же.

Это создаёт предпосылки для возникновения трагедии общин:


  • Каждому производителю (автору ПО) хочется отгружать так, как ему нужно. Подстраиваться под чужие правила (версии) — это трата усилий и времени, тем паче, что в мире много разных дистрибутивов
  • Пользователи хотят новые версии.

При этом, чем больше приложений идут со своими библиотеками, тем меньше польза от системных библиотек. Помните про «благодать»? Чем менее она «всеобщая», тем меньше она благодать. Если общая библиотека используется 5 разными приложениями из 995 других, то польза этой библиотеки — 0.5%. Обидно, да. Причём, это задевает всех пользователей, даже тех, кто, в принципе, не имеет острой потребности в новой фиче —, но если приложение доступно только в вендоринговом виде, то у пользователя нет вариантов.

Получается, у нас есть глобальный экстремум: все приложения используют только общие библиотеки (максимум общей благодать, неудобства у авторов отдельных приложений) или «каждый сам по себе» (толстенный дистрибутив, с кучей приложений у которых могут быть незамеченные, но широко используемые уязвимости, жрущий кучу памяти, но автором каждого приложения удобно).

Вот именно тут мы и приходим к спору rpm/deb VS snap/flatpack

Ubuntu очень, очень сильно ратует за snap’ы. GNOME уверен, что будущее за flatpack’ами. Каждый из них — это фреймворк для глубоко индивидуалистических приложений. Всякие electron’ы, у которых с собой не только подкапотный браузер, но и подкапотная операционная система. Свой libc, свой libssl, свои регэкспы, свои ncurses, etc. Общим выступает только ядро, т.е. по сути это тот же контейнеризированное приложение, но для десктопа. Дай каждому своё ядро, и получится appliance в форме виртуалки. Допишите метаданные — и получится контейнер Docker’а.

Индивидуализм приложений (авторов приложений) понятен, но кто тогда выступает за общее благо? Локальное крупное улучшение компенсируется незначительным общим ухудшением дистрибутива помноженным на чисто приложений. Если все делают себе локальные улучшения, то сумма ухудшений становится больше выгоды от суммы улучшений.

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

Ubuntu зависит от Debian куда больше, чем хотелось бы Canonical (компания, выпускающая Ubuntu). Ценность Ubuntu — не в усилиях мейнтейнеров Ubuntu, а в огромном репозитории ПО, идущем из Debian в форме, когда все приложения хорошо работают друг с другом усилиями тысяч мейнтейнеров отдельных пакетов, являющихся «владельцами» дистрибутива Debian. Canonical добавляет поверх этого свои усилия по полировке результата — и за это любима некоторыми. Добавим чуть-чуть маркетинга и фиксированный lifecycle, который по нраву энтерпрайзу, и получается отличный продукт.

… Который зависит от воли тысяч добровольцев где-то там.

Что совершенно не устраивает почти любую коммерческую компанию. Как разорвать эту зависимость? Правильно, сделав свой комплект приложений. Чем больше своих приложений, тем меньше «взбрыки» в апстриме будут задевать компанию. Достаточно вспомнить историю, когда голосование в Дебиан по поводу systemd похоронило upstart, разрабатывавшийся Canonical.

Но мейнтенить несколько десятков тысяч приложений, некоторые из которых — свой космос (erlang, go, perl, python, R, julia, etc), а некоторые — монстры в соответствующей предметной области (браузеры, emacs, tex, pacemaker, etc) — это неподъёмная работа. Не зря это тысячи мейнтейнеров.

… И тут есть идея. А, пусть, авторы приложений сами мейнтейнят приложения. Выдадим каждому по песочнице, пусть копаются. Авторы получают свободу, Canonical — приложения, которые не зависят от Debian и которые хоть кто-то мейнтейнит бесплатно. Пользователи получают…

… приложения, которые жирные, тяжёлые, у которых обновления нерегулярные и которые с лёгкостью могут держать уязвимости неисправленными годами… Зато некоторые из них shiny new.

Представьте себе мир, в котором все всё везут с собой… Знаете как это выглядит? Посмотрите на chefsdk. Он отгружает с собой внутри свою postgresql (со своими зависимостями), свой rabbitmq (который зависит от своего erlang’а), плюс chef-server тоже на erlang’е, так что у него тоже свой erlang. Внезапно, у нас внутри одного приложения два erlang’а и несколько десятков копий одних и тех же библиотек, чуть-чуть различающихся по версии. Это не финальный вариант, т.к. внутри между компонентами всё-таки есть общие библиотеки. Если мы их распилим дальше, то получится несколько десятков копий openssl и libc на одно приложение. Даже не в финальном виде это выглядит как 600Мб на одно приложение.

… Что, конечно, кратно больше, чем среднее приложение на electron.… И в 12 раз больше, чем целый mariadb сервер (целая СУБД!) или krita или gimp (огромные приложения для работы с графикой).

А если каждый будет такой? У меня на компьютере установлено 2000 пакетов (не считая -dev и lib)… 2000×300 = 600Гб (За средний размер получившегося я взял половину от chefsdk, т.к. не все настолько ужасны по зависимостям). Сейчас они занимают около 7Гб (включая ресурсы, вроде документации, текстур редакторов, шаблонов CAD-программ и т.д.).

Если это превратится в 600Гб, то не трагедия ли это общин в чистом виде? В каждом взятом моменте мы наблюдаем локальную оптимизацию (и решение чьего-то неудобства), но вместе, сумма этих локальных оптимизаций снижает общую оптимальность системы. На мой взгляд, больше, чем локальный выигрыш каждого из участников.

Я понимаю, зачем Canonical пушит snap. Я это понимаю, и не одобряю.

© Habrahabr.ru