Многие выбрали Git за его гибкость: в частности, модель веток и слияний позволяют эффективно децентрализовать разработку. В большинстве случаев эта гибкость является плюсом, однако некоторые сценарии поддержаны не так элегантно. Один из них — это использование Git для больших монолитных репозиториев — монорепозиториев. Эта статья исследует проблемы монорепозиториев в Git и предлагает способы их смягчения.

Скала Улуру
Скала Улуру в Австралии как пример монолита — КДПВ, не более

Что такое монорепозиторий?


Определения разнятся, но мы будем считать репозиторий монолитным при выполнении следующих условий:
  • Репозиторий содержит более одного логического проекта (например, iOS-клиент и веб-приложение)
  • Эти проекты могут быть не связаны, слабо связаны или связаны сторонними средствами (например, через систему управления зависимостями)
  • Репозиторий большой во многих смыслах:
    • По количеству коммитов
    • По количеству веток и/или тегов
    • По количеству файлов
    • По размеру содержимого (то есть размеру папки .git)

В каких случаях монорепозитории удобны?


Мне видится пара возможных сценариев:
  • Репозиторий содержит набор сервисов, фреймворков и библиотек, составляющих единое логическое приложение. Например, множество микросервисов и совместно используемых библиотек, которые все вместе обеспечивают работу приложения foo.example.com.
  • Семантическое версионирование артефактов не требуется или не приносит особой пользы из-за того, что репозиторий используется в самых разных окружениях (например, staging и production). Если нет необходимости отправлять артефакты пользователям, то исторические версии, вроде 1.10.34, могут стать ненужными.

При таких условиях предпочтение может быть отдано единому репозиторию, поскольку он позволяет гораздо проще делать большие изменения и рефакторинги (к примеру, обновить все микросервисы до конкретной версии библиотеки).
У Facebook есть пример такого монорепозитория:
С тысячами коммитов в неделю и сотнями тысяч файлов, главный репозиторий исходного когда Facebook громаден — во много раз больше,
чем даже ядро Linux, в котором, по состоянию на 2013 год, находилось 17 миллионов строк кода в 44 тысячах файлов.

При проведении тестов производительности в Facebook использовали тестовый репозиторий со следующими параметрами:
  • 4 миллиона коммитов
  • Линейная история
  • Около 1.3 миллиона файлов
  • Размер папки .git около 15 Гб
  • Файл индекса размером до 191 Мб

Концептуальные проблемы


С хранением несвязанных проектов в монорепозитории Git возникает много концептуальных проблем.

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

Тег в Git — это именованный указатель на определённый коммит, который, в свою очередь, ссылается на целое дерево. Однако польза тегов уменьшается в контексте монорепозитория. Посудите сами: если вы работаете над веб-приложением, которое постоянно развёртывается из монорепозитория (Continuous Deployment), какое отношение релизный тег будет иметь к версионированному клиенту под iOS?

Проблемы с производительностью


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

Количество коммитов


Хранение несвязанных проектов в едином большом репозитории может оказаться хлопотным на уровне коммитов. С течением времени такая стратегия может привести к большому числу коммитов и значительному темпу роста (из описания Facebook — "тысячи коммитов в неделю"). Это становится особенно накладно, поскольку Git использует направленный ациклический граф (directed acyclic grap — DAG) для хранения истории проекта. При большом числе коммитов любая команда, обходящая граф, становится медленнее с ростом истории.

Примерами таких команд являются git log (изучение истории репозитория) и git blame (аннотирование изменений файла). При выполнении последней команды Git придётся обойти кучу коммитов, не имеющих отношение к исследуемому файлу, чтобы вычислить информацию о его изменениях. Кроме того, усложняется разрешение любых вопросов достижимости: например, достижим ли коммит A из коммита B. Добавьте сюда множество несвязанных модулей, находящихся в репозитории, и проблемы производительности усугубятся.

Количество указателей (refs)


Большое число указателей — веток и тегов — в вашем монорепозитории влияют на производительность несколькми путями.
Анонсирование указателей содержит каждый указатель вашего монорепозитория. Поскольку анонсирование указателей — это первая фаза любой удалённой Git операции, под удар попадают такие команды как git clone, git fetch или git push. При большом количестве указателей их производительность будет проседать. Увидеть анонсирование указателей можно с помощью команды git ls-remote, передав ей в качестве аргумента URL репозитория. Например, такая команда выведет список всех указателей в репозитории ядра Linux:
git ls-remote git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

Если указатели хранятся не в сжатом виде, перечисление веток будет работать медленно. После выполнения команды git gc указатели будут упакованы в единый файл, и тогда перечисление даже 20.000 указателей станет быстрым (около 0.06 секунды).

