Почему GitHub не может хостить ядро Linux

в 8:20, , рубрики: Git, github, крупные проекты, монорепозиторий, Разработка под Linux, системное программирование, Системы управления версиями, ядро Linux

Некоторое время назад на отличной конференции maintainerati я пообщался с несколькими друзьями-мейнтейнерами о масштабировании по-настоящему больших проектов open source и о том, как GitHub подталкивает проекты к определённому способу масштабирования. У ядра Linux абсолютно иная модель, которую мейнтейнеры-пользователи GitHub не понимают. Думаю, стоит объяснить, почему и как она работает и чем отличается.

Ещё одной причиной для написания этого текста стала дискуссия на HN по поводу моего выступления «Мейнтейнеры не масштабируются», где самый популярный комментарий сводился к вопросу «Почему эти динозавры не используют современные средства разработки?». Несколько известных разработчиков ядра энергично защищали списки рассылки и предложение патчей через механизм, похожий на пулл-реквесты GitHub, Но по крайней мере несколько разработчиков графической подсистемы хотели бы использовать более современный инструментарий, который гораздо легче автоматизировать скриптами. Проблема в том, что GitHub не поддерживает тот способ, которым ядро Linux масштабируется на огромное число контрибуторов, и поэтому мы просто не можем перейти на него, даже для нескольких подсистем. И дело не в хостинге данных на Git, эта часть явно в порядке, а дело в том, как на GitHub работают пулл-реквесты, обсуждение багов и форки.

Масштабирование в стиле GitHub

Git классный, потому что каждый может очень легко сделать форк, создать свою ветку и изменять код. И если в итоге у вас получится нечто стоящее, вы создаёте пулл-реквест для основного репозитория и его рассматривают, тестируют и осуществляют слияние. И GitHub классный, потому что он представил подходящий UI, чтобы эти сложные вещи было приятно и просто находить и изучать, так что новичкам гораздо легче войти в курс.

Но если проект в итоге стал чрезвычайно успешным и никакое количество тегов, меток, сортировки, ботов и автоматизации уже не способны справляться со всеми пулл-реквестами и проблемами в репозитории, то приходит время снова разделить проект на более управляемые части. Более важно, что с определённым размером и возрастом проекта разным частям понадобятся разные правила и процессы: у сверкающей новой экспериментальной библиотеки иная стабильность и критерии CI, чем у основного кода, а может у вас в наличии мусорный бак legacy с кучей исключённых плагинов, которые уже не поддерживаются, но их нельзя удалять. Так или иначе, придётся разделить огромный проект на подпроекты, каждый с собственным процессом и критерием слияния патчей и с собственным репозиторием, где работают свои пулл-реквесты и отслеживание проблем. Обычно нужно от нескольких десятков до нескольких сотен контрибуторов, работающих полный рабочий день, чтобы головная боль от проекта выросла до такой степени, что станет необходимо разделение на части.

