ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

自建GitLab备份恢复全指南:从密钥到演练,避免翻车

自建GitLab备份恢复全指南:从密钥到演练,避免翻车

自建 GitLab 这件事,装起来有多爽,备份没做好翻车时就有多痛。前两篇聊了安装和日常使用,这篇把最后一块拼图补上:备份和恢复。GitLab 不是一个能靠"把整个/var/opt/gitlab目录打个 tar 包"就完事的系统,它背后有一整套 PostgreSQL 数据库、一堆加密密钥、N 个服务进程的数据都交织在一起。我见过有人把整个宿主机云盘快照当成备份,结果真到恢复时发现仓库文件更新丢了、CI 变量全部解密失败,那场面基本等于这几个月的代码白干。

这篇会把 GitLab 备份恢复的底层逻辑、标准操作、自动化和恢复演练完整过一遍,适合所有自己维护 GitLab 实例的开发者、运维和"公司里被迫兼管服务器"的朋友。里面提到的命令我都按当前主流版本的实际行为写,老版本命令差异也会标注,照着执行基本不会踩坑。

1. 自建 GitLab 的备份恢复,到底在备份什么

1.1 先讲一次不算严重但吓出一身冷汗的故障

我遇到过这么个事:某公司内部跑着一台单机 GitLab,数据放在一块数据盘里,看着挺正常。某天早上一到工位,同事说代码推不上去,登录服务器一看,磁盘 IO 错误,文件系统直接变成只读。重启之后部分数据卷损坏,Git 仓库目录里若干对象的校验和不对,仓库直接打不开。

当时所有人第一反应是"我们有云盘快照,怕什么"。等真正去翻快照的时候才发现,快照是前一天晚上打的,当天上午大家推的十几个 commit 全没了。虽然最后靠 git fsck 抢救回来一部分,但有些新仓库因为没有远端备份,损失是实打实的。

这个经历给我的教训是:底层快照不是备份,它只是"同一份数据的另一个时间点副本",一旦数据本身已经损坏,快照同样会跟着坏。GitLab 这种应用,必须做应用层一致性的备份,也就是要同时保证 PostgreSQL 数据库、Git 仓库、配置文件、密钥文件等所有组件处于一个逻辑一致的状态。

1.2 拆解 GitLab 的"家底":配置文件、数据库和密钥

GitLab 自建(Omnibus 安装)的数据分布并不复杂,但分散在好几处。先记住这张表:

路径内容丢了会怎样
/etc/gitlab/gitlab.rb主配置,包括域名、端口、备份路径、各种开关配置全部丢失,服务可以重新配,但很费时间
/etc/gitlab/gitlab-secrets.json密钥文件,存储数据库加密密钥、会话密钥、2FA 种子等最致命,用户密码全部失效,CI 变量无法解密
/var/opt/gitlab/git-data所有 Git 仓库的裸仓库代码全部丢失,这是最核心的资产
/var/opt/gitlab/postgresql/dataPostgreSQL 数据库数据,包含用户、权限、合并请求、CI 记录Web 界面数据全部丢失
/var/opt/gitlab/gitlab-rails/uploads上传的头像、附件、设计文件附件丢失
/var/opt/gitlab/gitlab-rails/shared/artifactsCI/CD 产物、页面制品构建产物丢失,流水线记录不完整
/var/opt/gitlab/gitlab-ci/builds构建日志日志丢失

其中最容易被人忽略的是/etc/gitlab/gitlab-secrets.json。很多人以为备份就是备份数据和配置,其实 GitLab 在数据库里存了很多敏感字段是加密的,加密用的密钥就放在这个 secrets 文件里。你的项目 CI 变量、用户 2FA 种子、Rails 的secret_key_base,全部依赖这个文件。密钥丢了,数据库就算完整恢复,也没法解开这些数据,等于白恢复。

所以我自己对备份的定义很简单:GitLab 的完整备份 = 数据备份(gitlab-backup生成的 tar)+ 配置文件备份(gitlab.rb)+ 密钥文件备份(gitlab-secrets.json),三样缺一不可。

1.3 gitlab-backup 的机制和产物

Omnibus 版本自带的备份命令是gitlab-backup create,在 13.x 之前的版本里则写作gitlab-rake gitlab:backup:create。它内部做的事情是:

  1. 对 PostgreSQL 执行pg_dump,导出数据库到一个临时目录;
  2. 整理/var/opt/gitlab下的仓库、上传文件、构建产物等数据;
  3. 把这些内容打成 tar 包,放到备份目录,默认是/var/opt/gitlab/backups;
  4. 生成一个backup_information.yml,记录 backup 的版本、时间、GitLab 版本、Git 版本等关键元数据。