Любая операция, которая требует обхода истории коммитов репозитория и учитывает каждый указатель (например, git branch --contains SHA1) в монорепозитории будет работать медленно. К примеру, при 21.708 указателях поиск указателя, содержащего старый коммит (который достижим из почти всех указателей), занял на моём компьютере 146.44 секунды (время может отличаться в зависимости от настроек кеширования и параметров носителя информации, на котором хранится репозиторий).

Количество учитываемых файлов


Индекс (.git/index) учитывает каждый файл в вашем репозитории. Git использует индекс для определения, изменился ли файл, выполняя stat(1) для каждого файла и сравнивая информацию об изменении файла с информацией, содержащейся в индексе.

Поэтому количество файлов в репозитории оказывает влияние на производительность многих операций:
  • git status может работать медленно, т.к. эта команда проверяет каджый файл, а индекс-файл будет большим
  • git commit также может работать медленно, поскольку проверяет каждый файл

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

Большие файлы


Большие файлы в одном поддереве/проекте влияют на производительность всего репозитория. Например, большие медиа-файлы, добавленные в проект iOS-клиента в монорепозитории, будут клонироваться даже разработчикам, работающим над совершенно другими проектами.

Комбинированные эффекты


Количество и размер файлов в сочетании с частотой их изменений наносят ещё больший удар по производительности:
  • Переключение между ветками или тегами, которое актуально в контексте поддерева (например, поддерево, с которым я работаю), по-прежнему обновляет дерево целиком. Этот процесс может быть медленным из-за большого числа затрагиваемых файлов, однако существует обходное решение. К примеру, следующая команда обновит папку ./templates так, чтобы она соответстовала указанной ветке, но при этом не изменит HEAD, что приведёт к побочному эффекту: обновлённые файлы будут отмечены в индексе как изменённые:
    `git checkout ref-28642-31335 -- templates`
  • Клонирование и загрузка (fetching) замедляются и становятся ресурсоёмкими для сервера, поскольку вся информация упаковывается в pack-файл перед отправкой.
  • Сборка мусора становится долгой и по умолчанию вызывается при выполнении git push (сама сборка при этом происходит только в том случае, если она необходима).
  • Любая команда, включающая создание pack-файла, например git upload-pack, git gc, требует значительных ресурсов.

Что насчёт Bitbucket?


Как следствие описанных эффектов, монолитные репозитории — это испытание для любой системы управления Git-репозиториями, и Bitbucket не является ислючением. Ещё важнее то, что порождаемые монорепозиториями проблемы требуют решения как на стороне сервера, так и клиента.
Параметр Влияние на сервер Влияние на пользователя
Большие репозитории (много файлов, большие файлы или и то, и другое) Память, CPU, IO, git clone нагружает на сеть, git gc медленный и ресурсоёмкий Клонирование занимает значительное время, — как у разработчиков, так и на CI
Большое количество коммитов git log и git blame работают медленно
Большое количество указателей Просмотр списка веток, анонсирование указателей занимают значительное время (git fetch, git clone, git push работают медленно) Страдает доступность
Большое количество файлов Коммиты на стороне сервера становятся долгими git status и git commit работают медленно
Большие файлы См. "Большие репозитории" git add для больших файлов, git push и git gc работают медленно

Стратегии смягчения последствий


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

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

Удалите указатели


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

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

Обращение с большим количеством файлов


Если в вашем репозитории много файлов (их число достигает десятков и сотен тысяч штук), поможет быстрый локальный диск и достаточный объём памяти, которая может быть использована для кеширования. Эта область потребует более значительных изменений на клиентской стороне, подобных тем, которые Facebook реализовал для Mercurial.

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

Используйте Git LFS (Large File Storage — хранилище для больших файлов)


Для проектов, которые содержат большие файлы, например, видео или графику, Git LFS является одним из способов уменьшения их влияния на размер и общую производительность репозитория. Вместо того, чтобы хранить большие объекты в самом репозитории, Git LFS под тем же имененм хранит маленький файл-указатель на этот объект. Сам объект хранится в специальном хранилище больших файлов. Git LFS встраивается в операции push, pull, checkout и fetch, чтобы прозрачно обеспечить передачу и подстановку этих объектов в рабочую копию. Это означает, что вы можете работать с большими файлами так же, как обычно, при этом не раздувая ваш репозиторий.

Bitbucket Server 4.3 полностью поддерживает Git LFS v1.0+, а кроме того, позволяет просматривать и сравнивать большие графические файлы, хранящиеся в LFS.

Git LFS в Bitbucket Server

Мой коллега Стив Стритинг активно участвует в разработке проекта LFS и не так давно написал о нём статью.

