Git LFS 拉取大文件实战:配置、排查与历史清理
前阵子组里一个同事把 1.2G 的设计源文件直接 commit 进了主干第二天早上全组拉代码进度条齐刷刷停在Receiving objects: 68%接着就是二十分钟的沉默。这事之后我把Git LFS 拉取大文件这套流程从头到尾重新捋了一遍从安装配置、追踪规则、按需拉取到配额爆表、指针错乱、历史清理全部自己踩了一遍。这篇东西就是那几天的实际操作记录包含我最终稳定使用的参数配置、失败排查路径以及几个只有真被坑过才知道的细节。它适合三类人看第一次给仓库接入 LFS 的开发者、正在被大文件拖慢拉取速度的人、以及接手了一个历史里全是大文件的老仓库、准备清理但不敢下手的维护者。不需要你对 Git 底层有多深理解但至少要能跑通git clone和commit。1. 搞清楚 Git 为什么会被大文件拖垮1.1 Git 的存储模型天生不待见二进制大文件Git 是一套内容寻址的版本控制系统每当你提交一次修改它并不是记录差异而是把变更后的完整文件重新算一次 SHA 存进去。文本文件好办因为 Git 在做打包packfile时会用 delta 算法把相似内容压缩在一起一个 1000 行的代码文件改了两行实际新增的数据可能只有几十字节。但二进制文件没这个福利一个 200MB 的 PSD 改了一个图层压缩后依然是接近 200MB 的新对象。改十次仓库就实打实多出接近 2GB 的历史。更麻烦的是拉取环节。git clone默认要把仓库全部历史的对象都下载到本地这包括十几次修改里每一个版本的大文件。所以真正拖垮拉取速度的不是当前那个大文件而是它背后堆积的全部历史版本。这解释了一个常见困惑仓库在托管平台上看着只有 300MB本地 clone 却要下 4GB原因就在 packfile 里塞满了历史大对象。1.2 LFS 的核心思路真身搬走仓库里只留一张取货单Git LFSLarge File Storage的做法非常直接把大文件的真实内容从 Git 对象库里拿出来存到一个独立的 HTTP 存储服务上而 Git 仓库里只保留一个约 130 字节的指针文件内容大概长这样version https://git-lfs.github.com/spec/v1 oid sha256:4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393 size 1234567890它的工作原理建立在 Git 的clean/smudge filter机制上。你git add一个大文件时clean过滤器介入把文件内容替换成上面那三行指针再入库你git checkout时smudge过滤器反向工作读到指针就去 LFS 服务把真身下载回来替换掉工作区里的那三行文本。整个过程对用户基本透明你在文件管理器里看到的还是完整的大文件。指针里的oid是内容的 SHA-256也就是文件在 LFS 存储中的唯一 ID。这个设计带来的关键好处是仓库历史里留下的只有指针指针本身几乎不占空间后续再怎么改这个大文件历史增长的成本都转移到了 LFS 存储上。代价是克隆和切换分支时多了一层网络请求如果 LFS 服务不可达或者文件已经被删你拿到的工作区里就只有几行文本这就是后面要讲的指针错乱。1.3 什么文件该进 LFS什么文件纯属自找麻烦LFS 不是把什么东西都往里塞就完事。我见过有人把*.png全量追踪结果一个只有 20KB 的小图标也要走一趟 HTTP 下载拉取速度反而变慢了。判断标准其实就两条文件是否二进制、单次版本体积是否足够大、改动是否低频。文件类型建议原因PSD / AI / Sketch 设计源文件强烈建议单文件动辄几百 MB二进制压缩率低视频、音频素材强烈建议体积极大历史版本增长快3D 模型、游戏资源包fbx、assetbundle建议二进制体积大但注意改动频率数据集csv、parquet、bin建议常作为训练数据体积大且不常改编译产物so、dll、aar、jar看情况若能通过构建生成不如加进.gitignore100KB 以下的小图标、JSON 配置不建议走 LFS 的额外网络开销大于收益纯文本代码、Markdown绝对不要Git 自己的 delta 压缩效率远高于 LFS一个经验阈值单文件稳定超过 1MB 且是二进制格式就可以考虑纳入 LFS。但阈值不是死线还要看改动频率。如果一个大文件每周都要改十次那 LFS 存储会迅速膨胀这时候更该反思的是工作流程比如把中间产物拆出去、用产物仓库单独管理而不是无脑往 LFS 里堆。2. 环境准备把 Git LFS 装对并让它真正生效2.1 三个平台的安装方式别装个假 LFS先说最简单的事实git lfs version能打印出版本号才算真的装上了。很多人以为自己装了其实只是 Git 主程序LFS 是独立组件。Windows 用户走官网下载git-lfs-windows-amd64安装包一路下一步即可。如果你装的是较新版 Git for Windows安装向导里会有一个 Git LFS 的勾选项勾上就一起装了。装完在 PowerShell 里执行git lfs version输出类似git-lfs/3.4.1 (GitHub; windows amd64; go 1.20.7)才算通过。macOS 用brew install git-lfsUbuntu/Debian 用sudo apt install git-lfsCentOS/RHEL 系可以先curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh | sudo bash再sudo yum install git-lfs。注意内网环境经常出现服务端支持 LFS、客户端却报 404的情况先确认客户端版本再确认服务端是否开启了 LFS 支持。有些私有部署版本默认关闭了这个功能需要在后台手动打开。如果平台包管理器的版本太老比如装出来是 2.x那建议直接去 Release 页面下二进制包手动放到PATH里。老版本在处理大批量并发下载时的稳定性明显不如 3.x。2.2 git lfs install 到底改了哪些配置装好之后有一步千万别漏git lfs install。这条命令做的事是在 Git 全局配置里写入四个 filter 定义让 Git 在add和checkout时知道该调用 LFSgit lfs install # 等价于手动写入以下配置 git config --global filter.lfs.clean git-lfs clean -- %f git config --global filter.lfs.smudge git-lfs smudge -- %f git config --global filter.lfs.process git-lfs filter-process git config --global filter.lfs.required true这里有个值得展开的细节filter.lfs.process和smudge是两套并行的机制新版本 Git 优先走process因为它可以一次批量处理多个文件比逐个调用smudge快得多。如果你的环境里还留着只有smudge的老配置建议直接重跑一次git lfs install让配置补齐。另外还有个参数经常被忽略git lfs install --skip-smudge。它的作用是全局默认不自动下载 LFS 文件只拉指针。这个模式特别适合以下场景你只改前端代码仓库里的美术资源对你是负担你经常要切分支不想每次 checkout 都触发大文件下载。我个人的做法是常用仓库用默认模式超大资源库单独用--local开启 skip-smudge。2.3 追踪规则和 .gitattributes 的正确写法让某个类型的文件走 LFS靠的是git lfs trackgit lfs track *.psd git lfs track *.zip git lfs track assets/**/*.bin git lfs track *.fbx执行后当前目录会生成或修改.gitattributes里面是一行行的匹配规则*.psd filterlfs difflfs mergelfs -text *.zip filterlfs difflfs mergelfs -text assets/**/*.bin filterlfs difflfs mergelfs -text这个文件必须 commit 上去否则等于白干。.gitattributes是团队共享的约定别人 clone 下来只有拿到这份规则Git 才知道哪些文件该走 LFS。我见过最典型的翻车就是本地track完直接提交了大文件本身却漏了.gitattributes结果同事拉下来全是几百兆的二进制对象在普通对象库里LFS 一点没生效。-text这个标记容易被忽略它的含义是不要做行尾转换。二进制文件如果被 Git 当成文本处理换行符文件就废了所以 LFS 规则里必须带上-textgit lfs track会自动加但如果你手动写.gitattributes就要自己注意。追踪规则匹配的是工作区里已存在文件的操作对已经提交的历史不起作用。如果你接手的老仓库里已经存了一堆大文件那就需要用git lfs migrate去改写历史这部分放到第 4 章讲。提示git lfs track不带参数直接执行会列出当前所有 LFS 追踪规则排查为什么这个文件没走 LFS时先敲一下这个命令。想临时取消追踪用git lfs untrack *.zip它只会改.gitattributes不会动已入库的文件。3. 拉取大文件的完整流程与关键参数3.1 首次 clone 时LFS 在背后做了哪些动作一次普通git clone实际上分成两个阶段。第一阶段是标准的 Git 对象下载这个阶段你下载的大文件其实是那堆 130 字节的指针速度很快。第二阶段由post-checkout钩子触发LFS 会扫描工作区所有指针文件把需要的内容整理成一个批次向 LFS 服务发起 HTTP 请求逐个下载这个过程会打印Downloading LFS objects: 45% (12/27), 320 MB | 1.2 MB/s这类进度。理解了这一点就能解释一个高频疑问为什么 clone 到 100% 了还在转圈因为 Git 对象传完了LFS 文件才刚开始下载进度条是两条独立的水位线。如果你只想先拿到仓库骨架、暂时不需要那些大文件可以先跳过 smudgeGIT_LFS_SKIP_SMUDGE1 git clone https://your-host/group/repo.git cd repo这样克隆下来大文件的位置会是那几行指针文本仓库体积可能只有几十 MB。等真正需要某个文件时再按需拉取特别适合我只改一个模块的场景。Windows PowerShell 里的写法略有不同用$env:GIT_LFS_SKIP_SMUDGE1设置环境变量或者干脆用git clone --no-checkout再配git lfs pull。3.2 fetch、checkout、pull 三个命令到底差在哪这三个命令长得很像但职责完全不同混淆了会浪费大量时间git lfs fetch只把 LFS 对象从远端下载到本地缓存目录.git/lfs/objects工作区不变你看到的还是指针文本。git lfs checkout只把工作区里的指针替换成已经躺在缓存里的真实文件不发起网络请求。git lfs pull等于fetchcheckout是一条命令搞定下载并落地的快捷方式。所以正确的按需拉取姿势是切到需要的分支后先git lfs pull如果只想拉其中一部分加过滤参数# 只拉 data 目录下的 bin 文件 git lfs pull --includedata/**/*.bin # 拉指定分支的全部 LFS 对象 git lfs pull --include* --exclude*.zip--include和--exclude支持标准的通配符也可以在一次命令里多次使用。这个能力在仓库里有几十个大文件但我只要其中一个的场景下非常实用能把下载量从几个 GB 压到几十 MB。还有一个容易被忽略的命令是git lfs fetch --all它会把所有分支的 LFS 对象都拉下来。这个命令不要随便敲除非你确实打算把整个仓库的 LFS 内容都同步到本地否则磁盘会被迅速填满。3.3 并发、超时和重试参数怎么调LFS 下载默认是并发进行的并发数由lfs.concurrenttransfers控制git config --global lfs.concurrenttransfers 8 git config --global lfs.activitytimeout 60 git config --global lfs.dialtimeout 30 git config --global lfs.tlstimeout 30 git config --global lfs.transfer.maxretries 5lfs.concurrenttransfers默认值是 8在带宽充足、服务端抗压能力强的环境可以调到 16 甚至 32。但我实测算下来并发数不是越高越好超过服务端或中间链路的承载能力后反而会出现大量超时和重试整体速度不升反降。我一般从 8 试起逐步上调到 16涨不动就停。lfs.activitytimeout管的是连接建立后多久没有数据往来就判超时如果你的网络出口对长连接有限制或者文件特别大导致单次传输很久可以适当调大到 120。lfs.transfer.maxretries设成 5 是为了应对偶发的网络抖动重试机制会自动补下失败的块不必手动重跑。想把 LFS 缓存挪到大磁盘上用git config --global lfs.storage /path/to/big-disk/lfs。默认缓存位置在.git/lfs跨仓库不共享。如果你机器上有多个大仓库把它们指向同一个 storage 反而省空间因为 LFS 是按 SHA-256 内容寻址的相同的文件天然去重。3.4 稀疏检出搭配跳过 smudge把拉取量压到最低如果你面对的是一个几百 GB 的 monorepo光是上面的手段可能还不够。这时候可以组合三件套git clone --filterblob:none --no-checkout https://your-host/group/monorepo.git cd monorepo git sparse-checkout init --cone git sparse-checkout set my-module docs git checkout main git lfs pull --includemy-module/**--filterblob:none让 Git 只拉提交树和目录结构不拉文件内容sparse-checkout让你只把关心的目录物化到工作区最后git lfs pull --include精确拉取这个目录下的 LFS 对象。三招叠起来一个原本要下几十 GB 的仓库可以在几分钟内只下几百 MB 就开始干活。要注意的是--filterblob:none属于部分克隆它要求服务端支持老版本的私有部署可能不认这个参数会直接报错。遇到这种情况就退回到--no-checkout加sparse-checkout的组合效果差一些但兼容性更好。4. 拉取失败和大文件卡住的排查实录4.1 常见报错速查表我在实际工作里收集到的 LFS 报错基本集中在下面这几类先放一张速查表后面再挑几个重点展开报错信息真实原因处理方式Smudge error: Object does not exist on the server文件从未成功推到 LFS或存储被清理找原始提交者重新git lfs push --allbatch response: This repository is over its data quotaLFS 存储配额或流量用尽清理历史大文件或申请扩容LFS: Client error: ... 401 / 403鉴权失败LFS 走 HTTP 需要独立凭据重新配置凭据助手确认账号权限Encountered N file(s) that should have been pointers, but werent大文件被直接提交进了普通对象库用git lfs migrate转换error: external filter git-lfs smudge failed客户端未安装 LFS 或版本过旧装/升级git-lfs进度长时间停在某个百分比并发过高导致链路拥塞或单个大文件超时降并发、调大 activitytimeout4.2 配额爆表最常见的拉不下来元凶This repository is over its data quota这条报错我遇到的次数最多。它的成因往往不是有人恶意塞了超大文件而是同一个大文件被反复修改、每个版本都完整上传了一份。一个 300MB 的设计稿改 20 版LFS 存储就吃掉 6GB免费或低档配额很快见底。处理思路分两步。第一步是止血用git lfs ls-files -s列出当前分支的 LFS 文件及大小找到占空间的大头跟对应负责人确认这些历史版本是否还有保留价值。第二步是清理历史用git lfs migrate import把历史中不符合追踪规则的大文件转成 LFS 指针如果它们还在普通对象库里或者用git lfs migrate export把 LFS 里不再需要的历史版本剥离出来。# 查看 LFS 实际占用的对象及大小 git lfs ls-files -s # 查看本地 LFS 缓存占用 git lfs env需要提醒的是migrate 会重写提交历史所有 commit hash 都会变。协作仓库执行前必须通知所有人并且清理完成后的第一次推送需要git push --force。这一步风险很高建议先在仓库的镜像或临时分支上演练确认所有步骤都跑通再动主干。4.3 缓存损坏和 incomplete 目录导致的卡死有时候不是网络问题而是本地缓存坏了。症状是git lfs pull反复卡在同一个文件、进度到 99% 就重来或者直接报unexpected EOF。这时候去看.git/lfs/incomplete目录通常能发现残留的半截临时文件。LFS 下载时先进incomplete校验通过后才移入objects如果进程被强杀或断电就会留下垃圾。清理办法很简单删掉整个 incomplete 目录再重拉rm -rf .git/lfs/incomplete git lfs pull如果问题依旧可以跑一次完整性检查git lfs fsck它会校验本地对象和指针的对应关系输出哪些文件缺失、哪些存在但没被任何指针引用。检查完可以用git lfs prune清理那些已经不在任何分支或历史中的 LFS 对象释放磁盘空间。prune默认会保留最近几个提交引用的对象具体保留策略由lfs.pruneoffsetdays控制默认是 3 天。注意git lfs prune是本地清理只影响你机器的缓存不会动远端存储。所以别指望它在配额爆表时救场那得靠 migrate 或者申请扩容。4.4 鉴权、SSH 与 HTTP 的边界问题这里有个几乎人人都会踩的坑你用 SSH 地址 clone 仓库但 LFS 文件走的依然是 HTTPS。很多人以为配置好了 SSH 密钥就万事大吉结果 git 元数据拉取畅通无阻LFS 下载全部 401。原因是 SSH 协议只负责传输 Git 对象和指针LFS 的批量 API 和对象下载都是独立的 HTTP 端点需要另外的凭据。解决办法取决于你的托管方式。走 HTTPS 的话确认凭据助手保存了正确的账号git config --global credential.helper在 Windows 上一般是managermacOS 上是osxkeychain。如果凭据过期可以先清掉再触发一次git config --global --unset credential.helper git config --global credential.helper store git lfs pull # 提示输入账号密码后会被记录下来如果公司部署的 LFS 服务地址和 Git 服务地址不一致可以用git config lfs.url显式指定 LFS 端点避免客户端猜错地址。另外要确认账号对 LFS 存储有读权限有些权限体系里代码只读和 LFS 读取是分开配置的这类问题只能找仓库管理员核对。4.5 历史里已经混入了大文件怎么办如果仓库在接入 LFS 之前就已经提交过大文件那么指针机制对历史无能为力。你可以用两种方式补救一是用git lfs migrate import把历史中匹配规则的文件全部转成 LFS 指针二是用通用的历史重写工具把这些文件从历史里彻底删掉。前者适合这个文件确实需要保留只是想换成 LFS 管理后者适合这个文件根本不该进仓库。# 把历史中所有 zip 和 psd 转成 LFS 管理 git lfs migrate import --include*.zip,*.psd --everything # 只处理某个分支 git lfs migrate import --include*.zip --include-refrefs/heads/main--everything会遍历所有分支和标签耗时可能很长仓库越大越慢。执行过程中 Git 会重写每一个受影响的提交最终输出的 hash 全变。跑完之后一定要git lfs ls-files验证文件确实变成了 LFS 对象再执行强制推送。关于推送这一步其实有些托管平台的默认分支开了保护git push --force会被拒绝需要先在后台临时关闭保护规则推完再打开。另外提一句强推之后其他同事的本地分支基本作废了正确的做法是让大家重新 clone而不是试图 pull 合并。这一条我在实际协作里强调过很多次总有人想省事直接 pull最后搞得本地历史一塌糊涂。5. 我踩过的坑和一份可直接抄的配置清单5.1 几个只有真被坑过才知道的细节第一个坑是漏提交.gitattributes。这个前面提过但值得再说一次因为我见过至少三次团队级的翻车。判断方法很直接clone 一个新仓库随手改一个应该走 LFS 的文件再看看git lfs ls-files有没有输出。没有输出就是规则没生效。第二个坑是图形化 Git 客户端不走 LFS。有些老版本的 GUI 工具在克隆时不会触发 smudge 过滤结果你拿到的大文件其实是一段指针文本用编辑器打开发现是三行莫名其妙的英文。这时候用命令行git lfs pull补一下就行但如果你用工具直接打开、编辑、保存了这个指针文本再提交就会把指针覆盖成真实的乱码问题会传染给所有人。第三个坑是把压缩包当仓库内容。zip 文件本质上已经是压缩数据Git 的 delta 压缩对它几乎无效所以每次改动都会产生一份完整的新副本。如果你确实需要管理打包产物请务必给它单独配 LFS 规则或者干脆用制品仓库来管理。我自己的习惯是凡是构建生成的产物一律进制品仓库绝不进 Git。第四个坑是改了 LFS 追踪规则却不通知团队。.gitattributes一旦改动所有人下次拉取时的行为都会变化。如果规则写错比如把*.json也纳入了 LFS那全组的配置文件都会变成指针排查起来相当费劲。所以改这个文件前先在群里说一声改完让大家重新拉一次。5.2 一份能直接抄的本地配置下面这套是我在开发机上稳定用了大半年的全局配置覆盖并发、超时、重试和缓存位置你可以按需改数值# 必做让 Git 认识 LFS filter git lfs install # 全局并发与超时 git config --global lfs.concurrenttransfers 16 git config --global lfs.activitytimeout 120 git config --global lfs.dialtimeout 30 git config --global lfs.tlstimeout 30 git config --global lfs.transfer.maxretries 5 # 大文件缓存统一放到大磁盘 git config --global lfs.storage /data/lfs-cache # 让 ls-files 默认显示体积 git config --global lfs.ls-files.size true # 默认跳过 smudge只对资源型仓库开普通仓库别开 # git config --global lfs.fetchexclude assets/large/**lfs.fetchexclude这个配置比环境变量更省事写进全局配置后每次 pull 都会自动排除掉指定路径不需要你每次手敲--exclude。我一般用它来屏蔽仓库里那些我百分之百用不到的超大资源目录。5.3 日常协作里的一些经验日常使用中最划算的一条经验是分支切得勤的仓库LFS 缓存一定要共享。默认情况下每个仓库各自维护.git/lfs目录同样的资源文件在三个克隆里存三份很容易把磁盘吃满。把lfs.storage指向同一个目录后相同 SHA 的文件天然复用实测能省一半以上空间。第二条经验是大文件删除也要走正常提交流程。有人在文件管理器里直接rm一个大文件然后提交结果历史里那个版本还在下次 clone 依然要下。正确的做法是git rm加提交让 Git 知道这个文件在这个版本被删了。至于历史里的版本那就只能靠前面说的 migrate 或 prune 处理了。第三条是给团队的建议在仓库根目录放一份简短的README写清楚哪些扩展名走 LFS、新同事第一次 clone 要注意什么、遇到Smudge error该找谁。这类说明看着琐碎但能省掉大量重复的答疑时间。我自己接手过的一个中台仓库就是这么做的新同事上手基本不会卡在大文件拉取这一步。