最终产生的文件命名规则类似这样:

1700010000_2025_01_01_17.0.0_gitlab_backup.tar

前面是一串时间戳,然后是备份日期和 GitLab 版本。注意,gitlab-backup默认不会把/etc/gitlab下的配置和密钥文件打进 tar,这是官方设计,意思是配置文件要自己单独备份。这个细节是无数人恢复失败的头号原因,后面我会专门展开。

2. 第一次备份该怎么跑:准备、执行、验证

2.1 跑备份前的环境准备和目录规划

第一次执行备份前,先把环境查清楚,不然很容易跑到一半磁盘满了。我的固定步骤是:

# 查看备份目录所在磁盘空间 df -h /var/opt/gitlab/backups # 查看现有数据大小 du -sh /var/opt/gitlab/git-data du -sh /var/opt/gitlab/postgresql/data

估算备份文件大概有多大,最稳的办法是预留出数据目录 1.5 倍以上的空间。因为gitlab-backup执行时先落临时文件再打包,高峰占用会明显超过最终 tar 的体积。如果你数据有 200GB,备份目录至少留 300GB 比较好。

其次要确认备份目录跟数据目录不要在同一块物理盘上。这个道理很简单:备份的意义就是在原始数据损坏时能救回来,如果磁盘直接物理故障,备份和数据一起没了,那备份就是白做。稍好一点的方案是把备份目录放到另一块挂载盘,或者直接通过网络备份到另一台机器。

权限方面,gitlab-backup create需要以root或者git用户执行。如果你用普通用户执行,通常会遇到权限报错。最省事的做法是sudo gitlab-backup create。

如果想把备份目录改到别的位置,在/etc/gitlab/gitlab.rb里改:

gitlab_rails['backup_path'] = "/data/gitlab/backups"

改完执行sudo gitlab-ctl reconfigure让配置生效。改之前记得备份目录要先建好,并设置成git:git属主:

mkdir -p /data/gitlab/backups chown git:git /data/gitlab/backups

2.2 手动备份的标准操作和参数说明

环境准备好后,执行手动备份:

sudo gitlab-backup create STRATEGY=copy CRON=1

这里面两个参数我建议第一次就跑上。STRATEGY=copy会用文件系统 copy 的方式处理正在写入的数据库文件,对整个实例的线上影响比默认方式小,特别适合还在被团队使用的生产实例。CRON=1表示非交互模式,避免执行过程中停下来问 "Do you want to continue?" 这类问题,便于以后把它写进定时任务。

如果你的实例数据量很大,还可以考虑加这些参数:

sudo gitlab-backup create STRATEGY=copy CRON=1 SKIP=artifacts,registry

SKIP后面可以跟逗号分隔的组件名,比如不备份制品和容器镜像仓库,速度会快不少。但注意,跳过了的东西恢复时也没有,需要自己权衡。

执行完之后,检查备份文件:

ls -lh /var/opt/gitlab/backups/

成功的标志有三个:tar 文件时间戳接近当前时间、文件大小不为零、日志结尾出现 "done" 字样。日志末尾如果出现红色或者 ERROR,别管 tar 文件存不存在,直接当作失败处理。

2.3 备份文件里的秘密:看懂备份内容的组成部分

很多人以为备份文件就是个黑盒,其实里面结构很清楚。执行:

tar tf /var/opt/gitlab/backups/1700010000_2025_01_01_17.0.0_gitlab_backup.tar | head -50

你会看到类似这样的内容:

backup_information.yml db/ db/database.sql.gz repositories/ repositories/group/ repositories/group/project.git/ uploads/ builds/ artifacts/ pages/

这么一看就明白了:db/database.sql.gz是 PostgreSQL 的导出结果,repositories/是裸仓库,uploads/是附件,builds/是构建日志。恢复时,gitlab-backup restore就是读取这个结构,把每个组件还原到对应目录。

还有个关键文件backup_information.yml,恢复工具会读取它来判断备份时的 GitLab 版本、Git 版本等信息。所以请不要手贱去改 tar 包里的文件,改坏了版本信息,恢复工具会直接拒绝工作。

2.4 验证备份有效性的最低成本方法

只备份不验证,等于没备份。这是我反复吃亏得出的结论。有一次定时任务日志显示备份成功,但实际 tar 文件在打包中途因为磁盘满被截断了,日志里报错被好几条无关输出盖住,一直没发现。直到后来要迁移实例,恢复时才发现备份文件根本不完整。

最低成本的验证分两层。第一层是物理验证,每隔一段时间做一次:

