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

Я Роман Соловьев, ведущий ИТ‑инженер в отделе RnD и готовых решений Управления развития продукта в СберТехе. Сегодня расскажу о том, как мы синхронизируем Git‑репозитории на двух узлах, какие существуют альтернативы и зачем это вообще нужно.

Для чего это нужно

Начнем с конца (ведь с начала скучно). После ухода множества компаний с российского рынка в 2022–2023 гг. отечественный бизнес всё чаще задумывается о создании своих архитектурных решений. Собственные инструменты — это удобно, безопасно и надежно. Так, в СберТехе создали GitVerse — платформу для совместной разработки и хостинга кода. Поэтому мы в отделе RnD задались вопросом: какие масштабируемые системы хранения кода можно было бы интегрировать в любой рабочий процесс?

Мы провели анализ рынка и выяснили, что подобные решения есть в Bitbucket, Github и ещё нескольких системах. Точнее, мы уверены, что они есть, иначе системы просто не работали бы. Но никто их не видел и не увидит: исходный код и документацию выкладывать в open source не спешат.

Очевидным open source конкурентом можно назвать Gitaly, но его огромный недостаток в том, что он приклеен и прикручен саморезами к Gitlab. И интегрирование его в другую систему хранения кода, использующую Git‑протокол, — трудоёмкий и неблагодарный процесс.

Из этих рассуждений родился Gardener — проект отказоустойчивой распределённой системы хранения Git‑репозиториев. Такое название мы выбрали потому, что проект косвенно работает с Git‑деревьями и ветками: занимается их созданием, клонированием и пересадкой на другие узлы участки. А основной, но не единственный инструмент для нашего садовника — это Git‑протокол, по универсальности не уступающий родной лопате.

В процессе создания Gardener мы столкнулись с ожидаемой проблемой: нужно как‑то добиться одинакового состояния репозиториев на мастере и репликах. Bare git‑репозиторий, который мы храним на узле, — это просто директория со специальными файлами. Поэтому у нас было два пути: умно копировать репозитории как обычные директории или придумать что‑то более оптимальное, исходя из специфики хранимых данных. Далее расскажу, какой путь выбрали и какие методы проанализировали.

SCP

Эта утилита известна, наверное, даже начинающим программистам. Под капотом она использует SSH и SFTP (с OpenSSH >= 9.0) и просто копирует файлы с одной машины на другую. Это мощный инструмент, но для нашей задачи подходит плохо, так как в коммите файлы, как правило, изменяются, а не создаются или удаляются. Соответственно, нужно получать список измененных файлов, удалять предыдущие версии, загружать новые (причем еще и транзакционно). Единственное преимущество этого метода по сравнению с другими в том, что scp есть в Linux по умолчанию — что, естественно, не показатель.

Rsync

Rsync изначально создавалась как замена rcp и scp в случаях, когда у принимающего узла уже есть отличающаяся версия объекта. Зеркалирование осуществляется одним потоком в каждом направлении, а не по одному или несколько потоков на каждый файл. Для копирования используется специальный delta‑transfer алгоритм, разбивающий на принимающей машине целевой файл на неперекрывающиеся куски фиксированного размера. Он читает их хеш‑суммы, сжимает с помощью zlib и отправляет узлу, с которым синхронизируется. Для нашей задачи эта утилита — одно из лучших решений.

Почему не zsync?

Zsync — инструмент, похожий на rsync, оптимизированный для множества загрузок в каждой версии файла. Мы не рассматриваем этот вариант по нескольким причинам:

  1. Gardener в основном работает по протоколу SSH, и завязывать синхронизацию на HTTP значит плодить сущности.

  2. Утилита в основном предназначена для распространения файлов с одного сервера на многие машины, тогда как в нашем случае мастер‑узел для репозитория может меняться.

  3. Утилита преимущественно работает с большими файлами (blob, iso и т. д.)

Git fetch (через SSH)