Почти все проекты на GitHub решают проблему путём разделения единого дерева исходников на множество различных проектов, каждый со своей отдельной функциональностью. Обычно это приводит к появлению ряда проектов, которые считаются ядром, плюс куча плагинов, библиотек и расширений. Всё связывается каким-то типа плагином или пакетным менеджером, который в некоторых случаях напрямую получает код из репозиториев GitHub.

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

  • Ваше сообщество слишком фрагментируется. Большинство контрибуторов будут просто иметь дело с кодом и соответствующим репозиторием, куда они напрямую контрибутят, игнорируя всё остальное. Им-то классно, но так значительно снижается вероятность вовремя заметить дублирующиеся усилия и параллельные решения между разными плагинами и бибилотеками. И людям, которые хотят управлять всем сообществом в целом, придётся иметь дело с кучей репозиториев, которые или управляются через скрипт, или подмодули Git, или что-то ещё худшее. К тому же, они утонут в пулл-реквестах и проблемах, если на что-нибудь подпишутся. Любая тема (может, у вас общий инструментарий для сборки билдов или документация, или что угодно ещё), которая не вписывается идеально в разделённые репозитории, становится головной болью для мейнтейнеров.
  • Даже если вы заметили необходимость рефакторинга и совместного использования кода, здесь возникает больше бюрократических препятствий: сначала нужно выпустить новую версию ключевой библиотеки, затем пройтись по всем плагинам и обновить их, и только потом, может быть, вы сможете удалить старый код в библиотеке совместного доступа. Но поскольку всё сильно разбросано вокруг, вы можете и забыть о последнем шаге.

    Конечно, всё это требует не такой уж большой работы, и многие проекты отлично справляются с управлением. Но здесь всё равно требуется больше усилий, чем простой пулл-реквест в единый репозиторий. Очень простые операции рефакторинга (как простое совместное использование единственной новой функции) происходят реже, и за долгое время такие издержки накапливаются. За исключением тех случаев, когда вы по примеру node.js создаёте репозитории для каждой функции, а затем по сути меняете Git на npm в качестве системы управления исходным кодом, и это тоже кажется странным.

  • Комбинаторный взрыв теоретически поддерживаемых разных версий, которые становятся де-факто неподдерживаемым. Пользователям приходится осуществлять интеграционное тестирование. А в проекте всё сводится к одобренным («благословенным») сочетаниям версий, или по крайней мере такое декларируется де-факто, поскольку разработчики просто закрывают сообщения о багах словами «пожалуйста, сначала обновите все модули». И снова, это означает, что у вас фактически монорепозиторий, разве что не на Git. Ну, только если вы используете подмодули, и я не уверен, что это считается Git…
  • Болезненная реорганизация при разделении общих проектов на подпроекты, поскольку вам нужно реорганизовать репозитории Git и то, как они разделяются. В едином репозитории смена мейнтейнера сводится к простому обновлению файлов OWNER или MAINTAINERS, а если ваши боты в порядке, то новые мейнтейнеры получат теги автоматически. Но если для вас масштабирование означат разделение репозиториев на разрозненные наборы, то любая реорганизация будет настолько же болезненна, как и первый шаг от единственного репозитория к группе разделённых репозиториев. Это означает, что ваш проект слишком надолго застрянет на плохой организационной структуре.

Интерлюдия: зачем существуют пулл-реквесты

Ядро Linux — один из нескольких известных мне проектов, который не разделён таким образом. Прежде чем мы рассмотрим, как он работает (ядро — это гигантский проект и он просто не может работать без некоторой структуры подпроектов), мне кажется, интересно посмотреть, зачем в Git нужны пулл-реквесты. На GitHub это единственный способ разработчикам внести предложенные патчи в общий код. Но изменения в ядре приходят как патчи в почтовом списке рассылки, даже через долгое время после внедрения и широкого использования Git.

Но уже первая версия Git поддерживала пулл-реквесты. Аудиторией этих первых, достаточно сырых, релизов были мейнтейнеры ядра, Git написали для решения мейнтейнерских проблем Линуса. Очевидно, Git был нужен и полезен, но не для обработки изменений от отдельных разработчиков: даже сегодня, а тем более тогда, пулл-реквесты использовались для обработки изменений целой подсистемы, синхронизации отрефакторенного кода или схожих сквозных изменений между различными подпроектами. Как пример, сетевой пулл-реквест 4.12 от Дэйва Миллера, представленный Линусом, содержит более 2000 коммитов от 600 разработчиков и кучу слияний для пулл-реквестов от подчинённых мейнтейнеров. Но почти все патчи сами по себе представлены мейнтейнерами и выбраны из почтовых списков рассылки, а не самими авторами. Такова особенность разработки ядра, что авторы в основном не коммитят патчи в общие репозитории — и вот почему Git отдельно учитывает автора патча и автора коммита.

Инновацией и улучшением в GitHub было использование пулл-реквестов для всего подряд, вплоть до отдельных патчей. Но не для этого пулл-реквесты изначально создавались.

Масштабирование способом ядра Linux