gzip -t /var/opt/gitlab/backups/xxx_gitlab_backup.tar

如果没有报错说明压缩层是完好的。再执行:

tar tf /var/opt/gitlab/backups/xxx_gitlab_backup.tar > /dev/null

它能确认 tar 包内所有文件都能正常读取。但这两步只能验证"文件没坏",不能验证"数据逻辑一致"。真正靠谱的验证是拿备份文件在一台临时 GitLab 实例上实际恢复一次,然后跑gitlab-rake gitlab:check检查完整性。这个成本高一些,但频率不用太勤,一个月或一个季度一次足够。

3. 别让备份变成废纸:自动化、异地和保留策略

3.1 定时备份的几种方案:cron 与日志告警

手动备份只是开始,定时备份才是常态。我见过太多团队说"我们会手动备份",结果一忙起来两三个月都不碰一次。最省心的方式是写进 cron:

0 2 * * * /usr/bin/flock -n /tmp/gitlab-backup.lock /usr/bin/gitlab-backup create STRATEGY=copy CRON=1 >> /var/log/gitlab-backup.log 2>&1

这里用flock -n给备份加锁,防止上一个备份还没跑完、下一个任务又开始执行,导致两个备份进程同时写同一个备份目录。加上>> /var/log/gitlab-backup.log 2>&1把输出存到日志,方便事后排查。

还有一些思路是把备份脚本做成 systemd timer,管理和日志都会更规范。但对多数单机自建场景来说,cron 足够可靠。关键在于必须有失败告警,否则写进 cron 也不代表真的在保护你。最简单的做法是让备份脚本在失败时退出码非零,再用外部监控盯住退出状态,或者把日志同步到专门的收集服务里。

3.2 异地备份:rsync 和对象存储

定时的本机备份只解决"数据损坏"问题,不解决"机器没了"的问题。机房断电、服务器被盗、磁盘阵列整体故障,这些情况下本机备份也一起完蛋。所以异地备份是必须的,尤其对代码资产来说。

单机自建最简单的方案是用 rsync 把备份目录同步到另一台机器:

rsync -avz --delete /var/opt/gitlab/backups/ backup-user@backup-host:/data/gitlab-backups/

--delete参数确保远程目录跟本地保持一致,本地清理过期备份时,远程也不会堆积。这个命令同样可以写进 cron,在 GitLab 备份完成后半小时执行。

如果公司有对象存储(S3 兼容的私有化部署也很多),GitLab 本身支持直接上传备份到对象存储。在gitlab.rb里配置gitlab_rails['backup_upload_connection']等参数,官方文档写得很详细。但我的建议是:盲目上对象存储之前,先确认你了解备份的上传和下载流程,否则恢复时反而多一道凭据障碍。很多时候 rsync 到一台内网备份机已经足够可靠。

3.3 保留策略:保留几份、怎么清理

备份文件体积会越来越大,不可能无限堆积。GitLab 的备份命令默认只负责生成,不负责清理旧文件。所以你要自己定保留策略。我现在的策略是:

  • 每日备份保留最近 14 天;
  • 每月 1 号的全量备份单独保留 6 个月;
  • 异地备份跟本地保持同样节奏。

清理可以用一行命令:

find /var/opt/gitlab/backups -name "*_gitlab_backup.tar" -mtime +14 -delete

但注意,如果每月归档目录不在同一个地方,要单独处理。另外提醒一句,find -mtime依赖的是文件修改时间,备份文件名里有时间戳,判断时最好用时间戳而不是 mtime,避免有人手动 touch 过文件导致误判。

备份加密这件事,没做的人总觉得"内网无所谓"。如果你把备份文件 rsync 到异地或者归档到对象存储,强烈建议加密。最轻量的方式是 gpg 对称加密:

gpg --symmetric --cipher-algo AES256 /var/opt/gitlab/backups/xxx_gitlab_backup.tar

执行后会生成.gpg文件,原文件建议删掉。恢复时先解密再放回备份目录。这里有个小坑,gitlab-backup restore识别备份文件靠的是文件名里的时间戳规则,解密出来的文件名必须保持跟原 tar 文件一致,才能被正确识别。

3.4 恢复演练不能只在脑子里做

我见过太多团队把备份脚本写得漂漂亮亮,但从来没有真正恢复过一次。等到版本升级、机器迁移、磁盘损坏这些真出事的时候,才发现恢复流程有一步漏了,或者备份文件本身已经坏了。恢复演练不是可有可无的加分项,它是备份策略的验收环节。