Определите границы и разделите ваш репозиторий


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

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


Штефан Заазен — архитектор Atlassian Bitbucket. Страсть к DVCS привела его к миграции команды Confluence с Subversion на Git и, в конечном итоге, к главной роли в разработке того, что сейчас известно под названием Bitbucket Server. Штефана можно найти в Twitter под псевдонимом @stefansaasen.

Комментарии (28)


  1. musuk
    28.03.2016 18:52
    +13

    Тут бы ещё рассказ про то, как готовить git submodules не помешал.


    1. detouched
      28.03.2016 19:10
      +3

      Спасибо, учту Ваше пожелание. Есть пара хороших статей на эту тему, обязательно до них доберусь.


    1. semenyakinVS
      28.03.2016 23:08
      +1

      Вот. Кое-какие мысли по поводу и некоторое обсуждение. Может, пригодится.


  1. bak
    28.03.2016 20:21

    Ещё бы они начали поддерживать lfs на самом bitbucket. Уже год тикет висит а подвижек не видно.


    1. detouched
      29.03.2016 03:14

      Неправда Ваша, работа кипит.


  1. slonopotamus
    28.03.2016 20:36

    становится неуклюжим для репозитория со многими несвязанными проектами.


    Репозиторий содержит набор сервисов, фреймворков и библиотек, составляющих единое логическое приложение.


    Вы уже как-то определитесь, они единое логическое приложение или таки не связаны.


    1. wizardsd
      28.03.2016 21:14
      +2

      BitBucket Server 4.3 уже поддерживает LFS.


    1. firegurafiku
      28.03.2016 23:10
      +2

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


    1. ilammy
      29.03.2016 03:17
      +2

      Важна даже не логическая связанность (части одного приложения). Git следит на всем деревом сразу. То есть вопрос о том, надо ли всё держать в одном репозитории или следует разбить на несколько, можно переформулировать следующим образом: есть ли смысл в том, чтобы разные части имели разные версии? Если да, то их следует поместить в отдельные репозитории, если нет — то в разные. Например, вполне можно представить случай, когда есть Сервер v.4 годовой давности и Сервис v.10, выпущенный месяц назад. Однако, если поставляется сразу цельное решение, то вполне можно и все сваливать в одну кучу.
      Проблема, в общем, в том, что Git продолжают считать просто интеллектуальным rsynс'ом по типу SVN: «Я жмякаю Commit. Мои изменения улетают куда-то на сервер. После этого Вася может их забрать, а я могу не волноваться, что мой жёсткий диск сломается». Коммиты как таковые не считаются чем-то самостоятельным, скорее отражением истории. Отсюда и естественность мысли о том, что Отдел №1 может возиться в своей части репозитория, а Отдел №2 — в своей, вроде бы не должны мешать, поэтому достаточно только одного репозитория. И ветки тоже не нужны, потому что история линейна как ось времени, а ветки только всё запутывают. В то же время Git считает коммиты версиями репозитория в целом, а не его отдельных файликов или частей.
      В Линуксе тоже много частей и тысячи разработчиков, но это не мешает им всем работать в одном логическом репозитории. Как им это удаётся? Просто все не коммитят сразу в master центрального репозитория.


      1. gearbox
        29.03.2016 11:56

        есть ли смысл в том, чтобы разные части имели разные версии?

        Здравая абстракция, запомню. Но есть ситуации когда и она может сбоить. Например мобильная версия приложения под разные платформы + сайт + расширения для броузеров. Логически можно разделить, по факту могут совместно использоваться ресурсы (картинки, шрифты). Сильно не копал в эту сторону но вроде у svn была возможность подключать один репозиторий как часть другого. В общем, на мой дилетантский взгляд — системам версионного контроля нужно нечто вроде view у баз данных. Сделал вьюху от репы, в которой только тебе нужные файлы и работаешь. Если коммиты других разрабов твое не трогают — ты о них и не знаешь. Ели кто что зацепил — делаешь pull и смотришь.


  1. ivanych
    28.03.2016 21:40
    +23

    Краткое содержание статьи:

    1. Свалим в один репозиторий то, что должно быть в разных
    2. Получим проблему
    3. Героически решим эту проблему


    1. funca
      29.03.2016 08:27
      +3

      В том же Subversion мультикомпонентные проекты нормально живут в рамках одного репозитория. Нет проблем ни с размерами (при клонировании отдельной директории не копируется ни чего лишнего и разработчик может делать любой срез из исходников проекта, который посчитает нужным), ни с версионированием (грубо говоря, в каждой директории своя история).
      А вот у решивших мигрировать из SVN в Git подобные вопросы возникают постоянно: и с размером, ведь Git не может не копировать весь репозиторий, и с версионированием отдельных частей, поскольку история в git не связана с файловой структурой проекта. Решение о том, чтобы ввести понятие компонентов для каких-то срезов дерева исходников, чтобы разложить проект на отдельные git репозитории, не всегда подходит. К тому же это создает новые проблемы с администрированием такой структуры.


      1. ivanych
        29.03.2016 08:44
        +5

        Решение о том, чтобы ввести понятие компонентов для каких-то срезов дерева исходников, чтобы разложить проект на отдельные git репозитории, не всегда подходит.

        Всегда подходит. И имено так и следует делать.


    1. asm0dey
      29.03.2016 08:42

      Тут как не повернись проблема будет. Вот у нас есть проект из 8 микросервисов и число их растёт. Напрямую они друг от друга не зависят никак, только API разделяют. Либо мы всё это суём в один проект и один репозиторий и тогда имеет простоту миграции API, либо по разным репозиториям и тогда не имеем проблемы быстроразрастающегося репозитория. Но имеем целый геморрой с любым изменением в API.


      1. detouched
        29.03.2016 08:50
        -1

        Есть подозрение, что 8 — это маленькое число, и если Вам удобнее держать их вместе, то надо так и делать, пока что не опасаясь разрастания репозитория, — достаточно помнить о бинарных файлах (в смысле, что добавлять их надо аккуратно или хранить в LFS).
        Проблемы, связанные с количеством файлов, коммитов и веток становятся заметными при очень больших абсолютных значениях.


      1. 183614956
        29.03.2016 14:59

        Поскольку они разделяют API, стоит их держать в одном репозитории. Они же должны работать вместе. А когда в один репозиторий складывают приложение под Android и расширение для Firefox и бэкэнд сайта то тут уже нужно задуматься.


  1. PavelMSTU
    28.03.2016 22:22
    -1

    Может быть решение проблемы "монолитного репозитория" — это Mercurial?


    1. DragonFire
      29.03.2016 02:34

      не, тормозит еще больше гита… по крайней мере мы от него отказались недавно


      1. PavelMSTU
        31.03.2016 17:00
        -1

        Чем же тормозит когда данные хранятся инкрементально?..
        Один коммит связан только со своим предком.

        На моем опыте — проблем не было.
        31 проект в одной папочке, каждый примерно по 45-80 *.py файлов…
        Полет нормальный.

        Единственный важный нюанс: на Win Mercurial лучше не ставить, мягко говоря… Два раза были проблемы с кодировками… Собственный опыт… :(

        Вот хороший пост про отличия Mercurial и Git:https://habrahabr.ru/post/168675/


    1. detouched
      29.03.2016 03:21

      Как упомянуто в статье, Mercurial тоже надо (было) допиливать для действительно большого репозитория. И эта доработка, на самом деле, решает лишь часть проблемы.


      1. SowingSadness
        29.03.2016 15:09

        А какие не решает?


        1. detouched
          29.03.2016 15:17

          Я имел в виду то, что созданные в Facebook расширения опираются на наличие memcache — он уже был у FB для других целей, так что его просто переиспользовали. Поэтому это уже не Mercurial сам по себе.


  1. scy
    28.03.2016 23:10
    +1

    Согласен. Монорепозиторий — это зло, в подавляющем большинстве случаев, поэтому статья скорее о том как не надо делать.
    А сабмодули — это решение, идеальное для фронт + бэк, менее идеальное для ядро+кастомизация.
    А когда ты файсбук проблемы твои далеки от обычных) так что не надо примерять к себе.


  1. ivlis
    29.03.2016 02:04
    +4

    Вот так бывает, когда из git пытаешься делать svn...


  1. kmmbvnr
    29.03.2016 05:19
    +5

    Мм, все так уверены что монорепозиторий зло. А гугл именно его и использует — http://www.wired.com/2015/09/google-2-billion-lines-codeand-one-place/


    И вот подробное объяснение почему — http://www.infoq.com/presentations/Development-at-Google


    1. kmmbvnr
      29.03.2016 05:22

      Упс… вот это видео более конкретно — Why Google Stores Billions of Lines of Code in a Single Repository


  1. kohver
    29.03.2016 14:59
    +1

    Babel тоже использует monorepo, вот тут доходчиво об этом написали github.com/babel/babel/blob/master/doc/design/monorepo.md


    1. dfuse
      31.03.2016 23:56

      Babel отличный пример почему монорепо — не зло, если использовать с умом. Там все очень взаимосвязано, а отдельные пакеты очень маленькие. Менеджить такое в отдельных репозиториях была бы такая боль, что лучше об этом вовсе не думать даже.