На первый взгляд, ядро выглядит как единый репозиторий, где всё в одном месте в репозитории у Линуса. Но это далеко не так:

  • Почти никто не использует основной репозиторий Линуса Торвальдса. Если у них и работает что-то из апстрима, то обычно это одно из стабильных ядер. Но намного более вероятно, что у них ядро со своего дистрибутива, где обычно есть дополнительные патчи и бэкпорты, и оно даже не хостится на kernel.org, это совершенно другая организация. Или у них ядро от своего аппаратного вендора (для SoC и почти для всего, связанного с Android), которое часто значительно отличается от всего, что хостится в одном из «основных» репозиториев.
  • Никто (кроме самого Линуса) не разрабатывает ничего для репозитория Линуса. У каждой подсистемы, а часто даже у крупных драйверов, есть собственные репозитории Git, с собственными списками почтовой рассылки для отслеживания патчей и обсуждения проблем полностью изолированно от всех остальных.
  • Работа между подсистемами производится поверх дерева интеграции linux-next, которое содержит несколько сотен веток Git из примерно такого же количества репозиториев Git.
  • Всё это сумасшествие управляется через файл MAINTAINERS и скрипт get_maintainers.pl, который для каждого данного сниппета кода может сказать, кто мейнтейнер, кто должен проверить код, где находится правильный репозиторий Git, какие использовать списки рассылки, как и где сообщать о багах. Информация основана не просто на расположении файла. Также производится анализ шаблонов кода для проверки, что кросс-подсистемные темы вроде обслуживания device-tree или иерархии kobject hierarchy обрабатываются правильными экспертами.

На первый взгляд это выглядит просто как изощрённый способ заполнить у каждого дисковое пространство кучей ерунды, которая ему не интересна, но есть и много сопутствующих незначительных преимуществ, которые накладываются друг на друга:

  • Совершенно легко провести реорганизацию, выделяя вещи в подпроект — просто обновите файл MAINTAINERS, и готово. Остальное немного сложнее, чем должно быть, потому что вам может понадобиться создать новый репозиторий, новые списки рассылки и новую bugzilla. Это просто проблема UI, которую GitHub элегантно решил классной маленькой кнопкой fork.
  • Очень, очень просто перевести обсуждение пулл-реквестов и проблем между подпроектами, вы просто изменяете поле Cc: в своём ответе. Также намного проще координировать работу между подсистемами, поскольку один пулл-реквест можно направить в несколько подпроектов, при этом ведётся только одно общее обсуждение (поскольку теги Msg-Ids: в тредах списка рассылки одинаковы для всех), хотя сами письма архивируются в куче разных архивов списков рассылок, проходят через разные списки рассылки и лежат в тысячах разных почтовых ящиков. Простое обсуждение тем и кода между подпроектами позволяет избежать фрагментации и так легче заметить, где будет полезно использование общего кода и рефакторинг.
  • Работа между подсистемами не нуждается в каком-то танце с релизами. Вы просто изменяете код, который весь у вас в едином репозитории. Заметьте, что это намного эффективнее, чем возможно в раздельных репозиториях. В случае действительно агрессивного рефакторинга можно просто разделить работу между несколькими релизами, например, когда есть так много пользователей, что вы можете просто изменить их всех сразу, не вызывая слишком больших проблем с координацией.

    Огромное преимущество того, что рефакторинг и обмен кодом стал проще — не нужно тащить с собой кучу легаси-мусора. Это детально объясняется в документе об отсутствии бреда со стабильными API.

  • Ничто не мешает вам создавать собственные экспериментальные дополнения, что является одним из ключевых преимуществ системы с множеством репозиториев. Добавили свой код в свой форк и оставили его там — никто никогда не заставит вас запушить код обратно или запушить его в один общий репозиторий или даже передать в основную организацию, просто потому что нет центральных репозиториев. Это действительно хорошо работает, может и слишком хорошо, свидетельством чему миллионы строк кода вне веток в различных репозиториях аппаратных вендоров Android.

В общем, думаю, это гораздо более мощная модель, потому что вы всегда можете откатиться и делать всё также, как с многочисленными разъединёнными репозиториями. Чёрт, есть даже драйверы ядра, которые находятся в своём собственном репозитории, отдельном от основного ядра, как проприетарный драйвер Nvidia. Ну, это просто как клей исходного кода вокруг блоба, но поскольку он может содержать никаких частей ядра по юридическим причинам, это прекрасный пример.

Это выглядит как фильм ужасов о монорепозитории!

Да и нет.

На первый взгляд ядро Linux выглядит как монорепозиторий, потому что в нём есть всё. И многие знают по собственному опыту, что монорепозитории доставлют много проблем, потому что с определённого размера они просто не могут масштабироваться.

Но если посмотреть ближе, такая модель очень, очень далека от единого репозитория Git. Одна только upstream-подсистема и репозитории драйверов уже дают вам несколько сотен. Если посмотреть на всю экосистему целиком, включая аппаратных вендоров, дистрибутивы, другие ОС на базе Linux и отдельные продукты, вы легко насчитаете несколько тысяч основных репозиториев и ещё много-много дополнительных. И это без учёта репозиториев Git чисто для личного использования отдельными разработчиками.

