页面转圈一分钟,最后蹦出 504 Gateway Timeout —— 很多人的第一反应是把 proxy_read_timeout 调到 300s,结果要么没用,要么把问题从「快失败」变成「慢失败」。这篇按排查顺序拆开讲,先定位责任方,再谈配置。

浏览器 ──①──> Nginx ──────────②──────────> upstream(后端) ──③──> 数据库
        总耗时 rt        等待响应 urt         后端内部耗时
                        (空闲间隔,非总时长)

先分清三种状态码,别急着改配置

状态码 含义 责任方
499 客户端主动断开(等不及了) 前端 / 用户网络
502 上游返回非法响应,或连接被拒 后端进程挂了 / 端口不通
504 Nginx 等待上游响应超时 后端处理慢

504 几乎一定是后端慢,不是 Nginx 慢。 改 Nginx 超时只是把耐心值调长,不解决慢的根因。

第一步:从日志判断超时发生在哪一段

默认日志不够用,先加一个带耗时的 format:

log_format timed $remote_addr $request $status rt=$request_time urt=$upstream_response_time ua=$upstream_addr;
access_log /var/log/nginx/access.log timed;

然后抓出 504 的行:

grep " 504 " /var/log/nginx/access.log | tail -20

重点看两个字段:

  • rt(request_time):Nginx 从收到请求到发完响应的总耗时
  • urt(upstream_response_time):后端从建立连接到返回完整响应的时间

三种组合对应三种病因:

rt urt 结论
60.000 60.000 后端自己处理超时了,去看后端
60.000 - 连接根本没建立或中途断开,看网络 / 端口 / 上游已挂
5.000 5.000 不是超时,是上游主动断开(多半表现为 502)

urt 出现多个值(如 1.2, 45.6)说明请求被重试过,命中了另一个节点 —— 这本身就是负载均衡有慢节点的线索。

第二步:核对三处超时的实际生效值

Nginx 的代理超时有三个,含义完全不同:

proxy_connect_timeout 5s;    # 与上游建立 TCP 连接的超时
proxy_send_timeout    60s;   # 向上游发送请求的空闲超时(两次写之间)
proxy_read_timeout    60s;   # 等待上游响应的空闲超时(两次读之间)

关键点:后两个都是「两次 IO 之间的空闲间隔」,不是整个请求的总时长。 所以一个持续吐数据的长轮询接口,只要每次间隔小于 proxy_read_timeout,跑一小时也不会 504;反过来,后端卡在一次慢 SQL 上 60 秒没有任何输出,就一定触发 504。

确认线上实际生效值:

nginx -T | grep -E "proxy_(connect|send|read)_timeout"

nginx -T 输出的是合并后的完整配置,比逐个翻 conf 文件可靠。如果 grep 结果和预期不符,检查是不是被 include 的片段覆盖了。

第三步:把锅定位到后端

确认 urt 已经超时后,转到后端侧查三件事:

  1. 慢 SQL:开慢查询日志,或观察连接池等待时间。连接池耗尽的表现是「请求在排队,单条 SQL 本身很快」。
  2. 外部依赖:第三方 API、短信、对象存储,往往没设超时,默认无限等待。
  3. 锁竞争:同一行数据的行锁等待,表象也是整体变慢。

后端没有埋点时,用 jstack 抓线程栈比读代码猜快得多 —— 找出卡在 BLOCKED 或 WAITING 的线程,一眼就能看到卡在哪一行。

常见坑

  • add_header 会覆盖上层:在 location 里写了一个 add_header,父层的全部失效,包括你在意的缓存头。
  • keepalive 写了不生效:upstream 里写了 keepalive 32,但没配 proxy_http_version 1.1 和清空 Connection 头,短连接照样把 TIME_WAIT 打满,表现为间歇性 502 / 504。
  • proxy_next_upstream 默认不含超时:默认只在连接失败时换节点,读超时不会重试,所以「一个慢节点拖垮整组」很常见。
  • 调大超时把问题藏起来:60s 改成 300s,用户等 5 分钟才看到错误页,体验损失比快速失败更大。

相关速查