排查 Git 中大文件导致的性能问题
诊断由大文件引起的 Git 仓库缓慢问题,谨慎选择 Git LFS,并在不惊动团队的情况下清理历史记录。
排查 Git 中大文件导致的性能问题
大文件对 Git 的影响往往容易被忽视,直到仓库变得难以使用。单个视频、压缩包、数据库转储或设计文件在添加时可能看起来并不危险。问题在于当该文件被多次修改时。Git 会保留历史记录,每次克隆都需要携带这些历史记录。
起初症状通常不明显。git clone 花费的时间比预期长。git fetch 在酒店 Wi-Fi 下感觉缓慢。CI 任务在构建前花费过多时间检出仓库。开发者开始使用旧的本地克隆,因为重新克隆很麻烦。这时就该检查仓库,而不是让大家耐心等待。
确认大文件是问题所在
从简单的检查开始:
du -sh .git
git count-objects -vH
du -sh .git 告诉你本地仓库数据库的大小。git count-objects -vH 显示松散对象和包文件大小。如果包文件大小与实际源代码树相比很大,那么历史记录很可能携带了旧的有效载荷。
要查找当前检出中的大文件:
find . -path ./.git -prune -o -type f -size +10M -print
这仅显示当前存在的文件。仓库可能因为几个月前删除的文件而变慢。要检查历史记录,Git LFS 在迁移之前就能提供有用的报告:
git lfs migrate info --everything --above=10MB
如果未安装 Git LFS,你仍然可以通过 Git 底层命令进行调查,但上述命令通常是针对此特定问题最直接的查看方式。
决定哪些内容属于 Git
并非每个大文件都是错误。少量稳定的二进制资源可能没问题。基础设施代码的仓库不应包含虚拟机镜像、数据库备份、客户导出文件或构建产物。游戏仓库可能合法地包含美术和音频资源,但这些文件通常需要 Git LFS 或独立的资源系统。
一个实用的规则是:Git 非常适合源代码文本和小型支持文件。Git 不适合频繁变化的二进制大对象。如果文件无法在差异比较中有意义地审查,并且经常变化,那么它可能不应该作为普通的 Git 对象存在。
适合 Git LFS 的常见文件类型包括:
*.psd
*.ai
*.mp4
*.mov
*.wav
*.zip
*.uasset
*.fbx
*.blend
对宽泛的图片模式要小心。在设计密集的仓库中跟踪所有 *.png 文件可能很有帮助,但对于包含许多小图标的 Web 应用来说可能会很烦人。模式应匹配实际导致问题的文件。
为未来的大文件使用 Git LFS
Git LFS 在 Git 中存储一个小的指针文件,并将大文件内容存储在 LFS 存储中。正常的 Git 历史记录保持轻量,同时用户在 LFS 下载时仍能在工作树中获得真实文件。
安装并初始化它:
git lfs install
跟踪你实际需要的文件模式:
git lfs track "*.psd"
git lfs track "*.mp4"
git add .gitattributes
git commit -m "使用 Git LFS 跟踪大型设计和视频文件"
.gitattributes 文件很重要。提交它,以便所有人都使用相同的 LFS 规则。
之后,正常添加文件:
git add demo.mp4
git commit -m "添加产品演示视频"
git push origin main
协作者应在使用该仓库前安装 Git LFS。如果他们在没有 LFS 支持的情况下克隆,可能会看到指针文件而不是真实资源,直到他们安装 LFS 并运行:
git lfs pull
同时检查 Git 托管平台的存储和带宽策略。Git LFS 解决了 Git 对象膨胀问题,但并不能让大型资源的存储或传输免费。
迁移现有历史记录
今天启用 LFS 并不会自动修复昨天的提交。如果一个 700 MB 的压缩包被提交后又删除,它仍然可能存在于历史记录中。清理它需要重写历史记录。
重写历史记录会更改提交 ID。任何拥有现有克隆的人都必须小心地重新同步,并且打开的拉取请求可能需要变基或重新创建。请在维护窗口内进行此操作,并首先创建镜像备份:
git clone --mirror [email protected]:ORG/REPO.git repo-backup.git
然后在一个新的克隆中工作。确保工作树是干净的:
git status
检查将要迁移的内容:
git lfs migrate info --everything --above=10MB
尽可能按模式迁移:
git lfs migrate import --everything --include="*.psd,*.mp4,*.zip"
如果仓库中有许多未知的大文件,也可以按阈值迁移:
git lfs migrate import --everything --above=10MB
在推送前审查结果:
git log --oneline --decorate -5
git lfs ls-files
git status
git lfs migrate info --everything --above=10MB
如果迁移达到了预期效果,谨慎地推送重写后的分支和标签:
git push --force-with-lease origin main
git push --force-with-lease origin --tags
对于有许多活跃分支的仓库,决定哪些分支重要。你可能不需要重写每个废弃的分支,但任何仍然包含大对象的分支都可能使远程仓库保持臃肿。
历史记录重写之后
明确告知团队成员发生了什么变化。最清晰的指令通常是重新克隆。如果人们有本地工作,他们应该先保存:
git status
git branch my-work-before-lfs-migration
git fetch origin
git rebase origin/main
对于混乱的本地克隆,重新克隆比尝试精细修复旧历史记录风险更低。
远程存储可能不会立即缩小。托管提供商会保留一段时间的不可达对象,有些需要联系支持或进行仓库维护后存储数字才会更新。在本地,确认迁移无误后可以清理旧对象:
git reflog expire --expire=now --all
git gc --prune=now --aggressive
不要用清理命令替代审查。它们会使旧的本地对象更难恢复。
防止同样的问题再次发生
如果大文件意外出现的情况持续发生,可以添加预提交或预接收检查。本地的预提交钩子可以在开发者提交大型工件之前发出警告。服务器端规则更强大,因为它能保护共享仓库,即使有人跳过了本地钩子。
一个简单的本地检查可以拒绝超过选定大小的文件,除非它们已经被 LFS 跟踪。具体阈值取决于项目。文档站点和游戏项目不应使用相同的限制。
同时修复文件的来源。如果 CI 在仓库内创建了 dist/、target/、覆盖率报告、压缩包或截图,请在 .gitignore 中添加正确的条目:
dist/
target/
coverage/
*.log
*.zip
不要盲目忽略文件。确保被忽略的路径是生成的输出,而不是源输入。
当 LFS 不是答案时
Git LFS 不是一个通用的工件存储库。构建产物通常应属于包注册表、对象存储、发布资产或 CI 工件存储。数据库转储应属于备份存储。大型数据集可能需要数据版本控制工具或单独的存储工作流。
目标不是向 Git 隐藏每个大文件。目标是保持仓库足够快,以便人们可以克隆、分支、获取和审查,而无需与工具斗争。
一次好的清理会留下三样东西:清晰的 .gitattributes 规则(用于属于 LFS 的文件)、.gitignore 规则(用于永远不应提交的文件),以及一份简短的团队说明(解释现有克隆应如何重新同步)。这样才能确保修复不会成为一次性的清理,而需要在下个季度重复进行。