
Исторически мы использовали GitLab 8, который работал на хосте Mac на VirtualBox. Потом конфигурация перестала устраивать, поэтому в локальной сети завели отдельную полноценную Ubuntu-машину. Заодно и GitLab обновили до версии 11.2.1-ee.
Ставили все по официальному гайду. При установке postfix возникли ошибки из-за цифры в имени хоста (решилось переименованием), в остальном сложностей не было. Зато они появились позже: гит-машине перестало хватать памяти на объекты, мы подключили LFS и решили проблему, но потом сломались бэкапы. В общем, было весело. О том, как все это чинили — рассказал под катом.
Подключение LFS
Однажды на одном из проектов гит-машине перестало хватать памяти при перепаковке объектов. Ошибка указывала на большие бинарные ассеты.
Стали ресерчить и решили покрутить параметры Git (похожие проблемы были, например, здесь и здесь):
pack.windowMemory pack.packSizeLimit core.packedgitwindowsize core.packedgitlimit core.deltacachesize pack.deltacachesize pack.window pack.threads
Потратили какое-то время на проверку различных сочетаний параметров, но, к сожалению, это не помогло. А со временем ситуация бы только ухудшилась.
Чтобы перенести бинарные ассеты в отдельное хранилище и закрыть вопрос, решили подключить Git Large File Storage (документацию по реализации можно найти здесь).
gitlab_rails['lfs_enabled'] = true
в файле
/etc/gitlab/gitlab.rb
и сделать
sudo gitlab-ctl reconfigure
Список типов файлов, которые мы храним в LFS:
*.fbx *.aar *.psd *.zip *.png *.exr *.mp3 *.obj *.a *.o *.pdf *.mov *.dylib *.so *.jpg *.wav *.blend *.jar *.tif *.dll *.ogg
Некоторые нативные плагины содержат большие бинарный файлы без расширений (в основном — исполняемые). Сначала хотели заводить отдельные файлы .gitattributes в каталогах этих плагинов и указывать в них имена этих файлов. В дальнейшем отказались от этого, чтобы не усложнять работу с репозиторием и избежать проблем в случае изменения структуры каталогов проекта. Например, при обновлении плагинов. Такие файлы сейчас хранятся у нас в обычном Git, не в LFS.
Так как мы использовали Sourcetree в качестве гит-клиента, а его Windows-версия тогда не очень хорошо дружила с LFS, то столкнулись с множеством проблем. Приходилось выкручиваться и как-то решать их, пока в более поздних версиях SourceTree их не пофиксил сам разработчик Atlassian.
Зато мы лучше разобрались во внутреннем устройстве Git и LFS, и сейчас все работает стабильно.
Работа с бэкапами
Хранилище LFS также усложнило нам работу с бэкапами. Как-то раз GitLab восстановился не полностью. Причину мы тогда так и не выяснили, но фикс написали — теперь проблем с резервными копиями нет. Пойдем по порядку.
Создание бэкапа
Бэкап делается раз в неделю вызовом скрипта gitlab_backup.sh при помощи cron.
Сам скрипт:
#!/bin/bash backup_path="/var/opt/gitlab/manual_backups" current_date="`date +%d:%m:%Y-%H:%M`" new_date_backup_path="/var/opt/gitlab/backup_storage/$current_date" fixed_backup_path="/var/opt/gitlab/backup_storage/last_backup" sudo gitlab-ctl stop unicorn sudo gitlab-ctl stop sidekiq sudo gitlab-rake gitlab:backup:create sudo rm -fr ${fixed_backup_path} sudo mkdir ${fixed_backup_path} sudo mkdir ${new_date_backup_path} sudo cp -R ${backup_path}/* ${new_date_backup_path} sudo mv ${backup_path}/* ${fixed_backup_path} sudo gitlab-ctl restart
В документации нет прямой рекомендации выполнять перед созданием бэкапа, но мы делаем это для большей безопасности:
sudo gitlab-ctl stop unicorn sudo gitlab-ctl stop sidekiq
Бэкапы регулярно копируются с этой машины и хранятся в отдельном хранилище.
Восстановление из бэкапа
Чтобы восстановить GitLab из резервки, заходим по ssh на гит-машину. Там из папки бэкапа переносим архив, имя которого оканчивается на _gitlab_backup.tar (!), по пути /var/opt/gitlab/manual_backups.
Там должен находиться только выбранный для восстановления архив с правами на чтение и запись. Далее в запущенном Git останавливаем два процесса:
sudo gitlab-ctl stop unicorn sudo gitlab-ctl stop sidekiq
Затем запускаем команду и следим за процессом восстановления в консоли, иногда утвердительно отвечая на вопросы:
sudo gitlab-rake gitlab:backup:restore
После окончания восстановления запускаем Git:
sudo gitlab-ctl start
Затем пушим ветки с тех клиентов, на которых они в наиболее актуальном состоянии — чтобы в GitLab попали коммиты и ветки, которые были созданы после последнего бэкапа.
Так все и работало, пока в один прекрасный день GitLab восстановился не полностью. При попытке переключиться на ветку он не хотел отдавать отдельные объекты LFS, а в сообщении об ошибке указывались конкретные объекты хранилища, которые были не найдены.
Фикс хранилища LFS
Пришлось на сервер закидывать эти объекты из тех локальных копий, где они были поштучно. Код такой:
scp ~/Projects/pg3d/.git/lfs/objects/32/29/3229f63c1eb75eaeae57ab3199e8b7555b44906961ef44e38be2500c470a9073 git-server@192.168.160.160:/home/git-server/
После этого копируем объект по нужному пути:
mv /home/git-server/3229f63c1eb75eaeae57ab3199e8b7555b44906961ef44e38be2500c470a9073 /var/opt/gitlab/gitlab-rails/shared/lfs-objects/32/29/f63c1eb75eaeae57ab3199e8b7555b44906961ef44e38be2500c470a907
/var/opt/gitlab/gitlab-rails/shared/lfs-objects — путь «по умолчанию» к хранилищу объектов LFS.
Ручной поштучный перенос объектов LFS был долгим и трудоемким, поэтому мы написали скрипт. Теперь при восстановлении из бэкапа не приходится ничего переносить руками, скрипт все делает за нас. Размер бэкапа на момент написания статьи составлял чуть больше 50 ГБ — в таких условиях скрипту для восстановления нужно минимум 200 ГБ свободного места.
Скрипт по ssh соединяется с Git-машиной:
/usr/bin/expect -c "spawn ssh \"git-server@192.168.160.160\" \"'/home/git-server/restore_gitlab_gitlab_side.sh'\" ; expect password; send PASSWORD\n; interact "
И запускает restore_gitlab_gitlab_side.sh:
#!/bin/bash SUDO_PASSW="PASSWORD" # каталог куда сохраняются бэкапы при создании (бэкапы создаются каждую неделю заданием cron) BACKUP_STORAGE="/var/opt/gitlab/backup_storage" # каталог где должен лежать бэкап, из которого Гитлаб будет восстанавливаться RESTORE_BACKUP_PATH="/var/opt/gitlab/manual_backups" # файл бэкапа из которого будем восстанавливаться BACKUP_TO_RESTORE="latest_gitlab_backup.tar"
Чистим бэкапы старше трех недель, чтобы освободить место:
echo "removing old backups" cd "$BACKUP_STORAGE/" || { echo 'cd for removing older than 3-week backups failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S find "$BACKUP_STORAGE/" -type d -mtime +21 -exec rm -rf {} \; echo "freeing additional space" echo "$SUDO_PASSW" | sudo -S rm -fR $RESTORE_BACKUP_PATH/* echo "$SUDO_PASSW" | sudo -S rm -fR "$BACKUP_STORAGE/last_backup/tmp" echo "creating backup of current state" echo "$SUDO_PASSW" | sudo -S gitlab-ctl stop unicorn || { echo 'backup of current state stop unicorn failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S gitlab-ctl stop sidekiq || { echo 'backup of current state stop sidekiq failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S gitlab-rake gitlab:backup:create || { echo 'gitlab:backup:create failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S gitlab-ctl restart || { echo 'backup of current state restart failed' ; exit 1; } echo "moving backup of current state to backups store" current_date=$(date +%d:%m:%Y-%H:%M) new_date_backup_path="$BACKUP_STORAGE/$current_date" echo "$SUDO_PASSW" | sudo -S mkdir "$new_date_backup_path" || { echo 'mkdir new_date_backup_path failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S mv $RESTORE_BACKUP_PATH/* "$new_date_backup_path" || { echo 'mv new backup failed' ; exit 1; } echo "fixing permissions for backup of current state" echo "$SUDO_PASSW" | sudo -S chown -R git:git "$new_date_backup_path" || { echo 'chown backup of current state failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S chmod -R 0660 "$new_date_backup_path" || { echo 'chmod backup of current state failed' ; exit 1; }
Кладем предыдущий бэкап (который был сделан по расписанию). Из него будем восстанавливаться:
echo "preparing to restore from backup" TAR_FILE=$(echo "$SUDO_PASSW" | sudo -S ls $BACKUP_STORAGE/last_backup/*_gitlab_backup.tar) echo "$SUDO_PASSW" | sudo -S cp "$TAR_FILE" "$RESTORE_BACKUP_PATH/$BACKUP_TO_RESTORE" || { echo 'cp backup to manual_backups failed' ; exit 1; } echo "checking free space" # https://unix.stackexchange.com/questions/16640/how-can-i-get-the-size-of-a-file-in-a-bash-script BACKUP_SIZE=$(echo "$SUDO_PASSW" | sudo -S du -k "$RESTORE_BACKUP_PATH/$BACKUP_TO_RESTORE" | cut -f1) # use backup size*3 ? https://stackoverflow.com/questions/15213127/variables-multiplication REQUIRED_SPACE=$((BACKUP_SIZE*4)) FREE_SPACE_AVAILABLE=$(df "$PWD" | awk '/[0-9]%/{print $(NF-2)}') if [[ $FREE_SPACE_AVAILABLE -lt $REQUIRED_SPACE ]]; then echo "You need $REQUIRED_SPACE or more for successful restore" exit 1 fi echo "fixing permissions for backup" echo "$SUDO_PASSW" | sudo -S chown git:git "$RESTORE_BACKUP_PATH/$BACKUP_TO_RESTORE" || { echo 'chown backup failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S chmod 0660 "$RESTORE_BACKUP_PATH/$BACKUP_TO_RESTORE" || { echo 'chmod backup failed' ; exit 1; } echo "restoring" echo "$SUDO_PASSW" | sudo -S gitlab-ctl stop unicorn || { echo 'stop unicorn failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S gitlab-ctl stop sidekiq || { echo 'stop sidekiq failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S sh -c "yes yes | gitlab-rake gitlab:backup:restore" echo "successfully restored"
Теперь достанем содержимое хранилища LFS из бэкапа сломанного состояния и смержим его с восстановленным хранилищем LFS. В результате в LFS будут все файлы, которые были в GitLab на момент поломки. А вероятность того, что какой-нибудь объект потеряется — будет меньше.
LFS_STORE_PATH="/var/opt/gitlab/gitlab-rails/shared/lfs-objects" # для наглядности смотрим размер хранилища LFS до мержа и после SIZE_OF_LFS_BEFORE_MERGING=$(echo "$SUDO_PASSW" | sudo -S du -sh "$LFS_STORE_PATH") echo "size of lfs store before merge: $SIZE_OF_LFS_BEFORE_MERGING" echo "merging lfs of current state with restored state" CURRENT_STATE_TAR=$(echo "$SUDO_PASSW" | sudo -S ls "$new_date_backup_path") # lfs.tar.gz — имя подархива LFS в основном tar-файле бэкапа. Мы будем извлекать только LFS из файла бэкапа LFS_TAR_GZ="lfs.tar.gz" echo "$SUDO_PASSW" | sudo -S tar -xf "$new_date_backup_path/$CURRENT_STATE_TAR" -C "$new_date_backup_path" "$LFS_TAR_GZ" >/dev/null || { echo 'tar -xf lfs failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S mkdir "$new_date_backup_path/lfs_new" || { echo 'mkdir lfs_new failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S tar -xzf "$new_date_backup_path/$LFS_TAR_GZ" -C "$new_date_backup_path/lfs_new" >/dev/null || { echo 'tar -xzf lfs.tar.gz failed' ; exit 1; } # Мерж — dажно указывать слэши в конце путей echo "$SUDO_PASSW" | sudo -S rsync -abuP "$new_date_backup_path/lfs_new/" "$LFS_STORE_PATH/" echo "$SUDO_PASSW" | sudo -S chown git:git "$LFS_STORE_PATH" || { echo 'chown merged lfs failed' ; exit 1; } echo "$SUDO_PASSW" | sudo -S chmod 0755 "$LFS_STORE_PATH" || { echo 'chmod merged lfs failed' ; exit 1; } SIZE_OF_LFS_AFTER_MERGING=$(echo "$SUDO_PASSW" | sudo -S du -sh "$LFS_STORE_PATH") echo "size of lfs store after merge: $SIZE_OF_LFS_AFTER_MERGING" # cleaning up echo "$SUDO_PASSW" | sudo -S rm -fR "$new_date_backup_path/$LFS_TAR_GZ" echo "$SUDO_PASSW" | sudo -S rm -fR "$new_date_backup_path/lfs_new" echo "$SUDO_PASSW" | sudo -S gitlab-ctl start || { echo 'start failed' ; exit 1; } echo "Finished"
После этого пушим ветки с тех клиентов, на которых они в наиболее актуальном состоянии. В общем-то, все.
Бэкап конфигов и секретов
GitLab не добавляет в бэкап файлы конфига и секреты, поэтому мы делаем их резервные копии вручную. Но пока что ни разу не приходилось их восстанавливать.
Документация GitLab рекомендует для Omnibus-версии делать бэкапы хотя бы файлов /etc/gitlab/gitlab-secrets.json и /etc/gitlab/gitlab.rb. Но мы создаем резервную копию всей папки /etc/gitlab и храним отдельно от основного бэкапа.
Кроме того, в версии 12.3 появился функционал для бэкапа таких файлов. Планируем его использовать, когда обновим GitLab.
Серверный хук для предотвращения коммита сломанных мерджей
Еще небольшой кейс вспомнился. У нас были повторяющиеся случаи, когда разработчики сбрасывали все изменения во время мерджа и коммитили его пустым.
Чтобы этого не происходило, мы добавили серверный хук с защитой от пустых коммитов:
#!/usr/bin/env bash # # Pre-receive hook that will block any empty commits # Artists often create empty merge commits by deleting all incoming changes. This hook exists to prevent such situations. zero_commit="0000000000000000000000000000000000000000" # Do not traverse over commits that are already in the repository # (e.g. in a different branch) # This prevents funny errors if pre-receive hooks got enabled after some # commits got already in and then somebody tries to create a new branch # If this is unwanted behavior, just set the variable to empty excludeExisting="--not --all" while read oldrev newrev refname; do echo "$refname" "$oldrev" "$newrev" # branch or tag get deleted if [ "$newrev" = "$zero_commit" ]; then continue fi # Check for new branch or tag if [ "$oldrev" = "$zero_commit" ]; then span=$(git rev-list $newrev $excludeExisting) else span=$(git rev-list $oldrev..$newrev $excludeExisting) fi for COMMIT in $span; do # if COMMIT is root commit in repo , skip it, because $COMMIT^ will cause error files_in_commit=$(git diff --name-status $COMMIT^ $COMMIT) if [ $? -ne 0 ] then echo "$COMMIT is root commit? skipping it" continue fi echo "$files_in_commit" # sed - for skipping blank lines cnt_files_in_commit=$(echo "$files_in_commit" | sed '/^\s*$/d' | wc -l) echo "$cnt_files_in_commit" if [ "$cnt_files_in_commit" -eq 0 ] then echo "$COMMIT is empty, cannot push empty commits" exit 1 fi done done exit 0
Положили его по этому пути:
/var/opt/gitlab/git-data/repositories/USER/PROJECT.git/hooks/pre-receive.d
Здесь важно не забыть дать скрипту права на выполнение. Подробнее про хуки в GitLab можно прочитать по ссылке.
Вместо заключения
В дальнейшем планируем обновить GitLab до актуальной версии. А еще собираемся дополнить наш скрипт восстановления из бэкапа парой новых:
ссылка на оригинал статьи https://habr.com/ru/articles/573686/
Добавить комментарий