在使用 Git LFS 跟踪大型二进制文件时,真正让人头疼的往往不是指针文件提交,而是推送阶段。执行 git push 后,客户端会先把 LFS 对象上传到远端,如果此时网络上行带宽被其他任务占满,或者服务器端 Nginx、代理层设置了较短的读写超时,就很容易看到 Uploading LFS objects 卡住,随后抛出 batch response、connection reset by peer 或 timeout 之类的错误。更麻烦的是,标准 Git LFS 传输协议并不会保存上传进度,一次失败后通常要重新发送整个文件,文件越大,重试成本越高。

要解决这类问题,不能只把超时时间调大,因为大文件上传失败常常是多个因素叠加的结果:客户端并发数过高、上行带宽不足、HTTP 传输没有触发断点续传、服务端请求体缓冲设置不合理、对象存储直传 URL 有效期过短等。下面从传输机制、客户端参数和服务端配合三个层面展开。
一、为什么 Git LFS 上传大文件会频繁失败
Git LFS 的核心思路是用指针文件代替大型二进制文件,实际对象存储在 LFS 服务器或对象存储中。推送时客户端先调用 batch API 获取上传地址,然后按照 transfer adapter 执行上传。默认的 basic transfer 是一个简单 HTTP PUT 请求,优势是兼容性好,但缺点也很明显:只支持单个完整请求。只要 TCP 连接断开、TLS 握手失败、代理超时或服务端返回 413 响应,客户端就只能根据重试次数重新发起整个 PUT。对一个 2GB 的文件来说,上传到 99% 再失败,下次仍然从 0 字节开始,时间成本会成倍增加。
带宽限制是另一个容易被忽略的因素。Git LFS 默认会并发传输多个对象,例如同时推送多个大文件时,客户端可能开启 3 个或更多的并发上传。并发传输虽然能提升吞吐,但在上行链路较窄或存在限速设备时,反而会引发拥塞、丢包和服务端限流。很多失败日志中出现的 connection reset by peer,并不是权限错误,而是路由器、防火墙或负载均衡器在带宽过载时主动断开连接。把并发数降下来,往往能显著提高大文件上传的成功率。
服务端配置也会造成上传中断。自建 LFS 服务通常运行在 Nginx 或 Apache 后面,如果没有关闭代理缓冲,服务端会先把整个请求体写入本地临时文件,再转发给后端。这样不仅增加磁盘 I/O,还容易在请求体超过 client_body_buffer_size 后产生额外延迟。与此同时,proxy_read_timeout 如果只有 60 秒,当上行速度较慢时,后端长时间没有收到完整请求体,连接也会被判定为超时。这些服务端细节需要和客户端参数一起调整。
二、客户端参数与重试机制:先降低失败率
在引入断点续传之前,最实用的做法是让 Git LFS 的默认传输尽量稳定。Git 和 Git LFS 提供了一组配置项,可以控制重试次数、退避时间、并发数量以及低速连接的处理方式。下面是一组适合大文件推送的基础配置:
# 增加 LFS 传输重试次数,默认通常较小 git config --global lfs.transfer.maxretries 5 # 单次失败后的最大退避秒数,避免过于频繁重试 git config --global lfs.transfer.maxbackoff 30 # 限制同时上传的 LFS 对象数量,降低上行带宽压力 git config --global lfs.concurrenttransfers 1 # 设置传输活动超时,网络较慢时可以适当调大 git config --global lfs.activitytimeout 300 # Git HTTP 低速判断:当 30 秒内平均速度低于 1KB/s 时中断连接 git config --global http.lowSpeedLimit 1024 git config --global http.lowSpeedTime 30
lfs.transfer.maxretries 用于控制单个对象传输失败后的重试次数。很多用户习惯设置成 10 甚至更高,但并不是越大越好,因为 basic transfer 每次重试都会重新上传整个对象,重试次数过多反而会把已经不稳的网络拖垮。一般设置 3 到 5 次,配合 lfs.transfer.maxbackoff 的指数退避,可以让客户端在每次失败后等待更长时间再试,给网络或服务端恢复留出缓冲。
lfs.concurrenttransfers 是最容易被低估的参数。将并发数设置为 1 后,上传速度不一定会下降,因为单个大文件的上传吞吐主要由上行带宽决定,而不是并发任务数。尤其是在通过 VPN、家庭宽带或共享出口推送时,单连接传输更容易保持稳定。http.lowSpeedLimit 和 http.lowSpeedTime 则用于识别长时间没有数据流动的连接,Git 会在触发阈值后主动断开,而不是无限期等待。对于上传场景,lowSpeedLimit 应该设置得小一些,比如 1024 字节每秒,避免正常的上传暂停被误判。
如果你管理的是自建 LFS 服务,仅调整客户端还不够。Nginx 反代层需要放开请求体大小、关闭代理缓冲,并加长读写超时。下面这段配置可以作为参考:
location /objects/ {
# 允许大对象上传,0 表示不限制
client_max_body_size 0;
# 关闭请求体缓冲,让数据流式转发到后端
proxy_request_buffering off;
# 调整代理层超时,避免低速上传被中断
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 60s;
}
其中 client_max_body_size 0 表示不限制请求体大小,也可以写成明确的上限,例如 2g。proxy_request_buffering off 很关键,如果不关闭,Nginx 会先把整个大文件写入本地临时目录,再转发给后端,既增加磁盘压力,也可能触发超时。将 proxy_read_timeout 和 proxy_send_timeout 设置为 600 秒,可以让单块上传在缓慢链路上有更长的存活时间。不过要注意,这些超时并非越长越好,过长会占用连接资源,实际可以结合业务文件大小和上行速度估算一个合理值。
如果这些参数仍然无法解决失败,可以观察 git lfs env 输出,确认当前生效的传输适配器、临时目录和并发配置:
git lfs env git config --global --list | grep lfs GIT_TRACE=1 GIT_CURL_VERBOSE=1 git push origin main
三、实现断点续传:标准协议的局限与替代方案
上述配置的目标是让完整文件更容易传上去,但它并没有改变 basic transfer 不能续传的本质。Git LFS 默认的 basic transfer 协议只定义了一个对象对应一个 HTTP PUT 请求,客户端和服务端之间不会交换分片信息。即使客户端本地有已上传部分的临时文件,服务端也没有机制告知从哪里继续。所以如果需要真正意义上的断点续传,必须更换传输适配器或使用支持分片上传的服务端实现。
Git LFS 从 3.0 开始允许通过 custom transfer adapter 接入其他传输协议。tus 协议就是比较适合大文件续传的一种选择,它把文件切分成多个 PATCH 请求,服务端记录每个分片的偏移量,客户端断线重连后可以从上次成功的位置继续。要使用 tus adapter,需要先安装对应的适配器程序,然后在 Git 配置中声明:
# 配置自定义传输适配器,使用 tus 协议进行 LFS 上传 git config --global lfs.customtransfer.tus.path /usr/local/bin/git-lfs-tus-adapter git config --global lfs.customtransfer.tus.args "upload" git config --global lfs.customtransfer.tus.concurrent false
配置完成后,Git LFS 会把上传动作交给这个适配器,由适配器负责分片、续传和校验。需要注意的是,服务端也必须支持相同的协议。目前一些自建 LFS 实现或对象存储网关提供了 tus 端点,而 GitHub、GitLab 的 LFS 服务是否原生支持 tus,需要查看其文档。若团队使用自建 LFS,可以直接部署 tusd 作为上传入口,再通过后端的元数据接口对接对象存储,这样既保留了 Git LFS 的指针管理,又获得了稳定的分片续传能力。
如果暂时不升级传输协议,还可以从架构上降低大文件失败影响:将超大文件拆成多个较小的 LFS 对象,而不是单个数 GB 文件。例如将模型权重、数据集或打包资源按目录拆分成 200MB 到 500MB 的压缩分卷,推送时每个对象独立上传,失败重传的范围更小。这虽然不是协议层面的断点续传,但在工程实践中非常有效。对于必须保持单文件完整性的场景,则建议尽早切换到 tus 或自定义分片适配器。