Одна из важных задач при разработке отказоустойчивой распределенной системы — синхронизация данных на мастер‑узле со слейв‑узлом. В дальнейшем будем звать слейв‑узлы репликами. Методов синхронизации множество, и иногда более эффективным оказывается тот, который учитывает специфику хранимых данных.
Я Роман Соловьев, ведущий ИТ‑инженер в отделе 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, оптимизированный для множества загрузок в каждой версии файла. Мы не рассматриваем этот вариант по нескольким причинам:
Gardener в основном работает по протоколу SSH, и завязывать синхронизацию на HTTP значит плодить сущности.
Утилита в основном предназначена для распространения файлов с одного сервера на многие машины, тогда как в нашем случае мастер‑узел для репозитория может меняться.
Утилита преимущественно работает с большими файлами (blob, iso и т. д.)
Git fetch (через SSH)
И, наконец, самый логичный и интуитивный метод — просто сделать Git fetch с мастера на реплику. Так же, как и rsync, Git определяет изменения в файлах и загружает только их путем загрузки специальных pack‑файлов. При загрузке изменений на узел измененные Git‑объекты сжимаются с помощью zlib, «рыхлые» объекты преобразовываются с помощью Git gc. Затем все упаковывается в pack‑файл, и он загружается на узел.
Резюмируя, можно сказать, что конкурировать за «место под солнцем» будут rsync и fetch. Чтобы выбрать наилучший метод, мы провели несколько тестов. Сценарий и результаты опишем в следующих секциях.
Эксперимент
Для чистоты результата мы подобрали несколько случаев:
Загрузка репозитория на чистый узел.
Изменение одного большого файла (добавление 100 000 строк в readme.md).
Изменение большого числа файлов (символ
a
в файлах заменяется на символb
).Создание тяжелого файла (использовался архив с 1000 фотографиями весом в 31 Мб).
Создание 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)
Andrey_Solomatin
29.08.2024 06:15+1Собственные инструменты — это удобно, безопасно и надежно.
Только когда вы начальству продаёте новый внутренний проект.
Andrey_Solomatin
29.08.2024 06:15rsync работает с файлами, а fetch c объектами гита.
Комит в гит не является атомарной операцией на диске, там меняются несколько файлов. В теории при использовании rsync может получиться неконсистениное состояние если вы не останавливаете мастер.
Andrey_Solomatin
29.08.2024 06:15+1Размер тяжёлого репозитория — 95 Мб (1691 файл)
Всё в мире относительно. Монорепы удивлённо поднимаю бровь, а дельфинчики смеются.
Как-то коллега случайно закомитил видео с дельфинчиками размером в 200MB.
FSA
Я проще сделал. Работаю с основным репозитоием, а если надо просто делаю пуш во все резервные с помощью небольшого скрипта.
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. Первая отправляет код, вторая теги.
me21
Вроде можно несколько URL указать для пуша.
И тогда git push origin должен отправлять изменения в два репозитория.
FSA
Да. Я тоже когда-то так делал. Но потом пришёл к выводу, что накосячить так могу. И уже после этого просто добавляю дополнительные репозитории со своими именами. В случае чего могу легко переключиться и сделать другой основным.