Монолитные репозитории в Git

в 14:41, , рубрики: bitbucket, Git, монолитный репозиторий, монорепозиторий, производительность, метки: ,

Многие выбрали 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 чрезвычайно успешным и популярным, это не означает, что стóит отказываться от возможностей Git только потому, что ваш репозиторий монолитный: в большинстве случаев, для возникающих проблем есть работающие решения.


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

Автор: detouched

Источник

Поделиться новостью

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