我的做法是每季度找一台 staging 虚拟机,安装跟生产相同版本的 GitLab,从备份服务器拉最新备份文件,完整执行一次恢复,然后跑gitlab-rake gitlab:check。整个过程不算太久,但能提前暴露很多问题,比如新版 GitLab 数据库迁移不兼容、备份文件被加密后忘了密钥、异地备份链路断了好几天没人发现。等真出事再发现这些问题,代价就不是一台虚拟机的事情了。

4. 恢复比备份更需要排练:从零到一完整恢复流程

4.1 恢复前必须对版本、环境、权限做的检查

恢复流程中最核心的一条铁律是:备份版本和待恢复的 GitLab 版本必须保持一致。GitLab 的备份文件包含数据库 dump 和内部结构,跨大版本恢复非常容易因数据库迁移问题卡住。官方文档虽然允许恢复到同版本或以后版本,但我的实践经验是:恢复到完全相同的版本最稳,尤其是小版本号最好也对上。

恢复之前要做三件准备工作。第一,检查备份文件里的backup_information.yml,确认gitlab_version与当前实例版本一致。第二,确认备份文件属主是git:git,否则恢复过程中 GitLab 无法读写,执行:

chown git:git /var/opt/gitlab/backups/xxx_gitlab_backup.tar

第三,确认备份文件已经放在当前实例的备份目录里,并且文件名没有被人为改过。

4.2 三种典型恢复场景的命令与步骤

场景一:同一台机器原地恢复。假设实例还在但数据被误删或损坏。官方推荐先停掉会写数据的服务,避免恢复过程中后端进程继续往数据库里写数据:

sudo gitlab-ctl stop puma sudo gitlab-ctl stop sidekiq

如果是老版本,puma可能是unicorn,用gitlab-ctl status确认一下实际服务名就好。然后执行恢复:

sudo gitlab-backup restore BACKUP=1700010000_2025_01_01_17.0.0

恢复过程会提示输入yes确认,如果你用脚本自动化,需要提前处理交互。恢复完成后执行:

sudo gitlab-ctl reconfigure sudo gitlab-ctl restart sudo gitlab-rake gitlab:check

场景二:迁移到新机器。新机器上安装与备份相同版本的 GitLab,先停止服务:

sudo gitlab-ctl stop

把旧机器的/etc/gitlab/gitlab.rb和/etc/gitlab/gitlab-secrets.json拷贝到新机器,再执行 reconfigure。然后把备份 tar 文件放到新机器的备份目录,chown 之后执行 restore。特别注意顺序:配置文件和密钥要在恢复数据之前准备好,否则 GitLab 启动时找不到密钥,或者用默认密钥初始化,数据恢复后反而可能解密失败。

场景三:容器化部署的恢复。如果 GitLab 跑在容器里,备份和恢复都在容器内执行:

docker exec -it gitlab gitlab-backup create docker exec -it gitlab gitlab-backup restore BACKUP=1700010000_2025_01_01_17.0.0

但容器场景有个额外风险:容器镜像版本和备份版本不一致的概率更高,因为很多人习惯直接docker pull最新镜像。恢复前务必确认镜像版本与备份文件版本一致。

4.3 恢复后必做的验证:登录、仓库、CI 变量、Webhook

恢复完成不等于万事大吉,必须做一轮业务验证。我的固定顺序是:

  1. 访问 Web 界面,用管理员账号登录,确认页面正常、项目列表数量正确;
  2. 随便挑一个项目执行git clone操作,确认 HTTP 和 SSH 两种协议都通;
  3. 打开一个项目的 CI/CD 设置页,确认变量能看到、Runner 列表里没有丢失;
  4. 检查一个 Webhook 配置,确认外部通知地址还在;
  5. 找一个开启 2FA 的用户账号实测登录,确认密钥文件恢复有效。

这些验证里最容易翻车的是 CI 变量和 2FA。如果之前没备份gitlab-secrets.json,恢复后 Web 界面可能正常,但 CI 变量会显示乱码或无法解密,团队成员的 2FA 全部失效。这个问题不是数据损坏,是密钥不匹配,只能靠密钥文件来解决。

4.4 把恢复演练做成脚本,而不是手工流程

手工恢复演练最多做一两次,时间长了就容易偷懒。我现在把演练过程写成了一个简单脚本,大致流程是:新起一台空白虚拟机 -> 安装对应版本 GitLab -> 拷贝配置和密钥 -> 从备份服务器拉备份文件 -> 恢复 -> 跑gitlab-check-> 输出报告。

这套脚本虽然不复杂,但每次演练只要半小时就能完成全部动作。相比手动操作,脚本化能保证每次都走同样的步骤,不会有"这次忘了 chown"这种低级失误。如果你团队规模不大,哪怕只是把命令按顺序存在一个文档里,也比每次临时回忆要强得多。

