尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

GitLab备份恢复与迁移实战:从基础命令到避坑指南

GitLab备份恢复与迁移实战:从基础命令到避坑指南 1. 先搞清楚一件事你备份的到底是什么很多团队用 GitLab 用了一两年备份脚本也配了Cron 定时任务也跑着但真到服务器硬盘报错、机房断电、或者领导突然说这台机器要退役数据迁到新机器的时候才发现自己对备份的理解只停留在跑一下 backup 命令。GitLab 的备份不是简单地把/var/opt/gitlab目录打个包就能完事的它至少有四个状态需要你分别对待。1.1 GitLab 的四个状态组成代码仓库、数据库、配置文件、密钥文件GitLab 本质上是一个组合体底层跑着 PostgreSQL 数据库、Redis 缓存、Gitaly 仓库存储服务、Sidekiq 异步任务前面还有 Nginx 和 Puma。代码仓库本身存在 Gitaly 管理的 repository 目录里但仓库里的所有元数据、权限关系、合并请求、Issue、CI/CD 变量、Webhook 配置、用户账号全都存在 PostgreSQL 数据库里。所以备份至少要做两件事把数据库完整导出把仓库目录同步走。但更关键的是/etc/gitlab/gitlab.rb和/etc/gitlab/gitlab-secrets.json这两个文件。gitlab.rb是你的配置gitlab-secrets.json是加密密钥文件。我在实际维护中见过不少人恢复完 GitLab 后页面能打开但用户密码全部失效、CI/CD 变量读不出来、Runner 连接异常最后排查下来都是因为这个密钥文件没跟着备份。因为 GitLab 里很多敏感数据比如外部服务 Token、数据库加密字段都是用这个密钥文件加密的恢复后的环境必须用回原来的密钥才能解开。丢失它备份数据库里的密文就是一堆无意义的乱码。1.2 官方备份命令到底做了什么Omnibus 安装包自带的gitlab-backup老版本是gitlab-rake gitlab:backup:create本质上做了几件事调用pg_dump导出 PostgreSQL 数据库中所有 GitLab 相关库压缩并打包/var/opt/gitlab/git-data/repositories下的仓库裸仓库目录同步/var/opt/gitlab/uploads里的附件上传文件打包/var/opt/gitlab/artifacts、/var/opt/gitlab/pages等公共目录最后生成一个带时间戳的 tar 包默认放在/var/opt/gitlab/backups。所以默认命令其实已经覆盖了代码和业务数据但它没有把/etc/gitlab/gitlab.rb和/etc/gitlab/gitlab-secrets.json打包进去。官方文档也明确说这两个文件需要你手动备份。所以正确做法是把备份脚本写成两步先备份配置和密钥再执行全量备份。1.3 一个完整的备份命令实例下面是我在实际环境中用的脚本结构兼顾了两步走和保留策略#!/bin/bash # GitLab backup with config and secrets BACKUP_BASE/var/opt/gitlab/backups CONFIG_BACKUP$BACKUP_BASE/config-$(date %Y%m%d%H%M%S).tar.gz KEEP_DAYS7 # 1. 备份配置文件与密钥 tar czf $CONFIG_BACKUP -C /etc/gitlab gitlab.rb gitlab-secrets.json # 2. 官方全量备份 /usr/bin/gitlab-backup create BACKUPlatest STRATEGYcopy GZIP_RSYNCABLEyes # 3. 清理超过保留时间的备份文件 find $BACKUP_BASE -maxdepth 1 -type f -name *.tar -mtime $KEEP_DAYS -delete find $BACKUP_BASE -maxdepth 1 -type f -name config-*.tar.gz -mtime $KEEP_DAYS -delete这里有两个参数值得单独说STRATEGYcopy默认方式是tar直接读取仓库目录如果备份期间有人正在 push 代码可能读到不一致的中间状态。指定copy会先把仓库目录硬链接到临时目录再打包降低这种风险但会多占用一份磁盘临时空间。GZIP_RSYNCABLEyes让 tar 包内部文件的压缩对齐方式更合理方便以后做增量同步或者用 rsync 传输。代价是备份耗时稍微变长。注意BACKUPlatest这个参数在部分版本里用来指定备份文件名如果脚本里不写默认也会用latest标识问题不大。但如果你在同一台机器上多实例运行 GitLab必须显式指定实例名。2. 备份实操默认配置能跑通但生产环境不能直接照搬好多人都经历过这种情况gitlab-backup create命令跑完看到Backup task is done就放心了。问题是备份文件落在系统盘上系统盘一旦损坏备份跟着一起没。备份真正要防的恰恰是服务器整机不可用这个场景所以备份文件不能只留在一台机器的本地磁盘。2.1 备份目录选型和空间估算Omnibus 默认的备份目录是/var/opt/gitlab/backups它是 GitLab 安装目录所在分区的一部分。如果/分区不够大大仓库打包到一半就会报No space left on device。我一般建议单独给备份挂一个分区或者直接挂到外部存储。空间怎么估算一个简单公式备份包大小大约是仓库磁盘占用量的 60% 到 80%取决于仓库类型和压缩效率数据库导出通常不大一般几 MB 到几 GB。但仓库目录里有大量objects/pack文件本身已经是压缩格式再打包收益有限。所以如果你仓库总共占 200GB备份包可能要 150GB 左右。在出备份方案时我习惯按仓库占用空间的 1.2 倍预留临时空间因为备份过程中copy策略还会多一份临时文件。想精确查看仓库目前占多大可以用du -sh /var/opt/gitlab/git-data/repositories du -sh /var/opt/gitlab/backups2.2 把备份推送到远端对象存储和 NFS 的取舍GitLab 官方支持把备份目录直接挂到 NFS 或者 S3 兼容对象存储上但我更推荐本地落盘 定时同步的方式。原因是 GitLab 的备份进程对文件系统性能和锁要求比较敏感直接写到对象存储偶尔会出现上传中断、文件不完整的问题虽然 GitLab 新版在改进但没必要在生产环境冒这个险。实测比较稳妥的方案是备份先写本地磁盘再用脚本把备份包rsync或通过s3cmd put推到远端。推送完比对一下文件大小和 md5确认一致再删本地的旧备份。# 推送到 S3 兼容存储示例 s3cmd put /var/opt/gitlab/backups/*.tar s3://your-bucket/gitlab-backup/ \ --no-progress \ --multipart-chunk-size-mb64 # 推送后校验 s3cmd ls s3://your-bucket/gitlab-backup/ | tail -5对象存储的好处是天然异地冗余NFS 的好处是恢复时可以直接把备份目录指过去不需要先下载到本地。但如果你的 NFS 本身就在同一机房同一机架那就没有异地容灾的意义了至少要做到跨机架或者跨机房。2.3 Cron 定时备份要处理的并发和覆盖问题Cron 定时备份本身不复杂但有一个坑很多人踩过gitlab-backup create默认备份文件名包含时间戳理论上不会互相覆盖但如果上一次备份因为仓库太大还没跑完下一次 Cron 又触发了两个备份进程会同时抢/var/opt/gitlab/.gitlab-backup目录下的临时锁导致其中一个进程直接报错退出留下一个残缺的 tar 包看起来成功了实际恢复时才发现文件不完整。解决办法是在脚本开头加个简单的锁判断LOCKFILE/var/opt/gitlab/backups/.backup.lock if [ -f $LOCKFILE ]; then echo Another backup process is running, skip this run. exit 0 fi touch $LOCKFILE trap rm -f $LOCKFILE EXIT同时给 Cron 任务加一个合理的执行时间窗口。比如每天凌晨 2 点执行如果仓库特别大可以预留一个 4 小时的窗口不要设置在业务高峰附近。GitLab 备份本身对 IO 的消耗很大尤其是STRATEGYcopy会额外产生大量硬链接和文件遍历业务量大时跑备份会拖慢 Gitaly 响应。2.4 增量备份和仓库级备份的适用场景很多从数据库领域过来的人会下意识问GitLab 支持增量备份吗官方全量备份工具在很长一段时间内都不支持增量。GitLab 17.x 之后开始推仓库级备份Repository Backup功能可以只备份某个仓库或者按组备份但数据库、对象存储、元数据仍然需要整体备份覆盖。如果你要的是RPO恢复点目标尽可能短更靠谱的方案是给底层 PostgreSQL 做持续归档WAL 归档或者把 GitLab 的数据库实例做成流复制。但这样做复杂度会上一个量级不太适合中小团队。我的观点是对于绝大多数团队每天一次全量备份 每天定时把备份推送到异地RPO 在 24 小时以内已经完全够用。代码仓库这种数据哪怕丢一天通常也就是重推一次提交的事比数据库业务数据好恢复得多。3. 恢复的完整链路从备份文件到可访问服务恢复这件事理论上就是一条命令gitlab-backup restore BACKUP时间戳。但我在实操中从没见过哪次生产恢复是一条命令没事的。恢复过程的坑比备份多得多核心原因在于 GitLab 对版本、环境、权限的要求非常苛刻。3.1 恢复前的硬性前置检查恢复前必须确认的东西先列个清单版本匹配备份文件是哪来的最好就用同一个 GitLab 大版本去恢复。如果你想恢复到更高版本的 GitLab官方要求先安装与备份时完全相同的版本去恢复再走升级流程不能备份是 15.11 的直接恢复到 17.4 的实例。跨大版本直接恢复数据库迁移脚本会直接把你的数据搞挂。空实例原则恢复前目标实例必须是一个全新安装、还没创建过任何项目的 GitLab。如果目标实例已经存在项目恢复时可能会因为仓库路径冲突直接失败。最稳妥的做法是先gitlab-ctl stop puma sidekiq停掉服务如果有旧数据先整体挪走。磁盘空间充足备份包 解压后的数据量 数据库导入临时文件这些加在一起需要比备份包大出至少一倍的空间。我第一次做恢复演练时就吃过亏备份包 80GB解压加导入峰值直接飙到 190GB系统分区差点塞满。3.2 恢复操作的分步过程以 Omnibus 安装的 GitLab 为例恢复流程大概是这样的# 1. 停掉相关服务避免恢复过程中有写入 sudo gitlab-ctl stop puma sidekiq # 2. 确认识别到备份文件文件必须在备份目录里 sudo gitlab-backup restore BACKUP1722233300_2024_07_29_16.9.0 # 3. 恢复完成后重建相关缓存和状态 sudo gitlab-ctl reconfigure # 4. 重启所有服务 sudo gitlab-ctl restart # 5. 如果恢复的是配置和密钥还要重启一次让所有进程读取最新配置 sudo gitlab-rake gitlab:checkgitlab-backup restore在执行过程中会做一个检查先确认数据库和 GitLab 版本是否兼容然后解包仓库目录到临时位置再导入数据库最后移动仓库目录到目标位置。它不会自动恢复/etc/gitlab/gitlab.rb和gitlab-secrets.json这两个必须先在恢复前手动放回去。如果你忘了放gitlab-secrets.json恢复后 GitLab 能启动但所有带加密的字段都用新密钥去解密旧数据结果就是一堆解密失败报错。3.3 恢复后至少要验证的 8 个点验证备份是否真正恢复成功不是看页面能打开就算数。我给自己定了一个恢复后检查清单每次恢复演练、真实恢复、迁移落地都要逐项过一遍用管理员账号登录 Web UI确认能登录、能切换页面没有 500 错误。新建一个临时测试项目clone 下来提交一个文件再 push 回去确认仓库读写链路正常。查看仓库的提交历史、分支、Tag 列表和备份前记录做对比。查看几个关键项目的 CI/CD 变量、Webhook、保护分支规则是否还在。检查/var/opt/gitlab/git-data/repositories下的仓库目录数量和备份前是否一致。用管理员进入 Admin Area - Users抽查几个非管理员用户的权限。查看gitlab-rake gitlab:check输出重点看gitlab-shell、GitLab API、Database这几项是否为绿色。确认 Sidekiq 队列没有积压异常任务gitlab-rails runner puts Sidekiq::Queue.new.size。这套检查看着多实际走一遍也就一两个小时但能避免两周后发布版本时才发现 CI 变量不见了这种灾难。3.4 单仓库恢复的思路如果你只是想恢复某一个仓库而不是整个 GitLab 实例有些情况下不需要做全量恢复。比如某个仓库被误删了但全量备份还在你可以用 GitLab Rails Console 从备份中把仓库单独捞出来。大体思路是准备一台临时 Ubuntu 或测试机安装同版本 GitLab恢复全量备份然后单独导出目标仓库的bundle文件再导入到生产实例# 在临时实例上导出单仓库 sudo gitlab-rake gitlab:repo:export \ RAILS_ENVproduction \ usernamegroup_name \ projectproject_name \ archive_path/tmp/export # 在生产实例上导入 sudo gitlab-rake gitlab:repo:import \ RAILS_ENVproduction \ usernamegroup_name \ projectproject_name \ archive_path/tmp/export但说实话如果不是仓库量巨大、不能停机我更推荐直接全量恢复。单仓库恢复要处理权限、群组、CI/CD 变量等各种依赖坑更多。4. 迁移方案换机、换版本、换部署方式的三条路径备份和恢复是基础迁移是更复杂的场景。迁移不只是恢复到一个空实例通常还夹杂着版本升级、部署方式变化、域名变化、存储路径变化这些东西。我把迁移拆成三种情况分别说清楚。4.1 同版本同架构迁移最快的方式场景旧机器要退役新机器配置更高两边都是同一个大版本的 Omnibus GitLab都是 Linux x86_64。这种迁移其实比备份恢复还简单因为不涉及版本差异。我个人最常用的是整体目录拷贝 重装配置的方式而传统备份恢复适合小仓库、讲究流程的场景。整体目录拷贝的步骤如下新机器安装与旧机器完全相同的 GitLab 版本完成初始化配置。在旧机器上停掉 GitLab 服务sudo gitlab-ctl stop保证数据一致。把/etc/gitlab整个目录、/var/opt/gitlab整个目录用rsync同步到新机器。命令大概是rsync -avzP --delete \ -e ssh \ /etc/gitlab/ rootnewserver:/etc/gitlab/ rsync -avzP --delete \ -e ssh \ /var/opt/gitlab/ rootnewserver:/var/opt/gitlab/新机器执行gitlab-ctl reconfigure和gitlab-ctl restart。这里的关键点是两个版本必须完全一致大版本一致不够小版本也要一致因为/var/opt/gitlab下的数据结构可能因为补丁升级而发生变化。如果不一致轻则 reconfigure 报错重则数据库起不来。这个方案的好处是速度快数 TB 数据也能通过 rsync 增量同步在可接受时间内完成。4.2 跨版本迁移必须遵守逐版本升级的铁律GitLab 官方升级路径的规则是不能跨大版本直接升级必须按照大版本逐个升级而且每个大版本还要考虑最后一个次要版本作为跳板。典型的升级路径是15.11.x - 16.0.x - 16.11.x - 17.0.x。这样做不是因为官方想折磨人而是数据库迁移脚本只保证相邻版本之间的兼容性。所以跨版本迁移的正确姿势是线上备份停写操作。在原机器上先升级到目标路径上的第一个中间版本跑一遍启动验证。再升级到下一个中间版本再验证。最后升级到最终版本。在最终版本上做一次完整备份再恢复到新机器。这个过程中最忌讳的就是备份是旧版本直接恢复到新版本实例然后跑reconfigure期望它自动迁移数据库。我见过有人从 GitLab 13 直接备份恢复到 GitLab 16结果数据库在db:migrate阶段直接崩了日志里面全是 SQL 语法错误。如果你确实是旧版本备份文件 新版本空实例的处境唯一的补救路径是先回退安装一个与备份一致的旧版本在旧版本上恢复再一级一级升级上来。这个时间成本通常很大但至少数据不会丢。4.3 Docker/K8s 和裸机之间的迁移现在很多团队用的 GitLab 是 Docker 镜像部署的迁移逻辑又不一样。Docker 部署时官方推荐的持久化目录主要三个/etc/gitlab配置文件与密钥/var/log/gitlab日志/var/opt/gitlab数据迁移到新 Docker 环境时最省事的方式是先把这几个目录从容器挂载卷里拷出来再到新环境起一个同版本容器把这些目录映射进去。但有个细节很多人忽略Docker 版的 GitLab 里默认的备份命令是gitlab-backup create恢复命令一样但必须先进入容器再执行docker exec -it container_name gitlab-backup create # 恢复时 docker exec -it container_name gitlab-backup restore BACKUP时间戳K8s 上的 GitLab无论是官方 Helm Chart 还是 Operator迁移就更复杂一般不走gitlab-backup而是直接用持久卷快照、或者用gitlab-backup恢复到新 PVC 实例。如果是中小团队自建的简化版 K8s 环境我的建议是别贪复杂直接按裸机思路处理先备份在临时 PVC 实例上恢复确认无误后再切流量。4.4 迁移后跑一遍 CI/CD 生态适配检查很多人以为数据迁过去了GitLab 能打开迁移就成功了。实际上你的 CI/CD 体系里还有一堆东西绑在旧环境中Runner 重新注册旧 Runner 的config.toml里有旧实例的注册 Token 或 URL迁移后如果不重新注册CI 任务会一直卡在 pending 状态。SSH Host Key如果客户端的 known_hosts 里缓存了旧机器的指纹迁移后第一次拉代码会报 Host Key Verification Failed需要更新。Webhook 地址所有配置了旧域名/旧 IP 的 Webhook都要批量改成新地址否则第三方系统调不通。域名和反向代理如果 GitLab 前端有 Nginx 反代迁移后要检查反代配置里的 upstream 地址是否指向新实例。这些内容官方文档不会一次性提醒你但它们才是迁移完能不能正常干活的关键。5. 我踩过的坑备份成功但恢复失败的几个根因备份与恢复最难受的不是备份失败而是备份日志显示成功恢复时才失败。以下五个问题我都在不同环境里真实遇到过每个都花了不少时间排查。5.1 gitlab-secrets.json 缺失恢复后用户密码全部失效有一次做容灾演练我特意模拟只恢复数据不恢复配置的场景。结果恢复完成后Web 能打开但所有账号密码都不对gitlab-rails runner查用户数据也正常数据库里用户记录都在。最后才想到是gitlab-secrets.json没恢复GitLab 生成的新密钥和数据库里的密文对不上。这个问题的本质是用户密码字段、2FA 相关数据、CI/CD 项目变量、外部集成的 Token 都是加密存储的加密密钥就存在gitlab-secrets.json里。恢复时必须让新实例读到旧密钥我的做法是先停服务把旧密钥放回去再执行恢复。如果你已经启动了服务务必将服务全部停止然后替换密钥再重新启动。5.2 备份文件权限对但 NFS 挂载导致文件损坏有一段时间我们备份目录直接挂在 NFS 上gitlab-backup create的日志从头到尾没报错。直到一次真实恢复时tar 包解压到一半报Unexpected EOF才发现 NFS 上有几个文件大小不对。检查下来是 NFS 客户端写入缓存和 GitLab 进程的 flush 时序问题文件虽然显示写完了但实际没完全落盘。从那以后我再也没把 GitLab 备份目录直接放在 NFS 上。生产环境的备份方案改成了本地磁盘 rsync 远端同步并且在远端同步校验文件大小。5.3 备份时磁盘空间满载备份包假成功还有一种情况更隐蔽备份过程中磁盘空间不够tar 打包直接退出但gitlab-backup create因为某个子任务已经成功返回了非零退出码Cron 没感知到因为很多人只重定向输出没检查退出码就认为备份成功了。实际生成的 tar 包是残缺的文件大小比正常备份小一大截。后来我在脚本里加了备份后自动校验大小的逻辑backup_file$(ls -t /var/opt/gitlab/backups/*_gitlab_backup.tar | head -1) min_size$((10 * 1024 * 1024 * 1024)) # 10GB根据实际环境调整 actual_size$(stat -c%s $backup_file) if [ $actual_size -lt $min_size ]; then echo Backup file too small, maybe failed! exit 1 fi5.4 跨版本恢复时 PostgreSQL 版本不匹配GitLab 每次升级都可能伴随 PostgreSQL 大版本升级比如 13.x 可能带 PostgreSQL 12到 16.x 已经带 PostgreSQL 14。如果你备份时的 GitLab 内置 PostgreSQL 是 12恢复目标实例的 PostgreSQL 是 14gitlab-backup restore一般会拒绝导入。Omnibus 的gitlab.rb里可以配置 PostgreSQL 数据目录但更稳妥的是恢复前先用gitlab-ctl status确认版本或者干脆安装与备份相同版本的 GitLab 再做恢复。如果你确实要在高版本 PostgreSQL 实例上恢复旧数据必须先用旧版本实例恢复然后走一次 GitLab 升级流程让内置的迁移脚本自动处理 PostgreSQL 的升级。5.5 Cron 任务没有加锁导致的重复备份互相覆盖这个问题前面提过但值得再强调一次出现后的现象备份目录里有多个 tar 包时间戳只差几小时文件大小忽大忽小但你看 Cron 日志每次执行都正常完成。实际上第一次备份进程还没结束第二次已经开始两个进程共享同一个临时数据库 dump 文件导致其中一个 dump 内容被覆盖生成一个数据不完整的备份。加锁的思路前面已经给了代码。还可以在 Cron 里指定一个足够长的执行窗口并且在执行前检查是否有遗留进程pgrep -f gitlab-backup create exit 06. 备份恢复演练怎么确定你的备份真的能救你备份有没有用这件事只有在真实恢复时才能证明。我见过太多团队脚本跑了半年从没验证过恢复等真出事才发现备份文件是坏的。所以备份方案必须配恢复演练而且演练要有周期性、有记录、有报告。6.1 演练频率和演练环境怎么搭建议至少每季度做一次完整恢复演练。演练环境不一定要和正式环境同等配置但版本必须一致。如果你用 Docker那最简单起一个同版本 GitLab 容器把备份数据恢复进去跑一遍检查清单验证完直接销毁容器。如果公司有条件我建议把演练环境固定成一套恢复演练专用虚拟机平时关机需要时开机。这样想验证哪个备份点都可以随时拉起。6.2 演练脚本化把恢复动作固化下来恢复操作如果不写成脚本每次演练都会有手忙脚乱的问题。下面是一个简单的演练脚本骨架#!/bin/bash # Assume fresh GitLab installed at /etc/gitlab and data dirs empty set -e # 1. stop services sudo gitlab-ctl stop puma sidekiq # 2. restore config and secrets sudo tar xzf /backup/config-*.tar.gz -C /etc/gitlab # 3. copy backup file sudo cp /backup/*_gitlab_backup.tar /var/opt/gitlab/backups/ # 4. run restore sudo gitlab-backup restore BACKUP$(basename /var/opt/gitlab/backups/*_gitlab_backup.tar | sed s/_gitlab_backup.tar//) # 5. reconfigure and restart sudo gitlab-ctl reconfigure sudo gitlab-ctl restart # 6. basic verification sudo gitlab-rake gitlab:check有了这个脚本每次演验证就是新装系统 - 跑脚本 - 验证三步走时间能压缩到半天以内。6.3 恢复演练发现的典型问题清单根据我自己的演练经验最常发现的问题有这几类备份目录保留时间设得太短演练时才发现需要的备份点已经被自动清理了。备份定时任务只配了备份没配推送远端演练时发现远端根本没有备份包。数据库备份成功但仓库目录备份因为文件权限问题被跳过日志里有一行 WARNING平时没注意。恢复时才发现/var/opt/gitlab/backups所在分区空间不足解压到一半失败。备份文件的用户属主变成了 root恢复进程是 git 用户直接报 Permission denied。这些问题全都可以通过一次定期演练提前暴露。所以别再问备份有没有必要做演练这种问题了真到了数据丢完再后悔代价远大于演练投入的时间。最后分享一个小技巧我习惯在每次重大变更升级大版本、迁移机器、调整存储路径前后都手动触发一次全量备份并把备份文件名的备注信息写到变更记录里。这样如果变更出了问题需要回滚我能清楚地知道哪个备份点是变更前最后一个可靠状态。别完全依赖 Cron 自动备份重大操作前后的手动备份是性价比最高的保险。
返回列表