Как мы переезжали на новую версию GitLab и внедряли LFS. А потом чинили бэкапы

Kate

Administrator
Команда форума
Исторически мы использовали 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 до актуальной версии. А еще собираемся дополнить наш скрипт восстановления из бэкапа парой новых:
  • скриптом проверки целостности хранилища LFS на основе этого;
  • скриптом актуализации содержимого базы данных LFS и файлов LFS на диске на основе этого и этого.

 
Сверху