Ключевое отличие в том, что в Linux единая файловая иерархия как общее пространство имён для всего, но очень много различных репозиториев для всевозможных нужд и проектов. Это монодерево с многочисленными репозиториями, а не монорепозиторий.

Примеры, пожелуйста!

Прежде чем я начну объяснять, почему GitHub не способен в данный момент обеспечить такой рабочий процесс, по крайней мере, если вы хотите сохранить преимущества GitHub UI и интеграции, нужно посмотреть на некоторые примеры, как это работает на практике. Если вкратце, то всё делается через пулл-реквесты Git между мейнтейнерами.

Простой случай — это прохождение изменений через иерархию мейнтейнеров, пока они в конце концов не осядут в дереве там, где нужно. Это легко, потому что пулл-реквест всегда идёт из одного репозитория в другой, так что его можно провести с нынешним GitHub UI.

Гораздо веселее с кросс-подсистемными изменениями, потому что тогда пулл-реквесты из ациклического графа превращаются в сетку. На первом этапе нужно рассмотреть изменения и протестировать их всеми вовлечёнными подсистемами и мейнтейнерами. В рабочем процессе GitHub это означает пулл-реквесты одновременно в многочисленные репозитории, с единым потоком обсуждения между ними. В разработке ядра этот этап осуществляется путём представления патча в кучу разных списков рассылки с указанием мейнтейнеров в качестве получателей.

Слияние отличается от рассмотрения патча. Здесь уже выбирается одна из подсистем как основная, она получает все пулл-реквесты, а все остальные мейнтейнеры соглашаются с таким вариантом слияния. Обычно выбирают ту подсистему, которую сильнее всего затрагивают изменения, но иногда выбирают ту, в которой уже идёт какая-то работа, которая конфликтует с пулл-реквестом. Иногда создают абсолютно новый репозиторий и команду мейнтейнеров. Это часто происходит для функциональности, которая распространяется на всё дерево и не очень аккуратно содержится в нескольких файлах и директориях в одном месте. Недавний пример — дерево отображений DMA, которое пытается объединить работу, до сих пор распределённую среди драйверов, мейнтейнеров платформы и групп поддержки архитектуры.

Но иногда есть многочисленные подсистемы, которые конфликтуют с набором изменений и которым всем нужно как-то решить нетривиальный конфликт слияния. В этом случае патчи не применяются напрямую (пулл-реквест Rebase на GitHub), а вместо этого используется пулл-реквест только с необходимыми патчами, на основании коммита, общего для всех подсистем — его вносят во все деревья подсистем. Такая общая база важна, чтобы избежать загрязнения дерева подсистем ненужными изменениями. Поскольку дальнейшие пуллы касаются только специфических тем, эти ветки обычно называют тематическими ветками.

В качестве примера могу привести поддержку audio-over-HDMI, в этот процесс я был вовлечён непосредственно. Она касается и графической подсистемы, и подсистемы звуковых драйверов. Одинаковые коммиты из одного и того же пулл-реквеста вошли в графический драйвер Intel, а также в звуковую подсистему.

Совершенно другой пример, что это не безумие — единственный сравнимый проект ОС в мире тоже выбрал монодерево с потоком коммитов примерно как в Linux. Я говорю о ребятах с таким гигантским деревом, что им даже пришлось написать полностью новую виртуальную файловую систему GVFS для его поддержки…

Дорогой GitHub

К сожалению, GitHub не обеспечивает поддержку такого рабочего процесса, по крайней мере, не нативно с GitHub UI. Конечно, это можно сделать просто с чистым инструментарием Git, но тогда вы возвращаетесь к патчам в списке рассылки и пулл-реквестам по почте, которые выполняются вручную. Я считаю, это единственная причина, почему сообщество разработчиков ядра ничего не выиграет от перехода на GitHub. Есть также небольшая проблема, что несколько ведущих мейнтейнеров настроены категорически против GitHub в целом, но это уже не технический вопрос. И дело не только в ядре Linux. Дело в том, что в принципе все гигантские проекты на GitHub имеют проблемы с масштабированием, потому что GitHub на самом деле не даёт им возможности масштабироваться на многочисленные репозитории, привязанные к монодереву.

