应用 0.049 秒就做完了,用户却等了 7 秒
一句话总结
应用在 0.049 秒内完成,用户却等待了 7 秒。状态码是 200,应用日志也没有异常。这类故障只会在 nginx 的缓冲区与连接配置中留下痕迹。
为什么需要它
5xx 至少很显眼:仪表盘变红,告警响起。真正拖延很久的,是看起来没有任何人做错事的故障。
用户说“报表页面很慢”。开发团队翻查应用日志后回答:“我们 0.05 秒就响应了。”基础设施团队展示 CPU、内存和网络图表,回答:“服务器很空闲。”两者都是事实,用户说的也是真话。会议没有结论,下周又会原样重开。
缺失的拼图在 nginx 内部:响应可能被写入磁盘,请求正文可能落到临时文件,也可能每个请求都新建 TCP 连接。这三种问题都不会出现在状态码中。
缓冲——响应开始泄漏到磁盘的瞬间
proxy_buffering 默认为 on,而且理由充分。nginx 尽快完整接收 upstream 响应并释放 upstream 连接,再由 nginx 向慢客户端缓慢发送,避免 upstream 线程被慢用户长期占用。
问题出现在响应超过缓冲区(proxy_buffer_size + proxy_buffers)时。nginx 会把剩余部分写入磁盘临时文件。下面是真实测得的日志。
[warn] an upstream response is buffered to a temporary file
/var/lib/nginx/proxy/1/00/0000000001 while reading upstream,
request: "GET /app/big?n=8000000 HTTP/1.1"
需要注意两点。
第一,级别不是 [error],而是 [warn]。 多数团队把 error_log 限制为 error,或只对包含 [error] 的内容告警,于是这一行永远不会到达任何人。把日志打开到 warn,是发现该故障的唯一方式。
第二,同一请求的 access log 是 200。 单看状态码永远发现不了。
对同一个 8MB 响应,分别在开启和关闭缓冲的路径上测得如下数据。
proxy_buffering on → ut=0.049 rt=4.173 + [warn] 임시 파일
proxy_buffering off → ut=3.188 rt=3.189 경고 없음
这张表完整揭示了缓冲的本质。开启时,upstream 只被占用 0.049 秒,其余由 nginx 承担,但会泄漏到磁盘;关闭时不再写入磁盘,代价是upstream 会一直被占用,直到客户端接收完毕。 ut 几乎贴近 rt 就是证据。
因此,应按场景判断。
- 大文件、报表下载 → 保持缓冲开启。尽快释放 upstream 的收益高于磁盘 I/O 成本,同时增大
proxy_buffers,减少写入临时文件的数据量。 - SSE、长轮询、实时日志流 → 关闭缓冲。upstream 长时间占用在这里属于正常现象,而缓冲可能把数据攒成一批发送,损害实时性。
但必须诚实记录一个结果。 在本实验环境中,以一秒间隔发送数据块时,开启和关闭 proxy_buffering 的两条路径都以一秒间隔到达。只要客户端能够接收,nginx 会边读边发。“开启缓冲会中断流式传输”这一常见说法,至少在这个条件下没有复现。流式场景关闭缓冲的真实理由,更接近于防止响应增大后泄漏到磁盘,以及有意维持 upstream 占用。
正文大小——应用永远看不到的 413
client_max_body_size 默认值是 1m,超过该值的上传会由 nginx 直接以 413 中断。
[error] client intended to send too large body: 3000000 bytes,
request: "POST /app/toobig HTTP/1.1"
同一请求的 access log 如下。
413 ut=- rt=0.002 us=- "POST /app/toobig HTTP/1.1"
ut 和 us 都是横线,表示请求从未到达 upstream。 在实验中统计 upstream 服务器日志,确实为 0 条。开发团队说“我们的日志里没有该请求”时,并非推卸责任,而是准确事实。
正文大小还关联另一个更少人知道的设置。超过 client_body_buffer_size(默认 16k,部分平台为 8k)的正文会写入磁盘而非内存。
[warn] a client request body is buffered to a temporary file
/var/lib/nginx/body/0000000003, request: "POST /app/spill HTTP/1.1"
如果每秒涌入数百个 500KB 表单上传,这条日志每秒就出现数百次,磁盘也以相同频率写入与删除,而状态码全部是 200。
连接复用——三行必须全部存在
upstream Keep-Alive 只有三项配置同时存在才会工作,缺少任一项都会静默关闭。
upstream app {
server 127.0.0.1:9101;
keepalive 16; # ①
}
location /app/ {
proxy_http_version 1.1; # ②
proxy_set_header Connection ""; # ③
proxy_pass http://app/;
}
连续发送 10 个相同请求,实测 upstream 观察到的 TCP 连接数如下。
| 配置 | upstream 看到的连接数 |
|---|---|
| 全部缺少 | 10 |
| 只有 ① | 10 |
| ① + ② | 10 |
| ① + ② + ③ | 1 |
只配置 ② 就结束的情况尤其常见。大家以为升级到 HTTP/1.1 就够了,但客户端发送的 Connection 请求头仍被转发到 upstream,导致每次连接都被关闭。③负责删除该请求头。
无法复用时,每个请求都会创建并关闭新 socket,TIME_WAIT 不断积累。平时毫无症状,一旦流量超过临界点,就会因端口耗尽集中爆发。此时表面症状是 connect() failed,也就是 502。根因是连接配置,症状却看起来像“后端死了”。本课程“症状与原因位于不同位置”的主题在这里再次出现。
在实际项目中
把 error_log 级别开放到 warn。 本节两类临时文件日志都是 [warn]。限制为 error,就等于让这些故障不存在。担心日志量时,应缩短轮转周期,而不是缩窄级别。
在日志格式中同时保留 $upstream_response_time 和 $request_time。 没有这两列,就无法完成“应用慢还是传输慢”的第一次分流;无法分流,会议就会反复召开。
修改配置前先测量。 增大缓冲区与关闭缓冲都不是免费的。如果不以数字记录收益与代价,六个月后就没人敢动这些值。
下一实验要做什么
对同一个 8MB 响应分别开启和关闭缓冲,比较两个时间值;用 upstream 日志证明 413 请求根本未到应用;在三项连接配置前后实测连接复用。最后把这些数字写进运维判断标准。