[GitLab] 主备冷备份部署方案

一、问题背景

基于CentOS 7搭建同版本GitLab主备冷备份环境,出现顽固权限问题:首次手动备份恢复正常,重复执行恢复操作必报日志权限拒绝错误

报错信息:application_json.log Permission denied

前期已排查常规干扰项,问题依旧存在:

  • SELinux 已彻底关闭

  • 主备机目录属主、软链接、基础权限配置一致

  • 防火墙无拦截、无权限限制策略

二、问题排查

2.1 报错日志

bash 复制代码
2026-06-18_05:30:23.51736 Rails Error: Unable to access log file. Please ensure that /opt/gitlab/embedded/service/gitlab-rails/log/production.log exists and is writable (i.e. make it writable for user and group: chmod 0664 /opt/gitlab/embedded/service/gitlab-rails/log/production.log). The log level has been raised to WARN and the output directed to STDERR until the problem is fixed.
2026-06-18_05:30:25.97975 {"timestamp":"2026-06-18T05:30:25.979Z","pid":32146,"message":"! Unable to load application: Errno::EACCES: Permission denied @ rb_sysopen - /opt/gitlab/embedded/service/gitlab-rails/log/grpc.log"}
2026-06-18_05:30:25.97989 bundler: failed to load command: puma (/opt/gitlab/embedded/bin/puma)
2026-06-18_05:30:25.98012 /opt/gitlab/embedded/lib/ruby/3.1.0/logger/log_device.rb:95:in `initialize': Permission denied @ rb_sysopen - /opt/gitlab/embedded/service/gitlab-rails/log/grpc.log (Errno::EACCES)

2.2 目录权限差异

经实测定位,问题根源为GitLab两套日志目录权限机制差异:

默认日志目录权限
bash 复制代码
nbi2731,root,root # ll /var/log/gitlab/gitlab-rails/
total 1664
-rw-r----- 1 git git    6944 Jun 18 13:22 api_json.log
-rw-r----- 1 git git  160284 Jun 18 12:00 application_json.log
-rw-r----- 1 git git     557 Jun 18 11:20 audit_json.log
-rw-r----- 1 git git     697 Jun 18 11:21 auth.log
-rw-r----- 1 git git   12182 Jun 18 11:25 database_load_balancing.log
-rw-r----- 1 git git   13103 Jun 18 11:20 exceptions_json.log
-rw-r----- 1 git git    1444 Jun 18 10:44 gitlab-rails-db-migrate-2026-06-18-10-44-08.log
-rw-r----- 1 git git      67 Jun 18 10:44 grpc.log
-rw-r----- 1 git git 1420877 Jun 18 13:22 production_json.log
-rw-r----- 1 git git   53350 Jun 18 13:21 production.log
-rw-r----- 1 git git      67 Jun 18 10:44 service_measurement.log
-rw-r----- 1 git git     925 Jun 18 13:20 sidekiq_client.log
自定义日志目录权限
bash 复制代码
nbi2731,root,root # ll /var/lib/libvirt/gitlab-logs/gitlab-rails/
total 2816
-rw-r--r-- 1 git git   74538 Jun 18 15:58 api_json.log
-rw-r--r-- 1 git git  618112 Jun 18 16:45 application_json.log
-rw-r--r-- 1 git git    4477 Jun 18 16:01 audit_json.log
-rw-r--r-- 1 git git    2767 Jun 18 15:57 auth.log
-rw-r--r-- 1 git git   69518 Jun 18 15:58 database_load_balancing.log
-rw-r--r-- 1 git git  469916 Jun 18 16:01 exceptions_json.log
-rw-r--r-- 1 git git   40253 Jun 18 16:01 graphql_json.log
-rw-r--r-- 1 git git      67 Jun 18 13:40 grpc.log
-rw-r--r-- 1 git git 1508321 Jun 18 16:48 production_json.log
-rw-r--r-- 1 git git       0 Jun 18 13:40 production.log
-rw-r--r-- 1 git git      67 Jun 18 13:40 service_measurement.log
-rw-r--r-- 1 git git   10598 Jun 18 16:01 sidekiq_client.log
  • 默认日志目录(/var/log/gitlab/gitlab-rails):权限严格锁定为rw-r-----。

  • 自定义日志目录:权限宽松rw-r--r--。

对比可知,自定义日志目录针对所有用户都有读权限。

三、解决方案

根治方案:备机单独配置自定义日志目录,避开系统日志权限锁。主备机统一备份路径、保留时长、文件权限参数,适配跨机同步。

1、主机 gitlab.rb 核心配置

统一备份相关参数,保障跨机同步兼容性

bash 复制代码
vim /etc/gitlab/gitlab.rb
plain 复制代码
# 自定义备份存放路径
gitlab_rails['backup_path'] = "/var/lib/libvirt/gitlab-backups"

# 备份保留时长7天
gitlab_rails['backup_keep_time'] = 604800

# 备份包权限644,适配跨机同步
gitlab_rails['backup_archive_permissions'] = 0644
bash 复制代码
# 这个命令执行之后,上面的配置才能生效
gitlab-ctl reconfigure

2、备机 gitlab.rb 核心配置

同步主机备份参数,新增自定义日志目录

bash 复制代码
vim /etc/gitlab/gitlab.rb
plain 复制代码
# 自定义备份存放路径,与主机完全统一
gitlab_rails['backup_path'] = "/var/lib/libvirt/gitlab-backups"

# 备份保留时长7天
gitlab_rails['backup_keep_time'] = 604800

# 备份包权限644,适配跨机同步
gitlab_rails['backup_archive_permissions'] = 0644

# 自定义日志目录,规避系统目录权限锁,根治反复执行权限报错
gitlab_rails['log_directory'] = '/var/lib/libvirt/gitlab-logs/gitlab-rails'
bash 复制代码
mkdir -p /var/lib/libvirt/gitlab-logs/gitlab-rails

# 这个命令执行之后,上面的配置才能生效
gitlab-ctl reconfigure

3、手动测试步骤

3.1 主机手动备份
bash 复制代码
# 主机手动备份流程
# 1. 生成GitLab业务数据备份tar包
gitlab-backup create

# 2. 定位本次最新生成的时间戳备份目录,并将主机的两个配置文件拷贝至最新生成的目录中
LATEST_BAK=$(ls -dt /var/lib/libvirt/gitlab-backups/gitlab-bak-* | head -n1)
cp /etc/gitlab/gitlab-secrets.json /var/lib/libvirt/gitlab-backups/
cp /etc/gitlab/gitlab.rb /var/lib/libvirt/gitlab-backups

核心备份逻辑 :主机每次备份生成带时间戳的独立目录,将业务tar包、gitlab-secrets.json、gitlab.rb全部归集至该目录,完整时间戳目录即为有效备份包,备机通过rsync同步整目录,文件齐全无散落。

核心恢复规范 :主备gitlab.rb存在差异化配置(备机独有日志目录配置)。日常恢复仅替换主机gitlab-secrets.json密钥不覆盖备机gitlab.rb,主机配置文件仅做备份留存。

3.1.1 备机手动同步备份文件
bash 复制代码
# 备机执行,同步主机最新所有备份数据(包含备份tar包、gitlab-secrets.json、gitlab.rb)
rsync -avz root@主机IP:/var/lib/libvirt/gitlab-backups/ /var/lib/libvirt/gitlab-backups/

路径说明 :建议将同步过来的所有备份文件、密钥、配置均存放于备机时间戳子目录,非备份根目录,避免混淆。

3.2 备机手动恢复
bash 复制代码
# 备机手动恢复核心操作
# 核心规则:只覆盖主机密钥、绝不覆盖备机gitlab.rb(保留备机日志目录差异化配置)

# 1. 自动获取最新备份目录(精准定位子目录密钥文件)
LATEST_BAK=$(ls -dt /var/lib/libvirt/gitlab-backups/gitlab-bak-* | head -n1)

# 2. 同步主机密钥并加固权限(修复原根目录路径错误)
cp /var/lib/libvirt/gitlab-backups/gitlab-secrets.json /etc/gitlab/
chown root:root /etc/gitlab/gitlab-secrets.json
chmod 600 /etc/gitlab/gitlab-secrets.json

# 3. 逐个停止前端应用服务,保留数据库、Redis、Gitaly核心服务
gitlab-ctl stop puma
gitlab-ctl stop sidekiq
gitlab-ctl stop gitlab-workhorse
gitlab-ctl stop nginx
gitlab-ctl stop gitlab-kas
gitlab-ctl stop gitlab-exporter

# 4. 获取备份包前缀、执行数据恢复
BAK_PREFIX=$(ls /var/lib/libvirt/gitlab-backups/*_gitlab_backup.tar | awk -F '/' '{print $NF}' | cut -d '_' -f1)
RAILS_LOG_TO_STDOUT=true gitlab-backup restore BACKUP=${BAK_PREFIX} force=yes SKIP=extensions

# 5. 数据库迁移、缓存清理
gitlab-rake db:migrate
gitlab-rails cache:clear

# 6. 校正权限、重启服务
gitlab-ctl reconfigure
gitlab-ctl restart

4、主机自动化备份方案

为了方便执行,将上述手动执行的步骤整合到脚本里面,再结合crontab,就可以实现自动化备份了。

4.1 主机备份脚本

脚本逻辑:定时备份业务数据,自动归集tar包、密钥、配置至时间戳目录,自动清理7天过期备份。

bash 复制代码
#!/bin/bash
BASE_DIR="/var/lib/libvirt/gitlab-backups"
DATE_TAG=$(date +%Y-%m-%d_%H-%M-%S)
CURR_BAK_DIR="${BASE_DIR}/gitlab-bak-${DATE_TAG}"
KEEP_DAYS=7

# 创建本次备份目录
mkdir -p "${CURR_BAK_DIR}"

# 1. 执行静默备份
/opt/gitlab/bin/gitlab-backup create CRON=1
if [ $? -ne 0 ]; then
    echo "数据备份失败"
    exit 1
fi

# 2. 移动最新tar包到本次备份目录
TAR_FILE=$(ls -lt ${BASE_DIR}/*_gitlab_backup.tar | head -n1 | awk '{print $9}')
if [ -f "${TAR_FILE}" ]; then
    mv "${TAR_FILE}" "${CURR_BAK_DIR}/"
fi

# 3. 备份配置与密钥
cp -a /etc/gitlab/gitlab.rb "${CURR_BAK_DIR}/"
cp -a /etc/gitlab/gitlab-secrets.json "${CURR_BAK_DIR}/"

# 4. 清理:只保留最新7份备份目录,删除全部根目录tar包
ls -dt "${BASE_DIR}"/gitlab-bak-* | tail -n +$((KEEP_DAYS+1)) | xargs rm -rf
find "${BASE_DIR}" -maxdepth 1 -type f -name "*_gitlab_backup.tar" -delete

echo "本次备份完成:${CURR_BAK_DIR}"
4.2 主机定时任务配置

主机仅负责定时生成备份,无需恢复、无需同步任务。

bash 复制代码
# 添加执行权限
chmod +x /usr/local/bin/gitlab_backup.sh

# 配置定时任务:每日凌晨3点自动同步并执行恢复
crontab -e
0 1 * * * /bin/bash /usr/local/bin/gitlab_backup.sh

5、备机自动化恢复方案

5.1 备机恢复脚本

脚本逻辑:定时同步主机完整备份、自动读取最新子目录密钥、仅停应用层服务、安全恢复数据、迁移缓存、校正权限。

bash 复制代码
#!/bin/bash
set -euo pipefail

# 修复cron缺少/usr/sbin导致semodule找不到
export PATH="/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin"

# ===================== 配置区 =====================
SCRIPT_LOG="/var/log/gitlab_restore.log"
BASE_DIR="/var/lib/libvirt/gitlab-backups"
REMOTE_USER="root"
REMOTE_IP="主服务器IP"
REMOTE_DIR="/var/lib/libvirt/gitlab-backups"
SYSTEM_SECRET="/etc/gitlab/gitlab-secrets.json"
BAK_KEEP_DAY=7
GITLAB_RB="/etc/gitlab/gitlab.rb"
CUSTOM_LOG_DIR="/var/lib/libvirt/gitlab-logs/gitlab-rails"
# ==================================================

echo "================ START $(date) ================" | tee "${SCRIPT_LOG}"

# 1. 前置:自动配置自定义日志目录,永久规避rake日志权限问题
echo "[1] Ensure custom log directory config in gitlab.rb" | tee -a "${SCRIPT_LOG}"
mkdir -p "${CUSTOM_LOG_DIR}"

# 精准匹配单引号格式,删除全部旧配置行
sed -i '/gitlab_rails\['\''log_directory'\''\]/d' "${GITLAB_RB}"
echo "gitlab_rails['log_directory'] = '${CUSTOM_LOG_DIR}'" >> "${GITLAB_RB}"

# 重载配置使日志目录生效
gitlab-ctl reconfigure >> "${SCRIPT_LOG}" 2>&1

# 2. 本地备份目录初始化
mkdir -p "${BASE_DIR}"
echo "[2] Skip manual directory chown, rely on default gitlab permission" | tee -a "${SCRIPT_LOG}"

# 3. rsync同步远端主机备份
echo "[3] rsync backup from master" | tee -a "${SCRIPT_LOG}"
BEFORE=$(ls -dt "${BASE_DIR}"/gitlab-bak-* 2>/dev/null | head -n1 || true)

rsync -avz --delete \
  "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_DIR}/" \
  "${BASE_DIR}/" >> "${SCRIPT_LOG}" 2>&1

AFTER=$(ls -dt "${BASE_DIR}"/gitlab-bak-* 2>/dev/null | head -n1 || true)

# 无备份直接退出
if [[ -z "${AFTER}" ]]; then
  echo "[EXIT] no backup found" | tee -a "${SCRIPT_LOG}"
  exit 0
fi

# 无新备份直接退出
if [[ "${BEFORE}" == "${AFTER}" ]]; then
  echo "[EXIT] no new backup" | tee -a "${SCRIPT_LOG}"
  exit 0
fi

LATEST="${AFTER}"

# 4. 定位备份tar包
DATA_TAR=$(ls "${LATEST}"/*_gitlab_backup.tar 2>/dev/null | head -n1 || true)
if [[ ! -f "${DATA_TAR}" ]]; then
  echo "[ERROR] backup tar missing" | tee -a "${SCRIPT_LOG}"
  exit 1
fi
BAK_PREFIX=$(basename "${DATA_TAR%_gitlab_backup.tar}")
echo "[4] latest backup: ${DATA_TAR}" | tee -a "${SCRIPT_LOG}"

# 5. 仅停止应用层服务,保留pg/redis/gitaly
echo "[5] stop application layer only" | tee -a "${SCRIPT_LOG}"
gitlab-ctl stop puma || true
gitlab-ctl stop sidekiq || true
gitlab-ctl stop gitlab-workhorse || true
gitlab-ctl stop nginx || true
gitlab-ctl stop gitlab-kas || true
gitlab-ctl stop gitlab-exporter || true

# 6. 同步密钥文件并加固权限(必须保留)
echo "[6] restore secrets" | tee -a "${SCRIPT_LOG}"
cp -f "${LATEST}/gitlab-secrets.json" "${SYSTEM_SECRET}"
chown root:root "${SYSTEM_SECRET}"
chmod 600 "${SYSTEM_SECRET}"

# 7. 复制备份包到本地备份根目录
cp -f "${DATA_TAR}" "${BASE_DIR}/"

# 8. 执行恢复,RAILS_LOG_TO_STDOUT=true 绕过旧日志文件读写
echo "[7] gitlab backup restore" | tee -a "${SCRIPT_LOG}"
RAILS_LOG_TO_STDOUT=true gitlab-backup restore \
  BACKUP="${BAK_PREFIX}" \
  force=yes \
  SKIP=extensions >> "${SCRIPT_LOG}" 2>&1

RESTORE_CODE=$?
if [[ ${RESTORE_CODE} -ne 0 ]]; then
  echo "[ERROR] restore failed code=${RESTORE_CODE}" | tee -a "${SCRIPT_LOG}"
  exit 1
fi

# 9. 数据库迁移(官方标准gitlab-rake,修复ruby找不到报错)
echo "[8] run database migrate" | tee -a "${SCRIPT_LOG}"
gitlab-rake db:migrate >> "${SCRIPT_LOG}" 2>&1
MIGRATE_CODE=$?
if [[ ${MIGRATE_CODE} -ne 0 ]]; then
  echo "[ERROR] db migrate failed code=${MIGRATE_CODE}" | tee -a "${SCRIPT_LOG}"
  exit 1
fi

# 10. 清理Redis缓存,解决登录循环跳转
echo "[9] clear application cache" | tee -a "${SCRIPT_LOG}"
gitlab-rails cache:clear >> "${SCRIPT_LOG}" 2>&1

# 11. 重载配置自动修复仓库、附件目录所有权限
echo "[10] gitlab reconfigure auto fix permission" | tee -a "${SCRIPT_LOG}"
gitlab-ctl reconfigure >> "${SCRIPT_LOG}" 2>&1

# 12. 完整重启GitLab全服务
echo "[11] restart full gitlab" | tee -a "${SCRIPT_LOG}"
gitlab-ctl restart >> "${SCRIPT_LOG}" 2>&1

# 13. 自动清理7天前过期备份文件夹
echo "[12] cleanup old backups" | tee -a "${SCRIPT_LOG}"
find "${BASE_DIR}" -maxdepth 1 -type d -name "gitlab-bak-*" -mtime +${BAK_KEEP_DAY} -exec rm -rf {} \;

echo "================ DONE $(date) ================" | tee -a "${SCRIPT_LOG}"
5.2 备机定时任务配置(仅备机)

备机仅负责定时同步主机备份+自动恢复,无需备份任务。

bash 复制代码
# 添加执行权限
chmod +x /usr/local/bin/gitlab_restore.sh

# 配置定时任务:每日凌晨3点自动同步并执行恢复
crontab -e
0 3 * * * /bin/bash /usr/local/bin/gitlab_restore.sh

四、gitlab-ctl reconfigure 简要说明

gitlab-ctl reconfigure核心作用:读取gitlab.rb配置、自动校正目录/文件权限、重置运行环境。修改配置、权限错乱、环境异常时执行生效。

与restart区别:restart仅重启服务,无法修复权限和配置。

五、总结

问题核心:默认日志目录权限锁定,root重复恢复残留权限污染导致报错。

解决方案:备机启用自定义宽松权限日志目录,主备统一备份参数,依托reconfigure自动校正权限。

整套方案支持无人值守定时备份、同步、恢复,流程闭环无冲突,可长期稳定运行。