Nginx 大文件上传与请求体处理:缓冲、临时文件与断点续传

讲解 Nginx 处理大请求体的完整机制,涵盖 client_max_body_size 的分层设置、client_body_buffer_size 与临时文件落盘、proxy_request_buffering 的取舍、超时联动、断点续传与直传对象存储方案,以及 413 错误的排查方法。

上传功能在测试环境总是正常的:开发用几 MB 的图片验证流程,一切顺畅。到了生产环境,用户上传几百 MB 的视频或数据集时,才会遇到 413、连接超时、临时目录写满、上游收到不完整请求体等一系列问题。这些现象的根源都指向同一件事——Nginx 默认按「小请求」假设设计,请求体超过缓冲区就落盘,超过限制就拒绝,超过超时就断开。本文沿着请求体的流动路径,逐段说明每个参数的语义与相互约束。

一句话总结: 请求体处理的核心是三组参数的协同——大小限制决定「收不收」、缓冲策略决定「怎么收」、超时决定「收多久」,任何一组与其他组不匹配都会表现为上传失败。

1. 请求体处理的基本模型

一句话总结: Nginx 读取请求体时先写入内存缓冲区,超过阈值后转为临时文件,全部读完后才或边读边转发给上游。

客户端 --[请求体]--> Nginx --[转发]--> 上游应用
                      |
                      +-- 1. 边收边写内存缓冲 client_body_buffer_size
                      +-- 2. 缓冲满 -> 落盘 client_body_temp_path
                      +-- 3. 收完(或边收边发)后转发上游

理解这条路径后,三个常见现象就能解释清楚:上传大文件时磁盘 IO 飙升是因为触发了落盘;上传中途失败但上游没有收到任何数据,是因为 proxy_request_buffering on 下 Nginx 要先收完整个请求体;上传小文件很快但大文件慢,是因为内存缓冲与磁盘写入的速度差异。

http {
    # 内存缓冲区大小:超出即落盘
    client_body_buffer_size 256k;

    # 临时文件目录(可配置多级哈希子目录)
    client_body_temp_path /var/cache/nginx/body 1 2;

    # 请求体大小上限
    client_max_body_size 100m;

    # 读取请求体的超时(针对单次读操作)
    client_body_timeout 60s;
}

client_body_temp_path 后的 1 2 表示建立两级子目录,每级用 1 位与 2 位十六进制散列。目录散列的意义在于避免单个目录下文件过多导致的查找性能下降,大流量上传场景务必配置。

2. client_max_body_size 与拒绝策略

一句话总结: 限制值应在 http 层给默认值、在具体上传 location 放大,超限时 Nginx 直接返回 413 且不读取剩余请求体。

2.1 限制值的确定与分层

一句话总结: 默认 1m 对任何真实业务都偏小,但也不应简单放大到极大值,而应按接口分档设置。

http {
    # 默认档:普通接口
    client_max_body_size 1m;

    server {
        listen 80;

        # 表单与 JSON 接口
        location /api/ {
            client_max_body_size 10m;
            proxy_pass http://api_backend;
        }

        # 图片与文档上传
        location /upload/image/ {
            client_max_body_size 20m;
            proxy_pass http://upload_backend;
        }

        # 视频与大文件上传
        location /upload/video/ {
            client_max_body_size 2048m;
            client_body_timeout 300s;
            proxy_pass http://upload_backend;
        }
    }
}

分层设置的意义不只是「放开限制」,更是安全边界:把所有 location 的上限都设为 2G,等于允许任何匿名请求在 Nginx 上写 2G 的临时文件,极易被用于磁盘打满攻击。正确做法是只对确需大文件的路由放大,并对这些路由额外加鉴权与限速。

2.2 413 错误的正确返回

一句话总结: Nginx 在读取请求体前就根据 Content-Length 判断超限,命中即返回 413 并可能直接关闭连接,因此客户端会看到连接重置而非完整响应。

server {
    listen 80;

    # 自定义 413 响应,让前端能给出友好提示
    error_page 413 = @too_large;

    location @too_large {
        default_type application/json;
        return 413 '{"code":413,"msg":"文件超过大小限制"}';
    }

    location /upload/ {
        client_max_body_size 100m;
        proxy_pass http://upload_backend;
    }
}

需要特别注意的是,客户端在上传过程中收到 413 时,很多 HTTP 库会表现为「连接被重置」而不是解析到 413 状态码——原因是 Nginx 在拒绝时往往不再读取剩余请求体,直接关闭连接,客户端仍在写数据就会收到 RST。前端应把这类错误统一映射为「文件过大」,并在上传前用 Content-Length 做一次本地预校验,减少无效上传。

3. 缓冲与临时文件

一句话总结: proxy_request_buffering on 让 Nginx 收完整个请求体再转发(对上游友好),off 则边收边转发(省磁盘、适合超大文件),两者在磁盘占用与上游连接时长上取舍不同。