Итак, у меня есть запрос только одной фичи на GitHub:

Пожалуйста, реализуйте пулл-реквесты и трекинг багов, охватывающие различные репозитории одного монодерева.


Простая идея, огромные последствия.

Репозитории и организации

Во-первых, нужно сделать возможными многочисленные форки того же репозитория в одной организации. Просто посмотрите на git.kernel.org, большинство их репозиториев не являются личными. И даже если вы поддерживаете разные организации, например, для разных подсистем, требование наличия организации для каждого репозитория — глупое и избыточное, оно без какой-либо необходимости до предела затрудняет доступ и управление пользователями. Например, в графической подсистеме у нас было бы по одному репозиторию для каждого тестового набора userspace, общая библиотека userspace и общий набор инструментов и скриптов, которые используются мейнтейнерами и разработчиками, это GitHub поддерживается. Но потом вы добавите общий репозиторий подсистемы, плюс репозиторий для ключевой функциональности подсистемы и дополнительные репозитории для каждого крупного драйвера. Это всё форки, которые GitHub не делает. И у каждого из этих репозиториев будет куча веток: по крайней мере, одна для работы над функциями, а другая для исправления багов в текущем релизе.

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

Родственный вопрос: нужно иметь возможность устанавливать связи между форками постфактум. Для новых проектов, которые всегда были на GitHub, это не является проблемой. Но Linux сможет перемещать максимум одну подсистему за раз, и на GitHub уже есть масса репозиториев Linux, которые не являются правильными форками друг друга.

Пулл-реквесты

Пулл-реквесты нужно привязать к нескольким репозиториям одновременно, сохраняя единый общий поток обсуждения. Вы уже можете переназначить пулл-реквест на другую ветку репозитория, но не на несколько репозиториев одновременно. Переназначение пулл-реквестов действительно важно, поскольку новые участники проекта будут просто создавать пулл-реквесты к тому, что они считают основным репозиторием. Боты затем могут перетасовать их с учётом всех репозиториев, перечисленных в файле MAINTAINERS для того набора файлов и изменений, которые содержит репозиторий. Когда я беседовал с сотрудниками GitHub, то сначала предложил им реализовать это напрямую. Но думаю, здесь всё можно автоматизировать скриптами, так что лучше будет оставить это только для индивидуальных проектов, поскольку тут нет единого стандарта.

Тут ещё довольно мерзкая проблема UI, потому что список патчей может отличаться в зависимости от ветки, куда идёт пулл-реквест. Но это не всегда ошибка пользователя, ведь какой-то из репозиториев уже может применить какие-то патчи.

Кроме того, статус пулл-реквеста должен отличаться для каждого репозитория. Один мейнтейнер может закрыть его, не принимая, поскольку решили, что его примет другая подсистема, в то время как другой мейнтейнер может произвести слияние и закрыть вопрос. В другом дереве могут даже закрыть пулл-реквест как невалидный, поскольку он неприменим для старой версии или форка от вендора. Ещё веселее, пулл-реквест может пройти через слияние несколько раз, с разными коммитами в каждой подсистеме.

Баги

Как и пулл-реквесты, баги могут относиться ко многим репозиториям, и нужно иметь возможность их перемещать. В качестве примера приведём баг, который сначала сообщили для репозитория ядра. После сортировки стало ясно, что это баг драйвера, который всё ещё присутствует в последней ветке разработки и, следовательно, относится к этому репозиторию, плюс к основной upstream-ветке и может к парочке других.

Статусы опять же должны быть отдельными, поскольку после появления багфикса в одном репозитории он не становится сразу доступен для всех остальных. Может даже потребуется портировать его на предыдущие версии ядер и дистрибутивов, а кто-то может решить, что баг не стоит того и закроет его как WONTFIX, даже если он помечен как успешно разрешённый в соответствующем репозитории подсистемы.

Вывод: монодерево, а не монорепозиторий

Ядро Linux не собирается переходить на GitHub. Но переход на модель масштабирования Linux как монодерева с многочисленными репозиториями станет хорошей концепцией для GitHub и поможет всем очень крупным проектам, которые уже размещаются там. Мне кажется, это даст им новый и более эффективный способ решать свои уникальные проблемы.

Автор: m1rko

Источник

Поделиться

* - обязательные к заполнению поля