LabHub
学习 学习路径 课程

nginx 故障处理

应用 0.049 秒就做完了,用户却等了 7 秒

在 LabHub 中继续学习

一句话总结

应用在 0.049 秒内完成,用户却等待了 7 秒。状态码是 200,应用日志也没有异常。这类故障只会在 nginx 的缓冲区与连接配置中留下痕迹。

概念图: 只会在 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 就是证据。

因此,应按场景判断。

但必须诚实记录一个结果。 在本实验环境中,以一秒间隔发送数据块时,开启和关闭 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"

utus 都是横线,表示请求从未到达 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 请求根本未到应用;在三项连接配置前后实测连接复用。最后把这些数字写进运维判断标准。