3.1 proxy_request_buffering 的取舍

一句话总结: 默认开启,适合小文件与需要重试的场景;大文件上传应关闭,避免磁盘双写与延迟累积。

location /upload/video/ {
    client_max_body_size 2048m;

    # 关闭请求体缓冲:边收边转发,不落盘
    proxy_request_buffering off;

    # 关闭后必须放大发送超时,否则慢速客户端会被切断
    proxy_send_timeout 300s;
    proxy_read_timeout 300s;

    proxy_pass http://upload_backend;
}

两者的差异可以对照如下:

proxy_request_buffering on(默认)
  优点:上游只接收完整请求体,可安全重试;上游连接占用时间短
  缺点:大文件会在 Nginx 落盘一次,再读出来转发,磁盘双倍 IO
  适用:小文件、需要失败重试的接口

proxy_request_buffering off
  优点:无磁盘落盘,内存占用低,首字节到达上游更快
  缺点:上游必须能处理慢速请求体;连接占用时间长;无法重试
  适用:大文件、流式上传、上传即转发的管道

关闭缓冲后,client_max_body_size 的检查方式会从「按 Content-Length 预判」变为「边读边计数」,超限时连接已经建立并转发了部分数据,上游可能收到半截请求体,需要应用层能识别并清理。

3.2 临时文件落盘与磁盘规划

一句话总结: 临时目录应放在容量充足且与系统盘分离的位置,并配置清理策略,否则一次大并发上传就能打满磁盘。

http {
    # 独立分区,避免打满根分区影响系统
    client_body_temp_path /data/nginx/body 1 2;

    # 与后端协商的超时与大小上限保持一致
    client_body_timeout 120s;
}
# 监控临时目录占用,超过阈值告警
du -sh /data/nginx/body
find /data/nginx/body -type f -mmin +60 -delete   # 清理一小时前的残留

# 挂载时预留 5% 给 root,防止完全写满
# /etc/fstab 中为数据盘添加保留块设置
tune2fs -m 5 /dev/vdb1

残留临时文件通常来自异常中断的连接:Nginx 在正常完成或客户端断开时会清理,但进程被强杀(OOM、kill -9)时可能遗留。定期清理任务应作为兜底,同时用磁盘使用率告警覆盖。若上传量很大,还可以把临时目录挂载为 tmpfs,代价是占用内存。

4. 超时联动

一句话总结: 上传链路上有四个超时参数互相制约,任何一个过小都会在慢速网络下切断上传。

client_header_timeout   读取请求头超时,上传场景通常不变
client_body_timeout     两次读请求体之间的最大间隔(不是总时长)
proxy_send_timeout      两次向上游写数据之间的最大间隔
proxy_read_timeout      两次从上游读数据之间的最大间隔
location /upload/video/ {
    client_max_body_size 2048m;

    # 慢速移动网络的单次读间隔可能很长,需放大
    client_body_timeout 180s;

    proxy_request_buffering off;
    proxy_send_timeout 180s;
    proxy_read_timeout 180s;

    proxy_pass http://upload_backend;
}

关键认知与长连接代理一致:这些超时都是间隔超时而非总时长超时。一个 2G 文件在 1MB/s 的网络上需要约 34 分钟,只要数据持续流动就不会触发超时;反之,客户端卡住 60 秒不动,即使只上传了 1MB 也会被切断。此外还要注意上游应用自身的请求体读取超时(如 Node.js 的 server.requestTimeout、Spring 的 spring.mvc.async.request-timeout),Nginx 侧放大后如果应用侧仍是默认值,故障会从 Nginx 转移到应用,表现是 502 而非 408。

5. 断点续传与大文件

一句话总结: 单次 HTTP 请求承载超大文件风险高,应改用分片上传或客户端直传对象存储,让 Nginx 只做鉴权与签名而不搬运数据。

5.1 Range 与分片上传

一句话总结: Range 请求适合下载与已存在资源的续传,上传断点续传一般由应用层分片协议实现。

location /upload/chunk/ {
    client_max_body_size 16m;      # 单片大小,可精确控制
    proxy_request_buffering off;
    proxy_pass http://upload_backend;

    # 关闭对分片请求的缓存,避免复用
    proxy_cache off;
}

分片上传把「一个大请求」拆成「多个小请求」,带来三个好处:单请求体积小因此超时风险低;单片失败只需重传该片;可以并发上传提高带宽利用率。代价是需要在应用层维护分片状态与合并逻辑,且必须处理分片乱序到达与超时清理。

5.2 直传对象存储

一句话总结: 让客户端向对象存储直传、Nginx 只负责签发临时凭证与回调校验,是最省资源的架构。

