上周帮团队把一个历史悠久的Git仓库迁到新的GitLab服务器,结果栽在LFS推送这一步。仓库里有大量设计稿和二进制素材,全部走Git LFS管理,迁移时推送LFS对象一直失败,整个仓库卡住推不过去。折腾了一整天,把认证、服务端限制、超时、证书这些坑挨个趟了一遍,最后才搞清楚问题到底出在哪。这篇就把整个过程记录下来,包括排查思路和最终解决手段,给后面还要做类似迁移的朋友一个参考。
如果你也是做DevOps、后端,或者负责代码仓库治理,平时经常跟GitLab和Git LFS打交道,那这篇文章应该能帮你省下不少冤枉时间。文章里涉及命令、报错分析、服务端配置判断,适合有一定Git基础但没深度碰过LFS迁移场景的开发者阅读;哪怕你完全没接触过LFS,把原理部分看完也能明白迁移时该注意什么。
1. 问题背景与迁移方案设计
1.1 这次迁移的完整场景还原
先交代一下背景。源GitLab是团队内部自建的,已经从14版本一路升到16版本,但服务器硬件老旧,磁盘空间也快见底,于是决定把全部仓库迁移到一套新搭建的GitLab实例上。新实例规划的版本是16.10,软件包走官方Omnibus方式部署,LFS存储使用本地目录,没有接外部对象存储。
迁移方式选的是最常规的“拉代码—换远端—推代码”路线。因为源仓库有几百个,本身不大,最大的一个仓库单仓库也就4GB左右,但里面通过LFS管理了大约3.2GB的二进制资源文件。理论上这种方式最省事,不至于动用GitLab自带的项目导入导出功能去处理一堆依赖关系。
实际操作时问题来了。常规的git push流程会先尝试推代码引用,再推送LFS对象。代码引用推得很顺利,但LFS对象一推就报错,报错信息显示LFS上传失败,然后整个push退出,仓库自然也就没迁移成功。
1.2 Git LFS的核心机制与迁移痛点
LFS这个概念看似复杂,原理其实很直白。普通Git仓库直接保存每个文件的完整快照,大文件一旦多起来,仓库体积会迅速膨胀,克隆、推送都变得很慢。LFS的解决思路是把大文件内容移出Git存储,单独存放在LFS存储中,Git仓库里只保留一个几十字节的文本指针,这个指针指向LFS对象的实际存储位置和文件名。
日常操作时,git lfs install会在Git层面注册一个filter,每次提交时把匹配规则的大文件用“指针替换”的方式写入Git对象,然后由LFS工具把真实文件上传到LFS存储。因此一个用了LFS的仓库,实际上由两部分组成:Git仓库历史中的指针文件,以及LFS存储中的真实文件。
这样划分带来的直接后果就是:迁移仓库时如果只处理Git引用,忽略了LFS存储,那么新仓库里只有一堆指针,实际文件一个都拿不到。GitLab在用户拉取或浏览时发现LFS对象缺失,会直接报404或者提示对象损坏。更麻烦的是,推送时如果LFS对象上传失败,git push会被整体中止,导致代码都没法过去。这次遇到的就是这种情况。
明白了这个机制,后续排查就有一条清晰的路线:一条链路看Git引用推送,另一条链路看LFS对象上传。两者都需要在新GitLab上具备相应权限和存储空间,任何一环出问题,迁移都完不成。
2. 迁移前最重要的三件事:LFS环境体检
2.1 本地环境检查:Git与Git LFS版本匹配
很多人一上来就急着克隆推送,其实迁移前先做一次环境体检,能规避大部分问题。本地环境是整套流程的起点,Git和Git LFS的版本必须互相兼容,尤其不能使用太老的LFS客户端。
我这次用的本地环境是一台Ubuntu 22.04服务器,Git版本2.34.1,Git LFS版本3.0.2。这个组合处于一个比较微妙的状态:Git LFS 3.x系列对服务端的要求相对较高,如果在旧的GitLab上执行推送,某些交互细节和2.x有差异。虽然这次问题的根因不在客户端版本,但检查版本仍然是第一步。
git --version git lfs version输入上面两行命令确认无误后,还要检查仓库内LFS是否正常初始化:
git lfs env如果输出中没有类似“git filter for lfs”相关的集成信息,说明当前仓库的LFS filter没有正确注册,提交、推送都可能会把大文件当成普通文件处理。
我建议在迁移前先在本机跑一遍git lfs ls-files确认仓库里LFS跟踪了哪些文件、文件数量和总体积。这样后面迁移时,心里清楚待推送的对象规模,便于判断是否超时。
2.2 新GitLab服务端侧检查项目设置
服务端侧的检查往往被忽略,但这里是整条链路上最容易出问题的一环。很多自建的GitLab实例,默认配置并不保证LFS一定可用。
首先要确认新GitLab实例的LFS是否在全局层面启用。GitLab的LFS功能默认开启,但部分发行版或定制安装可能改过配置。登录GitLab后台,依次查看“Admin Area → Settings → Repository”,找到“Git LFS”相关的开关,确保状态是启用。如果关闭了LFS,客户端推送时大概率会直接报错,或者推送后LFS对象不被存储服务端。
其次是项目层面的设置。在目标项目里,左侧导航点击“Settings → General”,展开“Visibility, project features, permissions”,确认“Git Large File Storage”是勾选状态。很多人在迁移时新建了项目但没有逐项检查功能开关,LFS功能被禁用后,推送LFS对象会返回错误或静默跳过,不容易察觉。
还有一个容易踩坑的地方是仓库大小限制。如果在GitLab后台配了Repository size limit,而仓库本身加上LFS对象已经超过了限制,推送时服务端会直接拒绝。这个限制在“Admin Area → Settings → Repository → Repository size limit”里配置,默认通常是空,也就是不限制。如果企业里有统一限额策略,迁移前务必确认限额是否够用。
2.3 盘点源仓库的LFS对象清单
迁移前,建议先在源仓库里盘点LFS对象。怎么盘点?用下面几个命令:
git lfs ls-files -s这个命令会列出所有被LFS跟踪的文件路径和大小。如果文件数量很多,可以加-l参数显示LFS对象ID:
git lfs ls-files -l还有一个更关键的视角:LFS对象在历史里有多少版本。因为Git支持回滚,历史中每一次文件修改都会产生一个LFS对象版本,哪怕当前工作区里已经没有这个文件了,它仍然存在于LFS存储中。迁移时必须把这些历史对象也带上,否则某些旧提交无法完整恢复。
要查看所有LFS对象的总数和总大小,可以统计git lfs ls-files -l的输出。如果发现仓库里有大量历史LFS对象,那么推送时耗时会非常长,需要考虑网络带宽和超时设置是否能够承受。
3. 实操:把已有仓库完整推到新GitLab
3.1 第一步:从源仓库拉取完整内容
迁移前,源仓库需要先被完整拉取到本地,这里说的“完整”包含所有分支、标签以及全部LFS对象。
git clone --mirror git@old-gitlab.example.com:group/legacy-repo.git cd legacy-repo.git git lfs fetch --all--mirror模式会克隆所有分支和标签引用,生成一个裸仓库目录,这是迁移的常用做法。裸仓库里没有工作区文件,但包含完整的Git历史。对LFS来说,裸仓库同样可以执行git lfs fetch,把远端LFS存储中的对象全部拉到本地缓存中。
这里有一个细节务必注意:git lfs fetch必须带--all参数。如果不带,LFS只会拉取当前HEAD所涉及的对象,历史版本中的LFS对象不会被拉取。迁移时如果遗漏了历史LFS对象,推送时依然会报失败,或者推上去之后仓库不完整。
拉取完后可以用git lfs ls-files再次确认本地缓存里对象的数量。如果远端LFS存储量比较大,这一步耗时会比较可观,我这次3.2GB的LFS对象拉取花了大约12分钟,主要是受限于源服务器带宽。
3.2 第二步:在新GitLab创建空项目并确认LFS配置
接下来在目标GitLab上创建新的空项目。注意创建时不要选择任何初始化模板(不要自动生成README、.gitignore),保持仓库完全空白,否则后面推送时容易产生无关联历史冲突。
创建完项目后,进入项目设置,按照前面说的步骤确认LFS功能已启用。再确认当前账号具备该项目的Maintainer或者更高权限,如果只有Developer权限,推送代码没问题,但是推送LFS对象时同样可能因权限不足被拒绝。
为了便于区分,本地裸仓库的远端地址先保持不变,稍后推送时单独指定目标地址,避免误操作把源仓库覆盖掉。
3.3 第三步:推送到新GitLab的正确姿势
这一步是整个迁移过程的核心。很多人的操作习惯是直接git remote add新地址然后git push --mirror,但这个操作遇到LFS仓库时不够稳妥,因为LFS对象推送可能隐藏在普通推送过程中,一旦失败,很难定位到底卡在哪一层。
更稳妥的做法是两条腿走路:先推送LFS对象,再推送代码引用。
先推LFS对象:
git lfs push --all git@new-gitlab.example.com:group/legacy-repo.git这条命令会把本地缓存的全部LFS对象推送到新GitLab的LFS存储中。--all参数确保所有历史对象也被推送,而不是只推送当前分支涉及的部分。
LFS推送成功后,再推送代码引用:
git remote add new git@new-gitlab.example.com:group/legacy-repo.git git push new --mirror为什么推荐这样的顺序?因为如果先推代码引用,Git会在服务端生成对应的提交记录,其中包含LFS指针;但这时如果LFS对象还没推上去,从用户角度看仓库已经存在,但实际大文件缺失,后续排查起来非常折磨。反过来先把LFS对象推上去,代码引用过去后仓库就是完整可用的状态。
3.4 失败现场:这次LFS推送到底报了什么错
我这次操作在第一步就卡住了。执行git lfs push --all后,LFS客户端开始分批上传对象,前面十几个小文件都很顺利,到了接近1GB的大文件时,进度条卡住,过了一会儿直接报错:
LFS: upload failed: (error: 500) Uploading LFS objects: 0% (0/1), 0 B | 0 B/s, done. LFS: failed to upload some objects报错信息很简略,只提示LFS上传失败,HTTP状态码是500。这个状态码说明客户端已经把请求发到了服务端,但服务端在处理过程中出了问题。这里要排除本地网络因素,重点查服务端日志。
查看GitLab生产日志是定位问题最快的方式。在GitLab服务器上执行:
sudo gitlab-ctl tail gitlab-workhorse或者看LFS相关的请求日志:
sudo gitlab-ctl tail | grep lfs我这边从日志里看到,LFS上传请求到达了GitLab,但在后端存储写入阶段报错,具体错误是和磁盘目录相关的权限异常。进一步检查后发现,新GitLab实例在安装时定义的LFS存储路径是/var/opt/gitlab/git-data/lfs-objects,但部署人员给这个路径配置了独立的磁盘挂载点,挂载点权限只对root开放,git用户无法写文件。服务端接收LFS对象后尝试保存文件时权限不足,随即返回500错误。
这个问题算是比较典型的自建GitLab LFS踩坑点。如果LFS存储放在自定义路径下,一定要确认目录所有者是否属于git用户和git组,并保证目录权限正确。
sudo chown -R git:git /var/opt/gitlab/git-data/lfs-objects sudo chmod 0750 /var/opt/gitlab/git-data/lfs-objects修完权限后重新执行推送,LFS对象顺利上传完成。
4. 常见问题与排查思路速查
4.1 报错401:认证与权限问题
LFS推送报401,通常有两种情况。第一种是请求LFS批处理接口时认证失败,客户端拿到的临时凭证无效。这种情况常见于使用HTTP方式连接GitLab,而GitLab配置了SSH密钥验证,或者客户端缓存的凭证过期。
第二种是账号有代码读取权限但没有LFS写入权限。虽然LFS对象和Git引用挂钩,但服务端权限校验时有细微差别。如果当前账号只是Developer角色,在某些GitLab版本中对LFS对象的写入会有问题。解决方法是把账号提升到Maintainer角色,或者用具备Maintainer权限的token执行推送。
推荐的做法是在迁移时使用带write_repository权限的Personal Access Token作为凭证,通过HTTPS方式推送:
git -c credential.helper='!f() { echo "username=oauth2"; echo "password=YOUR_TOKEN"; }; f' lfs push --all https://new-gitlab.example.com/group/repo.git用HTTPS + Token方式能绕开SSH key配置不一致带来的麻烦,迁移完再清理本地凭证。
4.2 报错413或500:服务端限制与反向代理
413错误表示请求体过大,常见于GitLab部署在Nginx反向代理后面,而Nginx的client_max_body_size默认只有1MB,远小于LFS大文件的体积。此时需要在Nginx配置里调大上传限制:
client_max_body_size 0;如果使用GitLab自带的Nginx,可以在/etc/gitlab/gitlab.rb中设置:
nginx['client_max_body_size'] = "4g"修改后执行sudo gitlab-ctl reconfigure生效。500错误则要复杂一些,需要从服务端日志入手。我这次遇到的是LFS存储目录权限问题,但其他情况也见过,比如对象存储配置不当、磁盘满、数据库连接异常等。通用的排查入口是:
sudo gitlab-ctl tail production.log sudo gitlab-ctl tail gitlab-workhorse关注报错堆栈里的异常类名,结合日志关键词搜索,基本能缩小到具体模块。
4.3 推送中断后如何安全地恢复
LFS对象推送是分批次进行的,中途断开后重新执行推送命令,已经上传的对象默认会跳过,不会重复上传。但如果本地LFS缓存中的对象不完整,或者服务端存储有部分损坏,恢复时就得先把本地LFS缓存补齐。
git lfs fetch --all这个命令会重新从源LFS存储拉取缺失对象到本地缓存。拉取完成后再次执行推送即可。这里提醒一点,恢复推送时不要反复手动删除本地.git/lfs/objects目录,否则每次都得全量重拉,浪费时间。
如果推送过程反复失败在同一个对象上,可以尝试单文件推送调试:
git lfs push --object-id git@new-gitlab.example.com:group/repo.git <object-id>这个命令只推送指定的LFS对象,便于聚焦问题。确认单个对象能否成功,基本能判断是服务端问题还是对象本身损坏。
4.4 常见Git LFS错误速查表
| 错误现象 | 可能原因 | 解决方向 |
|---|---|---|
| 401 Authenticated requests failed | Token无效 / 权限不足 | 更换可用Token,确认账号角色 |
| 403 Forbidden | 项目LFS开关未开启 / 限额超限 | 检查项目设置和Admin限制 |
| 413 Request Entity Too Large | 反向代理body大小限制 | 调大Nginx client_max_body_size |
| 500 Internal Server Error | LFS存储路径权限异常 / 磁盘满 | 检查git用户目录权限与磁盘空间 |
| 501 Not Implemented | GitLab版本太老或未开启LFS | 升级GitLab或开启LFS开关 |
| Upload timed out | 大对象上传超过服务端超时时间 | 调整workhorse超时配置,或分批推送 |
5. 如果一条路走不通:备选迁移方案
5.1 使用GitLab自带的项目导入导出
推送LFS一直失败的情况下,可以考虑放弃“本地中转”方案,改用GitLab原生的项目导出导入。这个方案的最大优势是服务端打包、服务端导入,LFS对象和Git历史会一并处理,不需要经过本地网络中转。
具体操作:源GitLab上进入项目,点击“Settings → General → Export project”,等待后台生成一个tar.gz格式的项目导出包。然后到新GitLab的“New project → Import project → GitLab export”上传这个压缩包等待导入完成。导入完成后LFS对象会同步导入。
这个方案适合两个GitLab实例都安装了Git LFS且版本兼容的情况。如果项目非常大,导出过程会比较耗时,且导入时对服务端资源占用较高,建议在低峰期执行。
5.2 临时调整跟踪规则后重新提交
还有一种场景是仓库里的大文件没有用LFS跟踪,而是直接以普通文件方式提交进了Git历史,导致Git对象库里体积巨大。这时候迁移失败可能也不是LFS的问题,而是普通Git对象太大推不上去。
处理思路是用git lfs migrate把历史中的特定类型文件改写为LFS指针。这个命令会重写历史,操作前一定要确认所有协作成员都已经同步过仓库,否则可能导致大家本地历史分叉。
git lfs migrate import --include="*.psd,*.zip,*.mp4" --everything执行后,这些历史文件会被替换成LFS指针,真实文件存入LFS存储。随后用git push --mirror和git lfs push --all推送到新GitLab。这个操作有一个风险:它会更改所有文件的SHA-1哈希,历史提交ID全部变化,对已有的CI流水线、issue引用会有影响,务必谨慎。
5.3 最后兜底:只迁移代码历史,剥离大文件
如果LFS存储始终无法在新GitLab上正常工作,还可以选择只迁移代码内容,把历史中的大文件彻底剥离。这种方案适用于大文件已经没有价值、或者旧版本不再需要保留的场景。
思路是重新初始化一个仓库,只保留当前分支的最新代码,不保留历史:
git clone git@old-gitlab.example.com:group/legacy-repo.git cd legacy-repo rm -rf .git git init git add . git commit -m "Initial import without LFS"或者保留历史但剔除所有LFS文件路径,把LFS指针留在仓库里:
git filter-branch --index-filter 'git rm -rf --cached --ignore-unmatch "*.psd" "*.zip"' HEAD这种做法本质上是在放弃部分数据完整性的前提下最小成本达成迁移目标。如果仓库里的大文件还承载着业务价值,不建议轻易走这条路,毕竟历史数据丢失后很难找回。
6. 迁移完成后的验证与收尾
6.1 验证LFS对象完整性
推送成功不代表一切结束,迁移后至少要验证一遍LFS对象的完整性。最直接的办法是从新GitLab重新克隆一次仓库,然后执行LFS拉取:
git clone git@new-gitlab.example.com:group/legacy-repo.git cd legacy-repo git lfs pull如果所有文件都能正常拉取,说明LFS对象在服务端存储是完整的。还可以执行git lfs ls-files,输出结果应该和源仓库中的LFS文件清单一致。
另一个验证点是抽查历史版本中的LFS对象。使用git checkout切换到某个旧提交,确认对应的大文件能被正常检出,而不是只剩下指针内容。
6.2 旧实例数据保留策略
迁移完成后,旧GitLab实例上的仓库暂时不要急着删除。建议保留一到两个月作为备份,期间观察新实例的运行情况,确认LFS相关功能稳定后再清理。毕竟LFS对象存储一旦丢了,几乎没有恢复的可能。
对于采用对象存储(比如S3、MinIO)的实例,还要确认LFS对象是否定期备份。如果之前没有备份策略,这次迁移正好是个契机,把LFS存储纳入备份体系,避免将来单点故障导致数据永久丢失。
这套流程走下来,我对LFS迁移的判断是:报错并不可怕,关键是要搞清楚LFS请求在整条链路上的流转过程。先验证本地缓存是否完整,再确认服务端权限和存储配置,最后才考虑网络和超时因素。排查的顺序决定了解决问题的速度,盲目重试只会浪费时间。
最后再分享一个小技巧:迁移这种带LFS对象的大仓库前,先给源GitLab的LFS存储目录打一个快照备份,这样即便操作失误导致对象损坏,也能用备份快速恢复。我这次就是靠这份备份兜底,才敢反复试验各种方案,最终顺利把仓库迁了过去。