前言
先纠正这个标题里的前提:proxy_send_timeout并不是"上传超时",把它调大通常也解决不了大文件上传失败。官方文档对它的定义是"Nginx 向被代理服务器(上游)发送请求时的超时,且只在两次相邻写操作之间计时,不是整个请求的传输时长"。方向是 Nginx 到后端,不是浏览器到 Nginx。
更关键的是默认行为:proxy_request_buffering的默认值是on,意思是Nginx 会先把客户端的请求体完整收下来(超过内存缓冲的部分落到临时文件),收完之后才向后端发起请求、把 body 转发过去。所以在默认配置下,浏览器上传大文件的过程根本没有触碰到proxy_send_timeout——真正决定成败的是客户端到 Nginx 这一段的那几个参数。
这就解释了一类很常见的现象:上传大文件失败,把proxy_send_timeout从 60s 调到 600s,一点用都没有;而把client_max_body_size从默认的1m改大,问题立刻消失。
proxy_send_timeout真正开始起作用,是在你把proxy_request_buffering off打开、让 Nginx 边收边转发之后——这时候 Nginx 向上游的写入节奏完全由客户端上传速度决定,proxy_send_timeout才成为"上传速度"的实际约束。本文把这条完整链路拆开讲清楚:哪一段归哪个指令管、怎么定位日志、以及两种工作模式下分别该怎么配。示例基于 nginx 1.24 / 1.22。
一、先分清四个超时各自管哪一段
| 指令 | 默认值 | 计量方式 | 管的是 |
|---|---|---|---|
client_header_timeout | 60s | 读请求头整体 | 客户端发请求头太慢 |
client_body_timeout | 60s | 两次相邻读操作之间 | 客户端上传 body 时的停顿 |
send_timeout | 60s | 两次相邻写操作之间 | Nginx 把响应写回客户端时的停顿 |
proxy_connect_timeout | 60s | 建连 | Nginx 连不上上游 |
proxy_send_timeout | 60s | 两次相邻写操作之间 | Nginx 把请求发给上游时的停顿 |
proxy_read_timeout | 60s | 两次相邻读操作之间 | 上游迟迟不返回 |
keepalive_timeout | 75s | 空闲 | 长连接空闲回收 |
这张表里最重要的是"计量方式"这一列。这几个超时的语义都是相邻两次 I/O 之间的间隔,而不是"整个请求从开始到结束最长多久"。所以一个逻辑上"传了 30 分钟"的大文件,只要数据一直在流动(每两次写之间没有超过阈值),就不会触发超时;反过来,一个只传了 3 秒的连接,如果中间卡了 61 秒没动静,照样会被掐掉。
二、大文件上传的完整链路:每个阶段谁在管
按数据流动的顺序走一遍,把每个阶段的控制点标出来:
| 阶段 | 控制指令 | 默认值 | 典型症状 |
|---|---|---|---|
| 客户端发请求头 | client_header_timeout | 60s | 少见的头部读取超时 |
| 请求体超过上限 | client_max_body_size | 1m | 413 Request Entity Too Large |
| 请求体在内存里还是落盘 | client_body_buffer_size | `8k\ | 16k` |
| 临时文件放哪 | client_body_temp_path | 默认在编译前缀下的client_body_temp | 目录满 → 500 |
| 客户端上传时的停顿 | client_body_timeout | 60s | while reading client request body |
| Nginx 向上游发 body | proxy_send_timeout | 60s | 只在proxy_request_buffering off时才是主要约束 |
| 上游处理后返回 | proxy_read_timeout | 60s | while reading response header from upstream |
| Nginx 把响应写回客户端 | send_timeout | 60s | 客户端下载响应太慢被断开 |
第一件该做的事就是把client_max_body_size调到你需要的量级。默认值1m意味着任何超过 1MB 的上传都会在请求头解析完后立刻被拒,客户端拿到 413,连数据都没开始传:
# 允许 2GB 的请求体;设为 0 表示不检查(即不限制) client_max_body_size 2g;这条指令可以放在http、server、location里,按站点或按路径精细控制比全局放开更合适。
三、两种工作模式:缓冲与流式
模式 A:默认的缓冲模式
Nginx 先把整个请求体收完(内存缓冲不够就落临时文件),再转发给上游。它的好处是上游不必处理慢速上传、请求可以被重试和均衡;代价是磁盘要能承下所有并发上传的体积之和,且上游要等到全部收完才开始处理。
location /upload/ { proxy_pass http://127.0.0.1:8080; client_max_body_size 2g; # 默认 1m,必须调 client_body_timeout 300s; # 客户端慢速上传的停顿容忍 client_body_buffer_size 1m; # 超过此值写入临时文件 proxy_send_timeout 300s; # 收完之后发给上游时的停顿容忍 proxy_read_timeout 300s; # 上游处理大文件可能需要更久 # 临时目录:不写则使用编译时的默认值。写的话目录必须存在且属主正确 client_body_temp_path /var/lib/nginx/body 1 2; }client_body_temp_path后面可以跟最多三层子目录参数(如上面的1 2),作用是把大量临时文件分散到多级子目录里,避免单目录海量文件导致性能下降。这个目录必须已经存在,并且属主是该 worker 进程运行的用户:RHEL 系通常是nginx,Debian 系是www-data。目录不存在时 Nginx 不会自动创建,上传会以 500 失败。
模式 B:关闭请求缓冲,边收边转发
location /upload/ { proxy_pass http://127.0.0.1:8080; proxy_request_buffering off; # 1.7.11 起可用,默认是 on proxy_http_version 1.1; # 需要流式/分块传输时要显式指定 client_max_body_size 2g; client_body_timeout 300s; proxy_send_timeout 300s; # 这个模式下它才真正管住上传速度 proxy_read_timeout 300s; }这个模式才有本文标题所说的意义:Nginx 一边从客户端读,一边往上游写,客户端的上传速度直接决定了写入间隔。上传慢的客户端会让proxy_send_timeout变得敏感,这时把它调大是合理的。
但要注意三个真实代价:
- 上游必须能处理流式请求。Nginx 可能无法预先知道请求体长度,会以分块传输(chunked)方式发给上游,因此通常需要
proxy_http_version 1.1,并且上游得支持分块编码。 - 请求不能再被重试或转投另一台上游。数据已经开始发出去,中途失败就没法悄悄重发。用
proxy_next_upstream的常规策略在这里帮不上忙。 - 上游会提前看到"慢客户端"。应用可能因为读不到完整数据而超时,这是把缓冲挪走之后的必然代价,需要在应用侧相应调整。
选择标准很直白:希望降低 Nginx 的磁盘占用和首字节延迟,用模式 B;希望上游逻辑简单、能重试、磁盘足够,用模式 A。
四、定位:从日志判断卡在哪一段
上传失败时,error.log里的一句话通常就能定位到具体指令。下面是几类常见记录的对照(不同 Nginx 版本的措辞略有差异,以你机器上的实际输出为准):
grep -E 'too large body|timed out|no space|prematurely' /var/log/nginx/error.log | tail -30| error.log 片段 | 对应指令 |
|---|---|
client intended to send too large body: 3145728 bytes | client_max_body_size |
client timed out (110: Connection timed out) while reading client request body | client_body_timeout |
upstream timed out (110: Connection timed out) while sending request to upstream | proxy_send_timeout |
upstream timed out (110: Connection timed out) while reading response header from upstream | proxy_read_timeout |
upstream prematurely closed connection while sending request to upstream | 上游自己断了(可能是它自己的限制) |
(28: No space left on device) while reading client request body | 临时目录所在分区写满 |
顺手确认最终生效的值,避免改了没生效:
nginx -T 2>/dev/null | grep -n -E 'client_max_body_size|client_body_timeout|client_body_temp_path|proxy_(send|read)_timeout|proxy_request_buffering'再看临时目录的余量,以及配置块之外的限制也别忘了:
# 找到实际使用的临时目录并看剩余空间 df -h /var/lib/nginx # SELinux enforcing 时目录标签不对也会失败 sudo ls -Zd /var/lib/nginx/body sudo restorecon -Rv /var/lib/nginx/body五、动手复现和验证
用一个大文件走一遍完整流程,同时观察各阶段的耗时。
# 1) 造一个 200MB 的测试文件(GNU coreutils 的 dd 支持 status=progress) dd if=/dev/zero of=/tmp/blob.bin bs=1M count=200 status=progress # 2) 用表单方式上传,并把各阶段时间与上传速率打出来 curl -s -o /dev/null \ -w 'connect=%{time_connect} starttransfer=%{time_starttransfer} total=%{time_total} speed_upload=%{speed_upload}B/s http=%{http_code}\n' \ -F 'file=@/tmp/blob.bin' https://upload.example.com/upload/%{speed_upload}是 curl 报的实际上传速率,%{http_code}能直接告诉你是不是 413。把这两组数字在调整参数前后各测一次,收益是你自己测出来的,不需要参考任何人的经验值。
想验证proxy_send_timeout到底怎么触发,可以在测试环境里扮演一个"只收不答、收得极慢"的上游:
# OpenBSD 版 netcat 的写法 nc -l 8080 > /dev/null # 部分传统版 netcat 需要写成 # nc -l -p 8080 > /dev/null让proxy_pass指向这个端口,再上传一个大文件,就能在error.log里看到while sending request to upstream的字样——用它来确认你的参数确实作用在这一段上,比读文档印象深得多。
调试阶段还有一个很有用的指令,它把请求体无条件写进文件:
client_body_in_file_only on; # 取值 on | clean | off,默认 off;仅用于排障用它确认"请求体有没有完整到达 Nginx",可以把"客户端没传完"和"上游没收到"两种情况区分开。用完记得改回off。
六、调参的原则:成组调,按最慢的合法用户定
- 这几个超时是成组生效的,只调一个往往没意义。至少要同时考虑
client_body_timeout、proxy_send_timeout、proxy_read_timeout、send_timeout。 - 取值依据应该是"最慢的合法用户":用第五节的方法测出真实上传耗时,再留 2 到 3 倍余量,而不是随手写个
3600s。 - 超时不是限速。想限速用
limit_rate(对响应)或limit_conn/应用层限速。把client_body_timeout设小来"限速"只会让正常用户传大文件失败。 - 调大超时等于允许连接占住 worker 更久,连接数会上升,记得同步检查
worker_connections与worker_rlimit_nofile。 - 临时目录所在分区的容量要按"并发上传数 × 单文件大小"估算。这条最容易被忽略,而它一旦满了,症状是 500 而不是超时,容易排查到别的方向去。
常见坑点
❌ 上传大文件失败,第一反应是把proxy_send_timeout调到 600s。 ✅ 默认proxy_request_buffering on时,这个超时管的是 Nginx 收完 body 之后发往上游的过程。先看是不是 413,也就是client_max_body_size(默认仅1m)。
❌ 把proxy_send_timeout理解成"整个请求最长可以传多久"。 ✅ 它的语义是相邻两次写操作之间的最大间隔,且只作用于 Nginx 到上游这一段。整个请求传得久没关系,中间长时间没有数据流动才会被断开。
❌ 只调client_body_timeout,忘了client_max_body_size。 ✅ 前者管"停顿"(默认 60s),后者管"总体积"(默认 1m)。体积超限是立刻返回 413,跟超时无关。
❌ 用超时参数来给上传限速。 ✅ 这是拿错误工具干正确的事。限速用limit_rate/limit_conn,超时只该用来兜异常。
❌ 开了proxy_request_buffering off却没有proxy_http_version 1.1。 ✅ 流式转发需要分块传输支持,上游通常也需要 HTTP/1.1 才能正确接收不定长的请求体。
❌ 开了proxy_request_buffering off,却指望失败时能自动重试到另一台上游。 ✅ 请求体已经开始发送,无法重放。这个模式的代价之一就是失去重试能力,需要应用层做补偿。
❌ 改了client_body_temp_path指向一个新目录,没建目录也没改属主。 ✅ 目录必须事先存在,属主是 worker 运行用户(RHEL 系nginx,Debian 系www-data),SELinux 下还需要正确的上下文(restorecon)。否则上传会以 500 失败,错误信息跟超时完全不像。
❌ 直接在http块把client_max_body_size设成0(不限制),图省事。 ✅ 这在暴露到公网的站点上等于取消了一道防护。按业务路径单独放开,并在边缘设备上做体积与速率限制。
总结
| 症状 / 目标 | 先看哪条指令 | 默认值 | 备注 |
|---|---|---|---|
| 413 Request Entity Too Large | client_max_body_size | 1m | 大文件上传第一嫌疑人 |
上传到一半断,日志while reading client request body | client_body_timeout | 60s | 相邻读操作之间的停顿 |
日志while sending request to upstream | proxy_send_timeout | 60s | 只在关掉请求缓冲后才是主要约束 |
| 后端处理慢导致 502/504 | proxy_read_timeout | 60s | 相邻读操作之间的停顿 |
500,日志No space left on device | client_body_temp_path所在分区 | — | 临时目录容量与权限 |
| 希望降低磁盘占用、更快让上游开始处理 | proxy_request_buffering off | on | 代价是不能重试、上游要支持分块 |
| 参数 | 调大的理由 | 代价 |
|---|---|---|
client_max_body_size | 允许大文件 | 攻击面变大,按路径限制 |
client_body_timeout | 容忍慢速客户端 | 连接占用更久 |
proxy_send_timeout | 流式模式下容忍慢速上传 | 上游连接占用更久 |
proxy_read_timeout | 上游处理大文件慢 | worker 被占住更久 |
结论:上传大文件失败时,proxy_send_timeout通常不是答案。先确认client_max_body_size、再看client_body_timeout、然后检查临时目录,最后才轮到代理侧的超时;而且只有在主动关掉proxy_request_buffering之后,proxy_send_timeout才真正开始管住上传速度。搞清楚每个超时管的是哪两个操作之间的间隔,这类问题就不会再靠"全都调大试试"来解决了。