Исторически мы использовали 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 на диске на основе этого и этого.

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


  1. Sm1le291
    19.08.2021 20:11
    -1

    А зачем вы до lfs(тут боюсь тролить потому что не работал с lfs) делали бэкапы гита?

    Вроде бэкап гита у каждого разработчика, по крайней мере так было днем когда я делал коммит.

    Бэкап есть у меня и ещё у нескольких тысяч моих коллег


    1. metamorph
      19.08.2021 21:28
      +7

      Ну, для начала, бекап не гита, а гитлаба.

      Гитлаб - это же не только кодики, которые под vcs, это и юзеры-логины-пароли-ключи, это переменные для CI, раннеры и дофига чего еще.

      Далее, было бы ошибкой предполагать, что при падении gitlab как некоего общего source of truth вы запросто сможете восстановиться без потерь. Напомню, что у Вас и тысяч Ваших коллег есть данные (кстати, потенциально неактуальные) только по тем репам, которые вы пуллили давеча. По остальным репам информации у вас нет.

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

      И даже если (теоретически) можно в итоге поскрести по сусекам и собрать хоть какое-то общее хранилище кода - времени это займет столько, что Вы триста тысяч раз пожалеете, что не бекапили гитлаб.

      Децентрализация гита - она только в головах. На практике дело обстоит намного хуже.


      1. Sm1le291
        20.08.2021 11:23
        -1

        Я в курсе что такое гитлаб и что это не только кодики, в сбере поднимал ажур на команду из 50 человек и успешно мигрировал через 4 версии, включая переход с tsvc на гит, и такого говна не встречал. Повторюсь как в коменте ниже, все должно делатся гораздо проще и навкрняка делается в гитлаб просто вы не стали копать, а решили поупражняться в написании велосипедов.

        Отдельное спасибо за минусы


        1. metamorph
          20.08.2021 11:25

          У меня такое чувство, что Вы меня с автором поста перепутали.


      1. PetrRnd Автор
        20.08.2021 12:15
        +3

        Да, смысл именно в бэкапе гитлаба, а не гита. Полностью согласен с комментом metamorph


    1. Pancir
      20.08.2021 02:50
      +1

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


  1. metamorph
    19.08.2021 21:39

    Статья, конечно, забавная, прям начиная с фразы "по историческим причинам".

    Ох, сколько ада в этом мире оправдано этими словами. Приходишь в новую компанию, видишь полную хрень, спрашиваешь: почему так? По историческим причинам!

    Потом растешь до большого начальника, творишь полную хрень в интересах бизнеса (сейчас быстренько накостыляем, а потом сделаем норм). Выводишь на работу новых сотрудников, а они спрашивают, почему так. По историческим причинам, вот почему! ))

    ---

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

    Короче, как руководство к действию я бы не стал принимать. Есть способы намного проще.


    1. 13werwolf13
      20.08.2021 06:36
      -2

      уж незнаю за что минусуют, я бы плюсанул, но увы кармы нет


    1. Sm1le291
      20.08.2021 08:53

      Вот да, работал с Azure Devops, там бэкап базы данных делается из графического интерфейса(репозитории тоже хранятся в бд, даже гитовые) и потом также накатывается. Уверен гитлаб тоже все это умеет из коробки. А не вот это вот все, что нам автор тут показал, если только он не с говном мамонта работает


  1. Temtaime
    20.08.2021 17:12
    +2

    У нас другой опыт(общий вес реп — около 100 гб).
    Из-за огромного количества багов с LFS и прочими причудами вылезающими с новыми версиями(добило gitlab.com/gitlab-org/gitlab/-/issues/271576) — избавились от LFS вообще.
    По опыту: гитлаб пытается в энтерпрайз, но сам — коленная поделка, собранная из говна и палок.


    1. markowww
      20.08.2021 17:48
      +1

      Вот из-за этого https://gitlab.com/gitlab-org/gitlab/-/issues/23625 мы перенесли все, что работает с LFS, на GitHub