Привет Хабр! Предлагаю вашему вниманию перевод статьи Git Virtual File System Design History. Продолжение следует…
Виртуальная файловая система Git (Git Virtual File System, далее GVFS) была создана для решения двух основных задач:
В нашем случае основной сценарий использования GVFS — это репозиторий Windows с его 3 миллионами файлов в рабочей директории в сумме занимающих 270 Гбайт. Чтобы клонировать этот репозиторий вам придется скачать упаковочный файл (packfile) размером в 100 Гбайт, что займет несколько часов. Если вам все же удалось его клонировать, все локальные команды git вроде checkout (3 часа), status (8 минут) и commit (30 минут) выполнялись бы слишком долго из-за линейной зависимости от количества файлов. Несмотря на все эти сложности мы решили мигрировать весь код Windows в git. В то же время мы старались оставить git практически нетронутым, так как популярность git и количество общедоступной информации о нем были одними из основных причин для миграции.
Нужно отметить, что мы рассмотрели огромное количество альтернативных решений прежде чем решили создать GVFS. Более детально о том как работает GVFS мы опишем в следующих статьях, сейчас же сконцентрируемся на рассмотренных нами вариантах и почему была создана виртуальная файловая система.
Разберемся сразу с самым простым вопросом: зачем вообще кому-то нужен репозиторий таких размеров? Просто ограничьте размер ваших репозиториев и все будет в порядке! Правильно?
Не все так просто. Уже написано много статей о преимуществах монолитных репозиториев. Несколько больших команд в Microsoft уже пробовали разбивать свой код на множество маленьких репозиториев и в результате склонились к тому, что монолитный репозиторий лучше.
Разбить большое количество кода непросто, к тому же это не решение всех проблем. Это решило бы проблему масштабирования в каждом отдельном репозитории, но в то же время усложнило бы внесение изменений в несколько репозиториев одновременно и как результат релиз финального продукта стал бы более трудоемким. Получается, что за исключением проблемы масштабирования, процесс разработки в монолитном репозитории выглядит гораздо проще.
Набор инструментов VSTS состоит из нескольких связанных сервисов. Поэтому мы решили что разместив каждый из них в отдельном репозитории git мы сразу избавимся от проблемы масштабирования, и одновременно создадим физические границы между различными частями кода. На практике же эти границы ни к чему хорошему не привели.
Во-первых, нам все равно приходилось изменять код в нескольких репозиториях одновременно. Много времени при этом уходило на управление зависимостями и соблюдение правильной последовательности commit-ов и pull request-ов, что в свою очередь привело к созданию огромного количества сложных и неустойчивых утилит.
Во-вторых, значительно усложнился наш процесс релиза. Параллельно с релизом новой версии VSTS каждые три недели мы выпускаем коробочную версию TeamFoundation Server каждые три месяца. Для корректной работы TFS необходима установка всех сервисов VSTS на одном компьютере, то есть все сервисы должны понимать от каких версий других сервисов они зависят. Собирать воедино сервисы, которые в течение трех последних месяцев разрабатывались совершенно независимо оказалось непростой задачей.
В конце концов, мы поняли, что нам будет гораздо проще работать с монолитным репозиторием. В результате все сервисы зависели от одной и той же версии любого другого сервиса. Внесение изменений в один из сервисов требовало обновления всех сервисов, зависящих от него. Таким образом, немного больше работы в начале сэкономило нам кучу времени при релизах. Конечно же это означало, что нам впредь надо было более осторожно относиться к созданию новых и управлению существующими зависимостями.
Примерно по этим же причинам команда, работающая над Windows, решила перейти на Git. Код Windows состоит из нескольких компонент, которые теоретически могли бы быть разбиты на несколько репозиториев. Однако у этого подхода были две проблемы. Во-первых, несмотря на то что большинство репозиториев были небольшие, для одного из репозиторев (OneCore), который занимал около 100 Гбайт, нам все равно пришлось бы решать проблему масштабируемости. Во-вторых, такой подход ни коим образом не облегчил бы внесение изменений в несколько репозиториев одновременно.
Наша философия выбора инструментов разработки состоит в том, что эти инструменты должны способствовать правильной организации нашего кода. Если вы считаете, что ваша команда будет более эффективна работая в нескольких небольших репозиториях, инструменты разработки должны помочь вам в этом. Если же вам кажется, что команда будет более эффективна, работая с монолитным репозиторием, ваши инструменты не должна препятствовать вам в этом.
Итак за последние несколько лет мы потратили много времени пытаясь заставить Git работать работать с большими репозиториями. Перечислим некоторые из рассмотренных нами вариантов решения этой проблемы.
Сначала мы попробовали использовать подмодули. Git позволяет указать (reference) любой репозиторий как часть другого репозитория, что позволяет для каждого коммита в родительском репозитории указать коммиты в под-репозиториях от которых этот родительский коммит зависит и где именно в рабочей директории родителя эти коммиты должны быть размещены. Выглядит как идеальное решение для разбивки большого репозитория на несколько маленьких. И мы потратили несколько месяцев работая над утилитой командной строки для работы с подмодулями.
Основной сценарий применения подмодулей — это использование кода одного репозитория в другом. В каком-то роде подмодули — это те же самые пакеты npm и NuGet, т.е. библиотека или компонент, который не зависит от родителя. Любые изменения вносятся в первую очередь на уровне подмодуля (в конце концов это независимая библиотека со своим независимым процессом разработки, тестирования и релиза), а затем родительский репозиторий переключается на использование этой новой версии.
Мы решили, что могли бы воспользоваться этой идеей разбив наш большой репозиторий на несколько маленьких, а затем склеив их обратно в один супер-репозиторий. Мы даже создали утилиту, которая бы позволяла выполнять «git status» и коммитить код поверх всех репозиториев одновременно.
В конце концов мы забросили эту идею. Во-первых, стало понятно, что таким образом мы только усложняем жизнь разработчика: каждый коммит теперь стал двумя или более коммитами одновременно, так как необходимо было обновить как родительский, так и каждый из затрагиваемых подмодульных репозиториев. Во-вторых, в Git нет атомарного механизма для выполнения коммитов одновременно в нескольких репозиториях. Можно было бы конечно назначить один из серверов ответственным за транзакционность коммитов, но в итоге все опять упирается в проблему масштабируемости. И в-третьих, большинство разработчиков не хотят быть экспертами в системах контроля версий, они бы предпочли, чтобы доступные инструменты делали это за них. Для работы с git разработчику необходимо научиться работать с направленным ациклическим графом (DAG, Directed Acyclic Graph), что уже непросто, а тут мы просим его работать с несколькими слабосвязанными направленными ациклическими графами одновременно и к тому же следить за порядком выполнения checkout/commit/push в них. Это уже слишком.
Если не получилось с подмодулями, то может быть получится с несколькими репозиториями склеенными вместе? Подобный подход был применен андроидом в repo.py и мы тоже решили попробовать. Но ничего хорошего из этого не вышло. Работать в рамках одного репозитория стало проще, зато намного усложнился процесс внесения изменений в несколько репозиториев одновременно. И так как коммиты в разных репозиториях теперь совершенно не связаны друг с другом, непонятно какие коммиты из разных репозиториев должны быть выбраны для определенной версии продукта. Для этого понадобилась бы еще одна система контроля версий поверх Git.
В Git существует понятие запасных хранилищ объектов (alternate object store). Каждый раз когда git ищет коммит, дерево или блоб он начинает поиск c папки .git\objects, затем проверяет pack-файлы в папке .git\objects\pack, и наконец, если указано в настройках git-а, ищет в запасных хранилищах объектов.
Мы решили попробовать использовать сетевые папки в качестве запасных хранилищ чтобы избежать копирование огромного количества блобов с сервера при каждом clone и fetch. Такой подход более-менее решил проблему количества копируемых файлов из репозитория, но не проблему размера рабочей директории и индекса.
Еще одна неудачная попытка использования функционала Git не по назначению. Запасные хранилища были созданы в Git для избежания повторного клонирования объектов. При вторичном клонировании того же репозитория можно использовать хранилище объектов первого клона. Предполагается, что все объекты и pack-файлы находятся локально, доступ к ним моментален, нет надобности в дополнительном кэше. К сожалению, это не работает если запасное хранилище находится на другой машине в сети.
В Git существует возможность ограничить количество клонируемых коммитов. К сожалению, этого ограничения недостаточно для работы с огромными репозиториями вроде Windows, так как каждый коммит вместе со своими деревьями и блобами занимает до 80 Гбайт. Тем более, что в большинстве случаев для нормальной работы нам не нужно содержимое коммитов целиком.
К тому же поверхностное клонирование не решает проблемы большого количества файлов в рабочей директории.
При чекауте Git по умолчанию помещает все файлы из этого коммита в вашу рабочую директорию. Однако в файле .git\info\sparse-checkout можно ограничить список файлов и папок которые могут быть помещены в рабочую директорию. Мы возлагали большие надежды на этот подход поскольку большинство девелоперов работаю лишь с небольшим подмножеством файлов. Как оказалось, у частичных есть свои недостатки:
Несмотря на всё вышеперечисленное частичные чекауты оказались одной из основополагающих частей нашего подхода.
Каждый раз, когда мы изменяем файл больших размеров, в истории изменений Git создается его копия. Для экономии места Git-LFS подменяет эти большие блоб-файлы на их указатели, сами файлы при этом помещаются в отдельное хранилище. Таким образом, при клонировании вы скачиваете только указатели на файлы, а затем LFS скачивает только файлы, которые вы чекаутите.
Было непросто заставить LFS работать с репозиторием Windows. В итоге нам это удалось, что позволило существенно уменьшить общий размер репозитория. Но мы так и не решили проблему большого количества файлов и размера индекса. Пришлось отказаться и от этого подхода.
Вот к каким выводам мы пришли после всех вышеперечисленных экспериментов:
В итоге мы решили сосредоточиться на идее виртуальной файловой системы, основные преимущества которой включают:
Однако и без трудностей, конечно, не обойдется:
В следующей статье мы обсудим как эти проблемы были решены в GVFS.
Виртуальная файловая система Git (Git Virtual File System, далее GVFS) была создана для решения двух основных задач:
- Скачивать только файлы необходимые пользователю
- Локальные команды Git должны брать в расчет не всю рабочую директорию (working directory), а только файлы, с которыми работает пользователь
В нашем случае основной сценарий использования GVFS — это репозиторий Windows с его 3 миллионами файлов в рабочей директории в сумме занимающих 270 Гбайт. Чтобы клонировать этот репозиторий вам придется скачать упаковочный файл (packfile) размером в 100 Гбайт, что займет несколько часов. Если вам все же удалось его клонировать, все локальные команды git вроде checkout (3 часа), status (8 минут) и commit (30 минут) выполнялись бы слишком долго из-за линейной зависимости от количества файлов. Несмотря на все эти сложности мы решили мигрировать весь код Windows в git. В то же время мы старались оставить git практически нетронутым, так как популярность git и количество общедоступной информации о нем были одними из основных причин для миграции.
Нужно отметить, что мы рассмотрели огромное количество альтернативных решений прежде чем решили создать GVFS. Более детально о том как работает GVFS мы опишем в следующих статьях, сейчас же сконцентрируемся на рассмотренных нами вариантах и почему была создана виртуальная файловая система.
Предыстория
Почему монолитный репозиторий?
Разберемся сразу с самым простым вопросом: зачем вообще кому-то нужен репозиторий таких размеров? Просто ограничьте размер ваших репозиториев и все будет в порядке! Правильно?
Не все так просто. Уже написано много статей о преимуществах монолитных репозиториев. Несколько больших команд в Microsoft уже пробовали разбивать свой код на множество маленьких репозиториев и в результате склонились к тому, что монолитный репозиторий лучше.
Разбить большое количество кода непросто, к тому же это не решение всех проблем. Это решило бы проблему масштабирования в каждом отдельном репозитории, но в то же время усложнило бы внесение изменений в несколько репозиториев одновременно и как результат релиз финального продукта стал бы более трудоемким. Получается, что за исключением проблемы масштабирования, процесс разработки в монолитном репозитории выглядит гораздо проще.
VSTS (Visual Studio Team System)
Набор инструментов VSTS состоит из нескольких связанных сервисов. Поэтому мы решили что разместив каждый из них в отдельном репозитории git мы сразу избавимся от проблемы масштабирования, и одновременно создадим физические границы между различными частями кода. На практике же эти границы ни к чему хорошему не привели.
Во-первых, нам все равно приходилось изменять код в нескольких репозиториях одновременно. Много времени при этом уходило на управление зависимостями и соблюдение правильной последовательности commit-ов и pull request-ов, что в свою очередь привело к созданию огромного количества сложных и неустойчивых утилит.
Во-вторых, значительно усложнился наш процесс релиза. Параллельно с релизом новой версии VSTS каждые три недели мы выпускаем коробочную версию TeamFoundation Server каждые три месяца. Для корректной работы TFS необходима установка всех сервисов VSTS на одном компьютере, то есть все сервисы должны понимать от каких версий других сервисов они зависят. Собирать воедино сервисы, которые в течение трех последних месяцев разрабатывались совершенно независимо оказалось непростой задачей.
В конце концов, мы поняли, что нам будет гораздо проще работать с монолитным репозиторием. В результате все сервисы зависели от одной и той же версии любого другого сервиса. Внесение изменений в один из сервисов требовало обновления всех сервисов, зависящих от него. Таким образом, немного больше работы в начале сэкономило нам кучу времени при релизах. Конечно же это означало, что нам впредь надо было более осторожно относиться к созданию новых и управлению существующими зависимостями.
Windows
Примерно по этим же причинам команда, работающая над Windows, решила перейти на Git. Код Windows состоит из нескольких компонент, которые теоретически могли бы быть разбиты на несколько репозиториев. Однако у этого подхода были две проблемы. Во-первых, несмотря на то что большинство репозиториев были небольшие, для одного из репозиторев (OneCore), который занимал около 100 Гбайт, нам все равно пришлось бы решать проблему масштабируемости. Во-вторых, такой подход ни коим образом не облегчил бы внесение изменений в несколько репозиториев одновременно.
Философия дизайна
Наша философия выбора инструментов разработки состоит в том, что эти инструменты должны способствовать правильной организации нашего кода. Если вы считаете, что ваша команда будет более эффективна работая в нескольких небольших репозиториях, инструменты разработки должны помочь вам в этом. Если же вам кажется, что команда будет более эффективна, работая с монолитным репозиторием, ваши инструменты не должна препятствовать вам в этом.
Рассмотренные альтернативные варианты
Итак за последние несколько лет мы потратили много времени пытаясь заставить Git работать работать с большими репозиториями. Перечислим некоторые из рассмотренных нами вариантов решения этой проблемы.
Подмодули Git
Сначала мы попробовали использовать подмодули. Git позволяет указать (reference) любой репозиторий как часть другого репозитория, что позволяет для каждого коммита в родительском репозитории указать коммиты в под-репозиториях от которых этот родительский коммит зависит и где именно в рабочей директории родителя эти коммиты должны быть размещены. Выглядит как идеальное решение для разбивки большого репозитория на несколько маленьких. И мы потратили несколько месяцев работая над утилитой командной строки для работы с подмодулями.
Основной сценарий применения подмодулей — это использование кода одного репозитория в другом. В каком-то роде подмодули — это те же самые пакеты npm и NuGet, т.е. библиотека или компонент, который не зависит от родителя. Любые изменения вносятся в первую очередь на уровне подмодуля (в конце концов это независимая библиотека со своим независимым процессом разработки, тестирования и релиза), а затем родительский репозиторий переключается на использование этой новой версии.
Мы решили, что могли бы воспользоваться этой идеей разбив наш большой репозиторий на несколько маленьких, а затем склеив их обратно в один супер-репозиторий. Мы даже создали утилиту, которая бы позволяла выполнять «git status» и коммитить код поверх всех репозиториев одновременно.
В конце концов мы забросили эту идею. Во-первых, стало понятно, что таким образом мы только усложняем жизнь разработчика: каждый коммит теперь стал двумя или более коммитами одновременно, так как необходимо было обновить как родительский, так и каждый из затрагиваемых подмодульных репозиториев. Во-вторых, в Git нет атомарного механизма для выполнения коммитов одновременно в нескольких репозиториях. Можно было бы конечно назначить один из серверов ответственным за транзакционность коммитов, но в итоге все опять упирается в проблему масштабируемости. И в-третьих, большинство разработчиков не хотят быть экспертами в системах контроля версий, они бы предпочли, чтобы доступные инструменты делали это за них. Для работы с git разработчику необходимо научиться работать с направленным ациклическим графом (DAG, Directed Acyclic Graph), что уже непросто, а тут мы просим его работать с несколькими слабосвязанными направленными ациклическими графами одновременно и к тому же следить за порядком выполнения checkout/commit/push в них. Это уже слишком.
Несколько репозиториев собранных воедино
Если не получилось с подмодулями, то может быть получится с несколькими репозиториями склеенными вместе? Подобный подход был применен андроидом в repo.py и мы тоже решили попробовать. Но ничего хорошего из этого не вышло. Работать в рамках одного репозитория стало проще, зато намного усложнился процесс внесения изменений в несколько репозиториев одновременно. И так как коммиты в разных репозиториях теперь совершенно не связаны друг с другом, непонятно какие коммиты из разных репозиториев должны быть выбраны для определенной версии продукта. Для этого понадобилась бы еще одна система контроля версий поверх Git.
Запасные хранилища (alternates) Git
В Git существует понятие запасных хранилищ объектов (alternate object store). Каждый раз когда git ищет коммит, дерево или блоб он начинает поиск c папки .git\objects, затем проверяет pack-файлы в папке .git\objects\pack, и наконец, если указано в настройках git-а, ищет в запасных хранилищах объектов.
Мы решили попробовать использовать сетевые папки в качестве запасных хранилищ чтобы избежать копирование огромного количества блобов с сервера при каждом clone и fetch. Такой подход более-менее решил проблему количества копируемых файлов из репозитория, но не проблему размера рабочей директории и индекса.
Еще одна неудачная попытка использования функционала Git не по назначению. Запасные хранилища были созданы в Git для избежания повторного клонирования объектов. При вторичном клонировании того же репозитория можно использовать хранилище объектов первого клона. Предполагается, что все объекты и pack-файлы находятся локально, доступ к ним моментален, нет надобности в дополнительном кэше. К сожалению, это не работает если запасное хранилище находится на другой машине в сети.
Поверхностное клонирование (shallow clones)
В Git существует возможность ограничить количество клонируемых коммитов. К сожалению, этого ограничения недостаточно для работы с огромными репозиториями вроде Windows, так как каждый коммит вместе со своими деревьями и блобами занимает до 80 Гбайт. Тем более, что в большинстве случаев для нормальной работы нам не нужно содержимое коммитов целиком.
К тому же поверхностное клонирование не решает проблемы большого количества файлов в рабочей директории.
Частичный (sparse) чекаут
При чекауте Git по умолчанию помещает все файлы из этого коммита в вашу рабочую директорию. Однако в файле .git\info\sparse-checkout можно ограничить список файлов и папок которые могут быть помещены в рабочую директорию. Мы возлагали большие надежды на этот подход поскольку большинство девелоперов работаю лишь с небольшим подмножеством файлов. Как оказалось, у частичных есть свои недостатки:
- Действие частичных чекаутов не распространяется на индекс, только на рабочую директорию. Если даже вы ограничите размер рабочей директории до 50 тысячи файлов, индекс будет все равно включать все 3 миллиона файлов;
- Частичный чекаут статичен. Если вы включили директорию А, а кто-нибудь добавил позже зависимость на директорию B, ваш билд будет сломан до тех пор, пока вы не включите B в список директорий для частичного чекаута;
- Часчтичный чекаут не распространяется на файлы, скачиваемые при операциях clone и fetch, так что если даже ваши чекауты не имеют никакого отношения к 95% файлов вам все равно придется их скачать;
- UX (User experience) частчиных чекаутов неудобен в использовании
Несмотря на всё вышеперечисленное частичные чекауты оказались одной из основополагающих частей нашего подхода.
Хранилище для больших файлов (LFS, Large File Storage)
Каждый раз, когда мы изменяем файл больших размеров, в истории изменений Git создается его копия. Для экономии места Git-LFS подменяет эти большие блоб-файлы на их указатели, сами файлы при этом помещаются в отдельное хранилище. Таким образом, при клонировании вы скачиваете только указатели на файлы, а затем LFS скачивает только файлы, которые вы чекаутите.
Было непросто заставить LFS работать с репозиторием Windows. В итоге нам это удалось, что позволило существенно уменьшить общий размер репозитория. Но мы так и не решили проблему большого количества файлов и размера индекса. Пришлось отказаться и от этого подхода.
Виртуальная файловая система (Virtual file system)
Вот к каким выводам мы пришли после всех вышеперечисленных экспериментов:
- Монолитный репозиторий это наш единственный путь
- Большинству девелоперов для работы необходимо лишь небольшое подмножество файлов в репозитории. Но им важно иметь возможность вносить изменения в любой части этого репозитория
- Мы бы хотели использовать существующий клиент git не внося в него огромное количество изменений
В итоге мы решили сосредоточиться на идее виртуальной файловой системы, основные преимущества которой включают:
- Скачивание только необходимого минимума блоб-файлов. Для большинства разработчиков это означает порядка 50-100 тысяч файлов и их истории изменений. Большая же часть репозитория так и не будет никогда скопирована.
- С небольшими ухищрениями можно заставить git брать в расчет только файлы, с которыми работает девелопер. Таким образом, операции вроде git status и gitcheckout будут выполняться гораздо быстрее, чем если бы они выполнялись на всех 3 миллионах файлов
- С помощью частичных чекаутов мы можем размещать только используемые фалы в рабочую директорию. И что более важно, каждый чекаут будет ограничен только файлами необходимыми разработчику
- Все используемые нами инструменты разработки продолжат работать без изменений, файловая система позаботится о том, чтобы запрашиваемые файлы были всегда доступны
Однако и без трудностей, конечно, не обойдется:
- Разработка файловой системы дело непростое
- Производительность должна быть на высоте. Нам могут простить небольшую задержку при первом доступе к файлу, но повторный доступ к этому же файлу должен быть практически моментальным
- Задержки в доступе к файлам могут и должны быть уменьшены с помощью предзагрузки. Как правило, большое количество мелких объектов привносит наибольшие задержки. Примером может служить огромное количество очень маленьких объектов-деревьев git
- Нам еще предстоит понять, как заставить git работать с виртуальной файловой системой. Как избежать обхода git-ом всех 3.5 миллионов файлов если для git это выглядит так будто все эти файлы действительно находятся на диске? Хорошо ли работают команды git с учетом настроек частичных чекаутов? Возможно ли такое что git обойдет слишком много блоб-файлов?
В следующей статье мы обсудим как эти проблемы были решены в GVFS.