前几天一个同事找我,说他被一条报错卡了两个小时:推送一个包含 900MB 数据集的仓库时,GitHub 直接拒绝,提示remote: error: File is too large。他想让我帮他“绕过限制”,但我先问了一句:你为什么要用 Git 存一个 900MB 的压缩包?其实这个问题的背后,就是 GitHub 配置大文件上传这件事被很多人理解成了“怎么把大文件塞进仓库”,而真正的答案是:要么用 Git LFS 这类工具改变存放大文件的方式,要么干脆不要把文件放到 Git 里。
这篇文章我会从 GitHub 的尺寸限制讲起,再到 Git LFS 的完整配置流程、报错排查、替代方案,最后说说我自己沉淀下来的一套标准操作。适合第一次踩坑的人,也适合准备给团队制定代码仓库规范的技术负责人。
1. 先搞清楚 GitHub 到底卡在哪里
1.1 单文件 100MB 限制与警告线的差异
GitHub 对单个文件有一套不太一样的判断方式:当文件超过 50MB 时,push 过程会提示警告,但还能继续;一旦达到 100MB,远程端直接拒绝接收。这里的“文件大小”并不是你当前工作区看到的文件大小,而是 Git 对象中 blob 的实际体积。很多人会在这里被误导——本地明明只有一个 60MB 的压缩包,却因为历史版本里存在一个 300MB 的中间产物,push 照样失败。
Git 的对象模型按内容寻址,每次 commit 都会把所有变更内容保存成新的对象。即使后来的提交里你把大文件删掉了,之前那个 blob 仍然躺在.git目录里,远程服务器扫描历史时不会被忽略。所以那些说“我把文件删了怎么还报错”的人,其实删的是工作区,不是 Git 历史。在网页端上传时,限制更严格,单个文件不能超过 25MB;超过就只能走本地客户端 push。
1.2 仓库整体体积的隐性红线
除了单文件限制,GitHub 还希望仓库整体体积保持克制。官方建议仓库保持在 1GB 以下,超过 5GB 可能会收到提醒,甚至要求整理。这个建议背后是真实的成本问题:克隆一个仓库会把当前分支和全部历史对象都拉下来,仓库体积越大,克隆越慢,磁盘占用越高。一台性能普通的电脑克隆一个 10GB 仓库,大概率要等几分钟以上,这对团队协作是难以接受的。
另外,Git 的存储特性特别容易带来“体积陷阱”:一个大文件哪怕只改过一个字节,新 commit 也会把完整的新版本存成对象,而旧版本对象依然保留。如果你持续更新一个 500MB 的二进制文件 10 次,仓库里就是 5GB 的历史积累。理解了这两条限制,再回头看“大文件上传配置”,你就能明白重点不是把文件强行塞进 Git,而是决定哪些文件应该留在 Git 里、哪些应该被 LFS 管理、哪些必须完全挪出去。
我把常见的限制项整理成一张表,方便你快速判断自己的仓库是否已经踩线:
| 限制项 | 触发阈值 | 后果 |
|---|---|---|
| 单文件警告 | 超过 50MB | push 会提示,但不中断 |
| 单文件硬限制 | 超过 100MB | 远程拒绝,push 失败 |
| Web 上传限制 | 超过 25MB | 网页端无法上传 |
| 仓库体积建议 | 大于 1GB | 克隆和操作体验明显变差 |
| 仓库体积警戒 | 大于 5GB | 可能被平台方要求瘦身 |
2. Git LFS 配置实操:从安装到推送
2.1 Git LFS 的底层原理
Git LFS(Large File Storage)并不是把文件压缩变小,而是把大文件从 Git 历史里“挪出去”,换成一个轻量级指针。用生活化的类比来说,它相当于在仓库里放了一张“存单”,真正的大文件放在保险柜,只有需要取用时才凭单取货。当你对某个扩展名执行git lfs track,后续这类文件会被替换为一个小文本文件,里面记录了版本号、对象 ID、文件大小等元数据,Git 本身只看到几百字节的指针。
真实的大文件则由 LFS 客户端上传到独立的存储服务。这样一来,Git 历史只记录指针的变化,仓库体积不会跟着二进制文件一起膨胀。克隆或拉取时,Git LFS 会根据指针去下载真实文件,甚至可以按需下载,而不是一次性全部拉取。这就解释了为什么 LFS 不是“绕过限制”,而是“改变大文件的存储位置”。它的核心价值是既保留版本管理能力,又不会让仓库变成一个巨型二进制仓库。
2.2 安装、初始化与版本检查
不同操作系统安装方式略有差异,但安装之后的关键步骤是一致的:一定要执行git lfs install,否则后续的 track 和 push 都不会自动触发 LFS。我之前见过有人只安装了 git-lfs 却没有初始化,结果 push 大文件时依然走普通 Git 对象,照样被 100MB 限制拦下来。
# macOS brew install git-lfs # Ubuntu / Debian sudo apt install git-lfs # Windows 包管理器示例 choco install git-lfs # 所有系统安装后都必须初始化 git lfs install git lfs versiongit lfs install这条命令会在 Git 配置里写入filter.lfs相关参数,让 Git 知道遇到 LFS 指针时该调用哪个程序处理。默认是写入当前用户的全局配置;如果是在多人共用的服务器上,或者你希望只对单个仓库生效,也可以进入仓库目录后执行git lfs install --local。安装完成后建议用git lfs version确认版本号,太老的版本在部分平台兼容性较差。
2.3 用 git lfs track 锁定需要跟踪的文件类型
初始化的下一步是告诉 Git LFS:哪些文件需要接管。最常见的做法是按扩展名匹配,例如:
git lfs track "*.zip" git lfs track "*.tar.gz" git lfs track "*.bin" git lfs track "models/weights.h5"注意 track 后面的参数是路径模式,不是具体路径。如果只想跟踪 models 目录下的某个特定文件,可以写成git lfs track "models/weights.h5",路径以仓库根目录为基准。执行这些命令后,仓库里会生成一个.gitattributes文件,记录类似*.zip filter=lfs diff=lfs merge=lfs -text的内容。这个文件非常重要,必须提交到仓库里,团队其他成员 clone 后才会自动应用相同的跟踪规则。
有几个细节值得特别留意:不要把*这种匹配所有文件的模式写进去,否则文本文件也会被当作 LFS 对象处理,提交时可能出现换行符被改写的问题;规则也不要写得过于宽泛,比如data/这样没有明确后缀的目录,建议配合白名单后缀一起使用。配置完成后,你可以用git lfs ls-files查看当前哪些文件被 LFS 管理,也可以用git lfs status查看暂存区里待处理的大文件,确保没有遗漏。
2.4 推送、克隆和拉取的正确姿势
配置好 track 规则之后,日常操作和普通 Git 没有太大区别:
git add . git commit -m "add large files with lfs" git push origin main第一次 push 时,Git LFS 会把所有匹配规则的大文件上传到远程存储,终端输出里会出现Uploading LFS objects的进度。这时不要中断进程,等它把全部对象传完,再继续显示普通 Git 的推送进度。如果某个文件上传失败,整个 push 也会失败,需要解决后重试。
克隆时也有讲究。普通的git clone会先下载 Git 对象,然后尝试把所有 LFS 文件也拉到本地。如果仓库里管理着几个 GB 的素材,全量下载会很痛苦。推荐用下面这两种方式按需获取:
# 跳过 LFS 文件的自动下载 GIT_LFS_SKIP_SMUDGE=1 git clone <repo-url> # 或者使用 filter 参数,延迟下载 blobs git clone --filter=blob:none <repo-url>之后需要真实文件时,再执行git lfs pull或git lfs pull --include="models/*"。在 CI 构建或临时环境下,这种按需拉取能省去大量等待时间,磁盘空间也不会被无意义占用。
3. 如果不想用 Git LFS:替代路径怎么选
3.1 已经误提交大文件:用工具重写历史
有一个场景比不会配置更常见:大文件是在没有.gitattributes保护时被提交进去的,现在仓库历史里已经躺了好几个 300MB 的安装包,push 直接被拒。这时候即使立刻补上 track 规则也没用,因为旧对象还在历史中。唯一的出路是重写历史,把大文件从所有提交记录里剔除。
首选工具是git filter-repo,操作方式可以直接通过命令完成。比如我想把所有.zip文件从历史里彻底删除:
git filter-repo --path glob:*.zip --invert-paths如果你更希望保留大文件并转为 LFS 管理,可以用git lfs migrate:
git lfs migrate import --include="*.zip" --everything这两个命令都会重写整个提交历史,所有 commit hash 都会发生变化。操作之前一定要先备份一份完整仓库,团队成员也要提前同步,确认所有人都不需要旧历史之后再执行。重写完成后,通常需要git push --force才会生效。这是一个有风险的操作,必须当成一次小型发布来处理。
3.2 把大文件放到 Git 仓库之外
如果文件数量多但不是经常变,比如发布用的安装包、预训练模型权重、数据集,最省事的方案其实是彻底不放进 Git。你可以把文件上传到对象存储、内部共享盘或者数据集托管平台,在 README 里写清楚下载地址和校验值。这样做的好处是 Git 仓库始终清爽,克隆速度不受影响,文件分发还能用内容分发网络加速,对大体积文件非常友好。
代价是这些文件失去了版本管理能力,更新后没有历史记录,也没法在代码审阅流程里做差异对比。如果文件本身很重要且更新频繁,用 Git LFS 更合适;如果只是定期发布新版本,旧版本还需要可靠保存,那么外部存储加命名规范(比如文件名包含日期或版本号)就已经足够了。我见过很多团队为了省事把所有类型的文件都往 Git 里塞,最后仓库膨胀到连正常开发都受影响,这种本末倒置实在不划算。
3.3 私有化托管与自建 Git 服务的取舍
对于团队规模较大、又不希望受公共平台默认限额约束的情况,可以在内网部署一套自建 Git 服务。市面上有不少开源 Git 服务器程序可选,部署后可以自定义单文件大小上限,甚至可以配置独立的大文件存储后端。内部网络环境下,带宽和流量都不再是瓶颈,数据安全性也会更有保障,尤其适合对合规有特殊要求的团队。
但自建服务并不是免费的:Git 服务本身不复杂,但大文件存储、备份策略、权限体系、监控告警都需要自己维护。一旦磁盘配额满了或者备份失败,影响面往往比公共平台更大。以我接触过的团队来说,5 人以下的小团队自建服务很容易变成运维黑洞;与其折腾自建,不如先考虑公共托管平台的付费方案或外部对象存储,性价比会高很多。只有当团队规模、数据安全要求或者网络隔离需求到了某个临界点,自建才是理性选择。
3.4 选型对比:什么场景用 LFS,什么场景别用
| 文件类型/场景 | 建议方案 | 核心原因 |
|---|---|---|
| 经常修改的二进制资产(设计稿、音视频素材) | Git LFS | 保留版本历史,团队工作流统一 |
| 一次性发布的安装包、数据集 | 外部存储 + 链接 | 不需要历史,下载速度快且成本低 |
| 历史已包含大文件的旧仓库 | filter-repo / migrate 重写历史 | 必须先清理历史才能解除 100MB 限制 |
| 对数据主权要求高的内网团队 | 自建 Git 服务 | 数据不出内网,限制和策略自己定制 |
这张表是我做技术方案时常用的判断框架。如果你问一个文件“应不应该放进 Git”,本质不是看它大不大,而是看它是否需要和代码一起做版本演进。一个设计稿如果每周都要改,团队要追踪历史版本,适当用 LFS;一个模型权重文件发布后基本不变,我倾向直接放对象存储,在文档里留好下载方式就够了。
4. 高频问题排查与避坑实录
4.1 push 报错 File is too large,但不知道是哪个文件
GitHub 的报错信息通常会给出文件名和大小,但有时候文件名只是历史中的中间文件,本地早就找不到了。一个快速定位方式是找出仓库里最大的那些 blob 对象:
git rev-list --objects --all \ | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \ | awk '/^blob/ {print $3, $4}' | sort -rn | head -20这个命令会列出全历史里体积最大的 20 个文件对象。找到之后,再利用git filter-repo或git lfs migrate做针对性清理。很多人一开始不知道问题出在哪个文件,其实大部分场景下都是历史里某个被删除的压缩包在“作祟”,不要只盯着当前工作区找。
4.2 LFS 配额和带宽不足
GitHub 免费账户的 LFS 配额通常是 1GB 存储和每月 1GB 带宽,这个额度非常少。只要团队连续上传几轮设计稿或素材,很快就用完了。之后你会看到类似batch response: Access denied或者exceeded your Git LFS storage quota的报错。
处理办法有两个:一是购买额外的存储和带宽用量,通常在仓库 Settings 的存储页面查看用量和付费入口;二是把大文件从 LFS 迁移到外部存储。我的建议是,只要团队开始使用 LFS,就定期看一眼用量,不要等到发布日才发现文件拉不下来。如果是组织或企业账户,额度可能不同,以对应平台的账户设置页为准。
4.3 .gitattributes 规则冲突
多人协作时,.gitattributes容易成为冲突点。比如 A 同事提交了*.psd filter=lfs,B 同事在另一个分支里把这个规则改成了普通文本规则,合并时 Git 无法自动判断,就产生冲突。解决方案是重新执行git lfs track写下你期望的状态,再手动处理冲突块。
更稳妥的做法是规定.gitattributes只由仓库维护者修改,其他成员通过 Pull Request 提出变更,经过审阅后再合并。另外,.gitattributes的匹配规则也有优先级,像/data/这种带路径前缀的规则要放在文件靠前的位置,避免被后面的泛规则覆盖。你不希望某天加了一个*规则,把整个仓库的文本文件都变成 LFS 对象。
4.4 clone 后 LFS 文件没有自动下载
如果你 clone 完成后,目录里只有几十上百字节的指针文件,说明 LFS 拉取没有执行。最常见的原因是执行过GIT_LFS_SKIP_SMUDGE=1,或者当前仓库没有正确安装 Git LFS 过滤器。先用git lfs install确认全局配置存在,再执行git lfs pull origin <branch>手动拉取指定分支的 LFS 文件。
如果手动拉取仍然失败,就要检查当前网络环境是否能正常访问 LFS 存储域名。部分公司安全网络会限制外部存储地址的访问,需要让运维放行对应域名后才能继续。这个问题在企业环境里很常见,不要一直怀疑配置,先换一个网络环境试一次,就能快速定位问题。
5. 我现在的标准操作流程
最后分享几条吃了多次亏后形成的个人习惯。
第一,新仓库一创建就先想清楚哪些路径是二进制大文件,提前写好.gitattributes并提交。等第一份大文件进来再补 track,往往已经晚了,历史里可能已经出现了超限对象。第二,LFS 适合几百 MB 以内、确实需要版本演进的文件;超过几个 GB,或者下载频次很高的交付物,放到对象存储更省心。第三,任何重写历史或迁移操作,先克隆一个备份仓库,确认所有同事都 pull 完再动手。第四,在 CI/CD 里克隆有 LFS 的仓库,尽量用git lfs pull --include=按需下载,避免每次构建都拉全量。
我见过很多团队为了省一点存储费用,把几百 MB 的二进制文件硬塞进 Git,最后在协作、备份、恢复上花的时间成本远高于那点存储费。GitHub 配置大文件上传这件事,本质不是“怎么绕过限制”,而是“在正确的位置存储正确的数据”。想清楚文件需要什么样的生命周期,再决定该放在哪,你就不会再被 100MB 的报错卡住。