上传功能在测试环境总是正常的:开发用几 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 的能力边界,而当服务数量增长到几百个、每个都需要互相调用时,另一个问题浮现出来:是不是该把这些通用能力下沉到边车代理里,这正是下一篇服务网格主题要回答的。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。