И, наконец, самый логичный и интуитивный метод — просто сделать Git fetch с мастера на реплику. Так же, как и rsync, Git определяет изменения в файлах и загружает только их путем загрузки специальных pack‑файлов. При загрузке изменений на узел измененные Git‑объекты сжимаются с помощью zlib, «рыхлые» объекты преобразовываются с помощью Git gc. Затем все упаковывается в pack‑файл, и он загружается на узел.

Резюмируя, можно сказать, что конкурировать за «место под солнцем» будут rsync и fetch. Чтобы выбрать наилучший метод, мы провели несколько тестов. Сценарий и результаты опишем в следующих секциях.

Эксперимент

Для чистоты результата мы подобрали несколько случаев:

  1. Загрузка репозитория на чистый узел.

  2. Изменение одного большого файла (добавление 100 000 строк в readme.md).

  3. Изменение большого числа файлов (символ a в файлах заменяется на символ b).

  4. Создание тяжелого файла (использовался архив с 1000 фотографиями весом в 31 Мб).

  5. Создание 100 легких файлов.

Все описанные пункты мы применили к тяжёлому и к лёгкому репозиториям. В качестве тяжёлого был выбран NixOS (https://github.com/NixOS/nix.git), в качестве лёгкого — Toolchain Registry (https://github.com/yandex/toolchain‑registry.git). В п. 3 изменили 1552 файла в тяжёлом репозитории и 39 в лёгком. Размер тяжёлого репозитория — 95 Мб (1691 файл), лёгкого — 860 Кб (40 файлов).

Тяжёлый репозиторий (nixOS)

опыта

1

2

3 (1552 файла)

4

5

rsync

1 м 45,40

0,472

4,374

0,230

0,330

fetch

1 м 37,77

0,257

1,232

0,895

0,182

Лёгкий репозиторий (Yandex Toolchain Registry)

опыта

1

2

3 (1552 файла)

4

5

rsync

0,178

0,179

0,187

0,231

0,177

fetch

0,154

0,156

0,181

0,797

0,159

Видно, что с загрузкой и изменением большого количества файлов лучше справляется fetch, так как rsync вносит изменения пофайлово, а с загрузкой больших файлов лучше работает rsync. Но на малом репозитории разница незначительна.

Вывод

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

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


  1. FSA
    29.08.2024 06:15

    Я проще сделал. Работаю с основным репозитоием, а если надо просто делаю пуш во все резервные с помощью небольшого скрипта.

    function gpa() {
    for server in $(git remote -v | cut -f1 | uniq) ; do
    echo "git push $server"; git push $server
    done
    }
    function gpat() {
    for server in $(git remote -v | cut -f1 | uniq) ; do
    echo "git push $server --tags"; git push $server --tags
    done
    }

    Будучи добавленными в .zshrc эти функции позволяют запушить во все репозитории, которые настроены в локальном с помощью команды gpa и gpat. Первая отправляет код, вторая теги.


    1. me21
      29.08.2024 06:15

      Вроде можно несколько URL указать для пуша.

      git remote set-url --add --push origin git@github.com:muccg/my-project.git
      git remote set-url --add --push origin git@bitbucket.org:ccgmurdoch/my-project.git

      И тогда git push origin должен отправлять изменения в два репозитория.


      1. FSA
        29.08.2024 06:15

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


  1. Andrey_Solomatin
    29.08.2024 06:15
    +1

    Собственные инструменты — это удобно, безопасно и надежно.

    Только когда вы начальству продаёте новый внутренний проект.


  1. Andrey_Solomatin
    29.08.2024 06:15

    rsync работает с файлами, а fetch c объектами гита.

    Комит в гит не является атомарной операцией на диске, там меняются несколько файлов. В теории при использовании rsync может получиться неконсистениное состояние если вы не останавливаете мастер.


  1. Andrey_Solomatin
    29.08.2024 06:15
    +1

    Размер тяжёлого репозитория — 95 Мб (1691 файл)

    Всё в мире относительно. Монорепы удивлённо поднимаю бровь, а дельфинчики смеются.

    Как-то коллега случайно закомитил видео с дельфинчиками размером в 200MB.