Расширяем Git и Mercurial репозитории с помощью Amazon S3

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

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

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

    Далее мы на примере рассмотрим интеграцию систем контроля версий и Amazon S3 (облачного хранилища файлов), чтобы использовать преимущества обоих решений и компенсировать недостатки.

    Решение написано на C#, использует API Amazon Web Services и показан пример настройки для Mercurial репозитория. Код открыт, ссылка будет в конце статьи. Все написано более или менее модульно, так что добавление поддержки чего-то кроме Amazon S3 не должно составить труда. Могу предположить, что для GIT настроить будет так же легко.

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

    Реализовать интеграцию с системой контроля версий можно с помощью так называемых хуков (hook) — событий, которым можно назначить собственные обработчики. Нас интересуют такие, которые запускаются в момент получения или отправки данных в другой репозиторий. У Mercurial нужные хуки называются incoming и outgoing. Соответственно, нужно реализовать по одной команде на каждое событие. Одна для загрузки обновленных данных из рабочей папки в облако, а вторая — для обратного процесса — загрузки обновлений из облака в рабочую папку.

    Интеграция с репозиторием осуществляется с помощью файла с метаданными или индексного файла или как вам будет угодно. Этот файл должен содержать описание всех отслеживаемых файлов, как минимум пути к ним. И именно этот файл будет находиться под контролем версий. Сами отслеживаемые файлы будут находиться в .hgignore, списке игнорируемых файлов, иначе пропадает весь смысл этой затеи.

    Интеграция с репозиторием


    Файл с метаданными выглядит как-то так:

    <?xml version="1.0" encoding="utf-8"?>
    <assets>
      <locations>
        <location>Content\Textures</location>
        <location>Content\Sounds</location>
        <location searchPattern="*.pdf">Docs</location>
        <location>Reference Libraries</location>
      </locations>
      <amazonS3>
        <accesskey>*****************</accesskey>
        <secretkey>****************************************</secretkey>
        <bucketname>mybucket</bucketname>
      </amazonS3>
      <files>
        <file path="Content\Textures\texture1.dds" checksum="BEF94D34F75D2190FF98746D3E73308B1A48ED241B857FFF8F9B642E7BB0322A"/>
        <file path="Content\Textures\texture1.psd" checksum="743391C2C73684AFE8CEB4A60B0317E634B6E54403E018385A85F048CC5925DE"/>
    	<!-- И так далее для каждого отслеживаемого файла -->
      </files>
    </assets>
    

    В этом файле три секции: locations, amazonS3 и files. Первые две настраиваются пользователем в самом начале, а последняя используется самой программой для отслеживания самих файлов.

    Locations — это пути, по которым будет осуществляться поиск отслеживаемых файлов. Это либо абсолютные пути, либо пути относительно этого xml файла с настройками. Эти же пути нужно добавить в ignore файл системы контоля версий, чтобы она сама не пыталась их отслеживать.

    AmazonS3 — это, как не трудно догадаться, настройки облачного хранилища файлов. Первые два ключа — это Access Keys, которые можно сгенерировать для любого пользователя AWS. Они используются для того, чтобы криптографически подписывать запросы к API Амазона. Bucketname — это имя бакета, сущности внутри Amazon S3, которая может содержать файлы и папки и будет использоваться для хранения всех версий отслеживаемых файлов.

    Files настраивать не нужно, так как эту секцию будет редактировать сама программа в процессе работы с репозиторием. Там будет содержаться список всех файлов текущей версии с путями и хешами к ним. Таким образом, когда вместе с pull мы заберем новую версию этого xml файла, то, сравнив содержимое секции Files с содержимым самих отслеживаемых папок, можно понять, какие файлы были добавлены, какие — изменены, а какие — просто перемещены или переименованы. Во время push сравнение выполняется в обратную сторону.

    Интеграция с системой контроля версий


    Теперь о самих командах. Программа поддерживает три команды: push, pull и status. Первые две предназначены для настройки соответствующих хуков. Status выводит информафию об отслеживаемых файлах и ее вывод похож на вывод hg status — по нему можно понять, какие файлы были добавлены в рабочую папку, изменены, перемещены и каких файлов там не хватает.

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

    Здесь может быть четыре разных ситуации:

    1. Рабочая папка содержит новый файл. Это происходит, когда нет совпадений ни по путяи, ни по хешам. В результате обновляется xml файл, в него добавляется запись о новом файла, а также сам файл загружается в S3.
    2. Рабочая папка содержит измененный файл. Это происходит, когда есть совпадение по пути, но нет совпадения по хешу. В результате обновляется xml файл, у соответствующей записи изменяется хеш, а в S3 загружается обновленная версия файла.
    3. Рабочая папка содержит перемещенный или переименованный файл. Это происходит, когда есть совпадение по хешу, но нет совпадения по пути. В результате обновляется xml файл, у соответствующей записи изменяется путь, а в S3 ничего загружать не нужно. Дело в том, что ключем для хранения файлов в S3 является хеш, а информация о пути фиксируется только в xml файле. В данном случае хеш не изменился, поэтому загружать повторно тот же файл в S3 не имеет смысла.
    4. Отслеживаемый файл бы удален из рабочей папки. Это происходит, когда одной из записей xml файла не соответствует ни один из локальных файлов. В результате эта запись удаляется из xml файла. Из S3 никогда ничего не удаляется, так как основное его назначение — хранить все версии файлов, чтобы можно было откатиться на любую ревизию.

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

    Команда pull также сравнивает список файлов из xml со списком локальных файлов и работает совершенно аналогично, только в другую сторону. Например, когда в xml содержится запись о новом файле, т. е. нет совпадения ни по пути ни по хешу, то этот файл скачивается из S3 и записывается локально по указанному пути.

    Пример hgrc с настроеными хуками:

    [hooks]
    postupdate = \path\to\assets.exe pull \path\to\assets.config \path\to\checksum.cache
    prepush = \path\to\assets.exe push \path\to\assets.config \path\to\checksum.cache
    

    Хеширование


    Обращения к S3 сведены к минимуму. Используются только две комманды: GetObject и PutObject. Файл загружается и скачиваестя из S3 только в случае, если это новый либо измененный файл. Это возможно благодаря использованию хеша файла в качестве ключа. Т. е. физически все версии всех файлов лежат в S3 Bucket без всякой иерархии, без папок вообще. Здесь есть очевидный минус — коллизии. Если вдруг у двух файлов будет одинаковый хеш, то информация об одном из них просто не зафиксируется в S3.

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

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

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

    Оптимизации


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

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

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

    путь к файлу|хеш файла|дата вычисления хеша
    

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

    Также оптмизацией можно с натяжкой назвать использование BufferedStream вместо оригинального FileStream для чтения файлов, в том числе для чтения с целью вычисления хеша. Тесты показали, что использование BufferedStream с размером буфера в 1 мегабайт (вместо стандартного для FileStram 8 килобайт) для вычисления хешей 10 тысяч файлов общим размером более гигабайта ускоряет процесс в четыре раза по сравнению с FileStream для стандартного HDD. Если файлов не так много и они сами по себе размером больше мегабайта, то разница уже не такая существенная и составляет что-то около 5-10 процентов.

    Amazon S3


    Здесь стоит прояснить два момента. Самый главный — это, вероятно, цена вопроса. Как известно, для новых пользователей первый год использования бесплатный, если не выходить за лимиты. Лимиты следующие: 5 гигабайт, 20000 GetObject запросов в месяц и 2000 PutObject запросов в месяц. Если платить полную стоимость, то месяц будет стоит порядка $1. За это вы получаете резервирование по нескольким датацентрам в пределах региона и хорошие скорости.

    Также, смею предположить, что читателя с самого начала мучает следующий вопрос — зачем этот велосипед, если есть Dropbox? Дело в том, что использование Dropbox напрямую для совместной работы чревато — он совершенно не справляется с конфликтами.

    Но что, если использовать не напрямую? На самом деле в описанном решении Amazon S3 можно с легкостью заменить на тот же Dropbox, Skydrive, BitTorrent Sync или другие аналоги. В этом случае они будут выступать в качестве хранилища всех версий файлов, а хеши будут использоваться в качестве имен файлов. В моем решении это реализовано через FileSystemRemoteStorage, аналог AmazonS3RemoteStorage.

    Обещанная ссылка на исходный код: bitbucket.org/openminded/assetsmanager
    Share post

    Similar posts

    Comments 23

      0
      Интересное описание возможностей. Спасибо.

      PS: К слову сказать — да, гитхаб выпилил уже раздел «Downloads» для бинарников, но Bitbucket всё еще — нет.
      Приходится держать два репозитория для взаимной компенсации возможностей.
        +1
        Гитхаб же на днях запилил релизы: github.com/blog/1547-release-your-software

        Edit: да, это не сильно большой выход для больших бинарных ассетов
          0
          О! Не знал, спасибо за линк.

          Однако, справедливости ради, надо отметить что «кнопка» releases слепая и мелкая, а главное, не такая интуитивно понятная для конечного пользователя, как Download.
        0
        А можно амазоновское хранилище подключать в качестве файловой системы?
          0
          Например s3fs.
            0
            В таком случае можно на этой файловой системе просто создать репозиторий Git или Mercurial.
              0
              Это не решит проблемы. DVSC с большими файлами работают плохо. Хостинги репозиториев с большими файлами не работают вообще. Т. е. такой репозиторий будет неюзабельным и его нельзя будет держать на Github или Bitbucket.
                0
                git-annex и hg largefiles.
                  0
                  Насчет первого не знаю, а второй не поддерживается в Bitbucket.
            0
            Нет. S3 предоставляет сервисы — операции, которые можно выполнять над хранилищем. Вы через API делаете запросы и получаете ответы. Также оно умеет делать публичные статические или временные ссылки на контент. Торренты создавать. Все остальное придется реализовать самому поверх этих возможностей.
            0
            Хотелось бы описание отличий (плюсов/минусов) по сравнению с largefiles (стандартное расширение Mercurial для больших бинарных файлов).
              0
              Largefiles не поддерживается в Bitbucket. Это была главная причина написания велосипеда.
                +1
                Тогда наверное стоит написать об этом в статье. А безотносительно к этому есть какие-то преимущества? И ещё, насколько я понимаю по описанию с сайта Mercurial (сам не использовал largefiles), можно пушить и в репозитории, не поддерживающие эту фичу, просто не будет этих больших файлов (будут только их контрольные суммы). Собственно, у вас они тоже не хранятся в репозитории, но сделано более удобно (для случаев, когда удалённый репозиторий не поддерживает это расширение). Кстати, вот и достаточно весомый плюс хостить свои репозитории на своём сервере — можно включать любые расширения :)
              0
              К подобной программе нет жестких требований к производительности. Работает она одну секунду или пять — не столь важно.

              Вопрос а чем собственно предложенное решение лучше чем родное хранение бинарников в меркуриал или гит?
                –1
                Меркуриал и, вероятно, гит используют сжатые дельты для хранения изменений файлов. Сами файлы в репозитории не хранятся. Таким образом, если большой бинарный файл раз 10 менялся, то в репозитории будет храниться 10 сжатых дельт размером с сам файл. При выполнении update придется все эти дельты разжать и последовательно применить к рабочей папке. Это абсолютно ненужный оверхед. В предложенном решении такой файл просто бы скачался один раз из облака. Кроме того публичные хостинги репозиториев явно либо неявно не поддерживают большие файлы. Так как им выгоднее предоставлять платные высокоуровневые сервисы, чем низкоуровневые — размер хранилища и пропускная способность.
                  0
                  Меркуриал и гит не хранят дельты если размер дельт больше определенного процента размера файла, в такой ситуации они хранят копии файлов:
                  Для меркуриала есть механизм снепшотов который гарантирует что не требуется накладывать больше дельт чем нужно для получения целого файла
                  Гит оперирует всегда полными обьектами, расчет дельт выполняет нижний уровень который отвечает за хранение и для бинарников он как раз и будет держать копии файлов вместо дельт.
                  Кроме того я думаю что даже сжатие/расжатие дельт значительно быстрее скачки файлов по сети из облака
                    0
                    Согласен с тем, что меркуриал и гит могут как-то работать с большими файлами. Насчет скорости сжатия/разжатия дельт не совсем понял. Когда мыделаем pull или clone центрального репозитория, то мы должны забрать все изменения оттуда. Т. е. не только последнюю версию файла, а все дельты и снепшоты. В моем решении во время pull скачиваются только новые или обновленные файлы и только один раз.

                    Кроме того это не отменяет того факта, что большие репозитории с большими файлами не получится хранить в Github или Bitbucket.
                      0
                      Когда мыделаем pull или clone центрального репозитория, то мы должны забрать все изменения оттуда. Т. е. не только последнюю версию файла, а все дельты и снепшоты. В моем решении во время pull скачиваются только новые или обновленные файлы и только один раз.

                      Тут я с Вами согласен, однако стоит уточнить что все дельты скачиваются только если их у Вас еще не было, как правило при первоначальном checkout, потом передаются только изменения.

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

                      Вроде бы гитхаб не имеет жесткого ограничения на размер репозитория? Раньше я видел что ограничение было 2Гб и они предлагали предоставить больше места по требованию.
                        0
                        В целом да, если не хранить большие файлы, на которые сейчас есть явный лимит. Да и без лимита работать с ними, на сколько я знаю, было невозможно. Если большие файлы часто меняются, то размер репозитория быстро расти. Amazon S3 лучше подходит для подобных вещей. Там нет подобных лимитов и нет необходимости скачивать несколько версий для того, чтобы получить последнюю.

                        Я вообще представляю использование моего решения следующим образом: текстовые файлы хранятся в VCS, большие бинарники в облаке. При этом и размер репозитория остается маленьким, и загружать/скачивать не приходиться больше, чем нужно.
                +1
                Из статьи не совсем понятно: при уже имеющемся локально репозитории (сделан pull) и выполнении hg update, файлы обновятся? Закешируются локально?
                  0
                  Да, этот момент я упустил, спасибо. Обновил пример hgrc. Использование хука postupdate позволяет выполнять скачивание файлов после апдейта, при этом скачаются именно те версии файлов, которые указаны в обновленном xml файле.
                  0
                  А кому-то уже пришла в голову идея подружить git и торренты, чтобы крупные файлы качались через торрент? Это бы сильно разгрузило хабы. Может уже давно есть?
                    –1
                    Залейте в свой авто молоко — может поедет?

                    Only users with full accounts can post comments. Log in, please.