location /api/upload/sign {
    # 应用签发带过期时间的直传凭证,Nginx 仅转发
    proxy_pass http://api_backend;
    proxy_set_header Host $host;
}

location /api/upload/callback {
    # 对象存储上传完成后回调,应用校验并落库
    client_max_body_size 1m;
    proxy_pass http://api_backend;
}
架构对比:
  经 Nginx 中转  客户端 -> Nginx -> 应用 -> 对象存储
                 带宽占用 Nginx,临时文件占用 Nginx 磁盘

  客户端直传     客户端 -> 对象存储(Nginx 只签发凭证)
                 带宽与磁盘都不经 Nginx,仅需处理凭证与回调

直传架构下,Nginx 的 client_max_body_size 只需保持很小的默认值,上传相关的超时与缓冲问题一次性消失。代价是凭证签发接口成为关键路径,需要做好限流与防重放。

6. 排错与调优

一句话总结: 上传类故障按「413 看限制、卡住看超时、磁盘满看临时目录、上游收不到看缓冲策略」四条线定位。

# 确认实际生效的请求体限制(可能被上层覆盖)
nginx -T | grep -n 'client_max_body_size\|client_body_timeout'

# 观察临时文件是否在增长(上传进行中)
watch -n1 'ls -l /data/nginx/body | tail -5; du -sh /data/nginx/body'

# 检查磁盘与 inode(小文件过多会耗尽 inode)
df -h /data/nginx/body
df -i /data/nginx/body

# 观察上传过程中的连接状态与错误日志
tail -f /var/log/nginx/error.log | grep -i 'client intended to send too large body'
tail -f /var/log/nginx/error.log | grep -i 'timed out reading client request body'

错误日志中的关键信息有两条:client intended to send too large body 对应 413,说明 client_max_body_size 不够;client body temp file write error 或 no space left on device 对应磁盘问题。另外可以开启 error_log ... info 观察请求体的落盘行为,确认是否真的走了临时文件。

# 在响应头暴露限制值,便于前端提前校验
add_header X-Max-Upload-Size "104857600" always;

把限制值通过响应头下发给前端,可以让前端在用户选文件时立即给出提示,而不是等上传到一半才失败,这是投入产出比很高的一个小改动。

7. 安全边界

一句话总结: 放宽请求体限制等于放宽了资源消耗的入口,必须同步加上鉴权、限速与内容校验。

# 上传接口的限速与并发限制
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=upconn:10m;

server {
    location /upload/ {
        client_max_body_size 2048m;

        # 限速与并发限制
        limit_req zone=upload burst=2 nodelay;
        limit_conn upconn 3;

        # 上传接口必须鉴权
        auth_request /internal/authz;
        # 只允许特定方法与内容类型
        limit_except POST { deny all; }

        proxy_request_buffering off;
        proxy_pass http://upload_backend;
    }

    location = /internal/authz {
        internal;
        proxy_pass http://auth_backend/verify;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
    }
}

需要额外注意的是「先鉴权再收体」的顺序:Nginx 在读取请求体之前就会执行 access 阶段的鉴权(auth_request),因此未通过鉴权的请求不会消耗磁盘写入。反过来,如果鉴权写在应用层,Nginx 会先把整个请求体收完(或落盘)才转发,攻击者可以用匿名请求把磁盘写满。

8. 总结

环节要点
处理模型内存缓冲超过阈值落盘,收完或边收边转发给上游
大小限制http 层设默认档,上传 location 分档放大,避免全局放开
413 处理按 Content-Length 预判即拒绝,客户端常见表现为连接重置
请求体缓冲大文件关闭 proxy_request_buffering,小文件保留以便重试
临时文件独立分区加目录散列,配置定期清理与磁盘容量告警
超时联动四个超时都是间隔超时,需与上游应用侧设置同步放大
断点续传分片上传或客户端直传对象存储,Nginx 只做鉴权与签名
安全边界鉴权与限速必须在 access 阶段完成,避免匿名请求写磁盘

大文件上传的调优本质是「把资源消耗控制在预期范围内」:限制决定上限,缓冲决定消耗位置(内存、磁盘还是上游),超时决定异常时的回收速度。真正成熟的方案往往不是把 Nginx 的参数调到极致,而是改变架构——用分片或直传让大文件根本不经过 Nginx。至此我们讨论的都是单机 Nginx 的能力边界,而当服务数量增长到几百个、每个都需要互相调用时,另一个问题浮现出来:是不是该把这些通用能力下沉到边车代理里,这正是下一篇服务网格主题要回答的。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「nginx」更多文章

  1. Nginx 在服务网格中的角色:边车代理、mTLS 与 Envoy 取舍
  2. Nginx 证书自动化与 ACME:certbot、DNS-01 通配符与自动续期
  3. Nginx 配置测试与 CI 流水线:从 nginx -t 到灰度校验