5. 我踩过的坑:这些细节让恢复差点失败

5.1 密钥文件没备份导致用户登录全部失效

有一次做跨机器迁移,我小心翼翼地把gitlab.rb和备份 tar 都拷到了新机器,恢复流程跑完,Web 界面能打开,项目和仓库都回来了。结果当天下午团队同事陆续反馈:密码登录报错、两步验证无法通过、CI 变量显示异常。

排查下来才发现,我在准备材料时把/etc/gitlab/gitlab-secrets.json漏掉了。新机器启动时 GitLab 生成了一套全新密钥,等我把数据库恢复回去,数据库里的加密数据是用旧密钥加密的,自然解不开。

解决方案是立刻把旧机器的gitlab-secrets.json传过去覆盖,然后重启服务。因为密钥文件必须在实例第一次启动前就放好,否则后续再换也会引发不一致。这个坑之后,我把密钥文件直接列为备份材料的第一项,每次先检查它有没有被拷走。

5.2 版本不一致导致数据库迁移卡死

另一次恢复演练,我为了省事在 staging 上装了一个比备份版本新很多的主版本,想着"反正能恢复再升级"。结果gitlab-backup restore执行到一半,数据库迁移脚本直接报错退出,整个 staging 实例处于半初始化状态。查阅日志发现备份文件里的版本是老的,新版本 GitLab 在恢复老数据库 dump 后会自动执行迁移,但迁移过程遇到一个旧数据残留导致约束冲突。

后来我改了策略:恢复演练和生产迁移都严格要求备份版本和安装版本一致,跨版本操作一定是"先恢复到同版本 -> 验证没问题 -> 再正常执行 GitLab 升级"。这个顺序能避免把"恢复失败"和"升级失败"两件事混在一次变更里,排查问题时会轻松非常多。

5.3 恢复后仓库 hook 和 CI 的权限问题

还有一次恢复完成、页面正常、clone 也正常,但用户推代码时收到 500 错误。跑gitlab-rake gitlab:check发现一堆仓库的权限有问题。原因很常见:备份文件是从另一台机器上拉过来的,解压后属主不是git:git,或者两台机器的git用户 UID 不一致。

解决方式比较直接:

chown -R git:git /var/opt/gitlab/git-data chown -R git:git /var/opt/gitlab/gitlab-rails/uploads sudo gitlab-ctl reconfigure

恢复完数据后别急着开放提交权限,先用gitlab-rake gitlab:check把权限类问题扫一遍再放人进来。如果公司同时维护多套环境,建议在两台机器上统一git用户 UID,能省很多跨环境拷贝之后的麻烦。

5.4 备份超时和备份文件损坏的排查思路

最后说一个最隐蔽的坑:日志显示备份成功,但备份文件实际已经损坏。当时定时任务每天凌晨跑,某天数据量涨得快,磁盘快满了,备份脚本在打包阶段因为空间不足报错退出。但由于脚本没有做退出码判断,外部监控看到进程"跑过了"就以为没问题,后续的异地 rsync 也一直在同步那个被截断的 tar 文件。

排查这类问题的路径其实很清晰:先看/var/log/gitlab-backup.log的结束状态,确认有没有DONE标记;再执行gzip -t验证文件完整性;顺带用df -h检查磁盘占用。三个动作加起来不到一分钟,却能拦截掉大部分备份失效场景。为了防止再发生,我在备份脚本里加了一步:备份结束后自动执行gzip -t,失败就通过 webhook 推消息告警。

6. 我的固定检查表和最后的建议

把前面所有经验整理下来,我自己现在给团队定的固定动作是这么一组:

  • 每天 02:00 执行自动备份,日志输出到独立文件,退出码异常必须告警;
  • 每周手动抽查一次备份日志,确认 tar 文件大小和gzip -t都通过;
  • 每月把最新备份文件 rsync 到异地备份机,并实际验证异地文件可读;
  • 每季度在 staging 环境做一次完整恢复演练,顺手验证密钥文件和配置文件是否齐整。

这套节奏听起来很简单,但真正坚持下来并不容易。备份恢复这个事,难的不是跑通一条命令,而是让"能恢复"变成常态。我在经历过密钥丢失、版本不匹配、备份文件损坏这些事故之后最大的体会是:与其研究各种花哨的高可用方案,不如先把最基础的备份-验证-恢复闭环做扎实。每次备份完顺手在临时环境里恢复一次,虽然占一点时间,但真到翻车那天,你会感谢当时多花的那十分钟。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进