Git LFS 拉取大文件实战:配置、排查与历史清理
2026/9/17 20:40:19 网站建设 项目流程

前阵子组里一个同事把 1.2G 的设计源文件直接 commit 进了主干,第二天早上全组拉代码,进度条齐刷刷停在Receiving objects: 68%,接着就是二十分钟的沉默。这事之后我把Git LFS 拉取大文件这套流程从头到尾重新捋了一遍:从安装配置、追踪规则、按需拉取,到配额爆表、指针错乱、历史清理,全部自己踩了一遍。这篇东西就是那几天的实际操作记录,包含我最终稳定使用的参数配置、失败排查路径,以及几个只有真被坑过才知道的细节。它适合三类人看:第一次给仓库接入 LFS 的开发者、正在被大文件拖慢拉取速度的人、以及接手了一个"历史里全是大文件"的老仓库、准备清理但不敢下手的维护者。不需要你对 Git 底层有多深理解,但至少要能跑通git clonecommit

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 LFS(Large 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)看情况若能通过构建生成,不如加进.gitignore
100KB 以下的小图标、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-lfs;Ubuntu/Debian 用sudo apt install git-lfs,CentOS/RHEL 系可以先curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.rpm.sh | sudo bashsudo yum install git-lfs

注意:内网环境经常出现"服务端支持 LFS、客户端却报 404"的情况,先确认客户端版本,再确认服务端是否开启了 LFS 支持。有些私有部署版本默认关闭了这个功能,需要在后台手动打开。

如果平台包管理器的版本太老,比如装出来是 2.x,那建议直接去 Release 页面下二进制包手动放到PATH里。老版本在处理大批量并发下载时的稳定性明显不如 3.x。

2.2 git lfs install 到底改了哪些配置

装好之后有一步千万别漏:git lfs install。这条命令做的事是在 Git 全局配置里写入四个 filter 定义,让 Git 在addcheckout时知道该调用 LFS:

git 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.processsmudge是两套并行的机制,新版本 Git 优先走process,因为它可以一次批量处理多个文件,比逐个调用smudge快得多。如果你的环境里还留着只有smudge的老配置,建议直接重跑一次git lfs install让配置补齐。

另外还有个参数经常被忽略:git lfs install --skip-smudge。它的作用是全局默认不自动下载 LFS 文件,只拉指针。这个模式特别适合以下场景:你只改前端代码,仓库里的美术资源对你是负担;你经常要切分支,不想每次 checkout 都触发大文件下载。我个人的做法是常用仓库用默认模式,超大资源库单独用--local开启 skip-smudge。

2.3 追踪规则和 .gitattributes 的正确写法

让某个类型的文件走 LFS,靠的是git lfs track

git lfs track "*.psd" git lfs track "*.zip" git lfs track "assets/**/*.bin" git lfs track "*.fbx"

执行后当前目录会生成(或修改).gitattributes,里面是一行行的匹配规则:

*.psd filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text assets/**/*.bin filter=lfs diff=lfs merge=lfs -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 文件才刚开始下载,进度条是两条独立的水位线。

如果你只想先拿到仓库骨架、暂时不需要那些大文件,可以先跳过 smudge:

GIT_LFS_SKIP_SMUDGE=1 git clone https://your-host/group/repo.git cd repo

这样克隆下来,大文件的位置会是那几行指针文本,仓库体积可能只有几十 MB。等真正需要某个文件时再按需拉取,特别适合"我只改一个模块"的场景。Windows PowerShell 里的写法略有不同,用$env:GIT_LFS_SKIP_SMUDGE=1设置环境变量,或者干脆用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:等于fetch+checkout,是"一条命令搞定下载并落地"的快捷方式。

所以正确的按需拉取姿势是:切到需要的分支后先git lfs pull,如果只想拉其中一部分,加过滤参数:

# 只拉 data 目录下的 bin 文件 git lfs pull --include="data/**/*.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 5

lfs.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 --filter=blob: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 --include="my-module/**"

--filter=blob:none让 Git 只拉提交树和目录结构,不拉文件内容;sparse-checkout让你只把关心的目录物化到工作区;最后git lfs pull --include精确拉取这个目录下的 LFS 对象。三招叠起来,一个原本要下几十 GB 的仓库,可以在几分钟内只下几百 MB 就开始干活。

要注意的是--filter=blob:none属于部分克隆,它要求服务端支持,老版本的私有部署可能不认这个参数,会直接报错。遇到这种情况就退回到--no-checkoutsparse-checkout的组合,效果差一些但兼容性更好。

4. 拉取失败和大文件卡住的排查实录

4.1 常见报错速查表

我在实际工作里收集到的 LFS 报错基本集中在下面这几类,先放一张速查表,后面再挑几个重点展开:

报错信息真实原因处理方式
Smudge error: Object does not exist on the server文件从未成功推到 LFS,或存储被清理找原始提交者重新git lfs push --all
batch 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 weren't大文件被直接提交进了普通对象库git lfs migrate转换
error: external filter 'git-lfs smudge' failed客户端未安装 LFS 或版本过旧装/升级git-lfs
进度长时间停在某个百分比并发过高导致链路拥塞,或单个大文件超时降并发、调大 activitytimeout

4.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 上一般是manager,macOS 上是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-ref=refs/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该找谁。这类说明看着琐碎,但能省掉大量重复的答疑时间。我自己接手过的一个中台仓库就是这么做的,新同事上手基本不会卡在大文件拉取这一步。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询