直接用c.SaveUploadedFile或c.FormFile处理几百MB以上文件会触发OOM,因Gin默认调用ParseMultipartForm将整个文件载入内存或临时磁盘,未限大小且高并发时runtime: out of memory导致进程被kill。

直接用 c.SaveUploadedFile 或 c.FormFile 处理几百 MB 以上文件,Go 进程大概率被系统 kill——因为 Gin 默认把整个文件读进内存再保存。
为什么大文件上传会触发 OOM
根本原因不是 Gin 本身,而是它默认调用 http.Request.ParseMultipartForm 时,会把所有 multipart 数据(包括大文件)先加载到内存或临时磁盘文件;若未显式限制大小,且并发一高,runtime: out of memory 就来了。
-
ParseMultipartForm内部使用maxMemory参数控制内存阈值,默认是 32MB;超过就写入临时磁盘,但磁盘 IO + 文件句柄 + goroutine 累积仍可能压垮服务 - 前端没做分片、后端没设限、Nginx 还开着
proxy_buffering on,三者叠加会让单次请求吃掉数百 MB 内存 -
c.MultipartForm()返回的*multipart.Form是完整解析结果,哪怕你只取其中一个小字段,整个表单(含大文件)也已载入
必须在路由 handler 开头设 MaxBytesReader
这是最简单有效的第一道防线:拒绝超限请求体,不让它进 Gin 的解析流程。
- 错误做法:在中间件里先调
c.ShouldBindJSON()或c.Request.ParseForm(),再设MaxBytesReader——此时 body 已被读空,设置无效 - 正确位置:每个需要接收文件的 handler 第一行,例如:
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10<code>1024</code><code>1024</code>)
- 注意第二个参数是
c.Writer(http.ResponseWriter),不是*gin.Context;传错会导致 413 错误无法写出,客户端卡住 - 若需差异化限制(如
/upload允 100MB、/api/login仅 2MB),就按接口单独设,别全局统一
真要收大文件?绕过 MultipartForm 自己流式解析
当文件超 500MB 或并发 > 50,连 TemporaryUploadedFile 级别的磁盘中转都扛不住,就得跳过 Gin 封装,自己用 mime/multipart 边读边处理。
- 先禁用 Gin 自动解析:
c.Request.ParseMultipartForm(0)或干脆不调任何 Parse 方法 - 用
multipart.NewReader(c.Request.Body, boundary)逐 part 读取,遇到文件 part 就用io.Copy直接写入磁盘或对象存储,不缓存全文本 - 务必提前设置
c.Header("Content-Type", "text/plain"),否则c.Writer可能 panic “header written” - 记得手动检查
Content-Length和Content-Range(如支持断点续传),别依赖框架自动补全
Nginx 层必须关掉 proxy_request_buffering
就算 Go 层写得再流式,Nginx 默认会把整个请求体缓存完才转发——这对分片上传或大文件是致命的。
- 在对应
location块里加:proxy_buffering off;<br>proxy_request_buffering off;<br>proxy_http_version 1.1;<br>proxy_set_header Connection '';
-
proxy_request_buffering off是关键项(要求 Nginx ≥ 1.13.10),它让 Nginx 不等待 body 结束就流式透传给后端 - 漏掉这步,你会看到上传卡在 99%、超时、或 Nginx 日志报
upstream prematurely closed connection
真正难的不是“怎么写代码”,而是判断该在哪一层拦截:Nginx 控制入口带宽,Go 控制单请求内存上限,业务逻辑决定是否流式落盘。三者少一个,OOM 风险就翻倍。

















