适用场景

本文适用于 Nginx 作为反向代理或网关时,用户访问接口偶发或持续返回 504 Gateway Time-out,错误日志中出现 upstream timed outwhile reading response header from upstream 等信息的场景。

典型架构如下:

Client -> SLB/CDN -> Nginx -> 应用服务 -> 数据库/缓存/第三方接口

这类问题不能只靠调大 proxy_read_timeout 解决。超时只是结果,真正原因可能在 Nginx 到 upstream 的连接、应用线程池、慢 SQL、外部依赖、DNS、网络抖动或队列堆积。

现象描述

线上常见现象包括:

  • 用户侧看到 504 Gateway Time-out
  • Nginx access log 中 status=504request_time 接近某个固定阈值,例如 30 秒或 60 秒。
  • Nginx error log 中有类似记录:
upstream timed out (110: Connection timed out) while reading response header from upstream

如果 access log 已经配置 upstream 字段,可能看到:

10.0.1.24 - - [27/Jul/2026:09:12:33 +0800] "GET /api/orders/export HTTP/1.1" 504 167 "-" "curl/8.0" rt=60.001 uct=0.003 uht=- urt=60.000 us=504 ua=10.0.2.18:8080

关键字段含义:

  • rt:客户端请求在 Nginx 总耗时。
  • uct:Nginx 连接 upstream 的耗时。
  • uht:Nginx 等待 upstream 返回响应头的耗时。
  • urt:upstream 整体响应耗时。
  • us:upstream 返回状态码。
  • ua:实际命中的 upstream 地址。

上面这条日志里 uct=0.003 说明 Nginx 很快连上了后端,urt=60.000 且最终 504,说明主要时间花在等待应用响应,而不是 TCP 建连。

可能原因

upstream timed out 不是单一问题,常见原因可以分为四类。

第一类是应用自身处理慢:

  • 慢 SQL、缺索引、锁等待。
  • 请求触发大文件导出、大批量同步、复杂报表。
  • 应用线程池、连接池、协程池耗尽。
  • GC、CPU 飙高或内存压力导致响应变慢。

第二类是依赖服务慢:

  • Redis、MySQL、对象存储、第三方 HTTP 接口响应慢。
  • 依赖服务连接池等待时间过长。
  • 跨机房或跨公网调用链路抖动。

第三类是 Nginx 代理配置不匹配:

  • proxy_read_timeout 小于业务合理处理时间。
  • upstream keepalive、连接复用、HTTP 版本配置不完整。
  • 大响应未及时发送,触发读超时。
  • 缓冲区配置不合适,导致临时文件或磁盘 IO 压力。

第四类是网络与系统资源问题:

  • upstream 主机 TCP accept 队列满。
  • Nginx 或 upstream 端口耗尽、连接数过高。
  • 防火墙、conntrack、负载均衡健康检查异常。
  • DNS 解析延迟或 upstream 地址漂移。

排查思路

排查时先确认超时发生在哪一段:客户端到 Nginx、Nginx 到 upstream 建连、upstream 处理、还是响应传输。

1. 让 access log 带上 upstream 耗时

如果当前日志没有 upstream 字段,建议增加一个专用格式:

log_format upstream_timing '$remote_addr - $remote_user [$time_local] '
  '"$request" $status $body_bytes_sent '
  'rt=$request_time '
  'uct=$upstream_connect_time '
  'uht=$upstream_header_time '
  'urt=$upstream_response_time '
  'us=$upstream_status '
  'ua=$upstream_addr '
  'xff="$http_x_forwarded_for" '
  'reqid="$request_id"';

access_log /var/log/nginx/access.log upstream_timing;

检查配置并平滑加载:

nginx -t
nginx -s reload

观察 504 请求:

awk '$9 == 504 {print}' /var/log/nginx/access.log | tail -n 20

如果日志格式不同,可以按关键字筛选:

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

判断方式:

  • uct 很高:Nginx 连 upstream 慢,优先看网络、监听队列、upstream 进程状态。
  • uct 很低但 uhturt 接近超时阈值:后端应用处理慢或依赖慢。
  • ua 固定为某台机器:可能是单个 upstream 节点异常。
  • us 出现多个状态码,例如 502, 504:可能发生了重试,需要结合 proxy_next_upstream 判断。

2. 查看 Nginx error log 的具体阶段

grep -E 'upstream timed out|connect\\(\\) failed|no live upstreams|upstream prematurely closed' /var/log/nginx/error.log | tail -n 50

常见日志含义:

  • while connecting to upstream:连接 upstream 阶段超时或失败。
  • while reading response header from upstream:upstream 已连接,但迟迟没有返回响应头。
  • while reading upstream:响应头已返回,响应体读取过程中超时。
  • upstream prematurely closed connection:后端提前断开连接,可能是应用崩溃、超时中断或连接复用不兼容。

不同阶段对应的处理方向不同。不要看到 504 就直接增加所有 timeout。

3. 按接口和 upstream 节点聚合

先找出最常超时的接口:

grep ' 504 ' /var/log/nginx/access.log \
  | awk -F'"' '{print $2}' \
  | awk '{print $2}' \
  | sort | uniq -c | sort -nr | head

再按 upstream 地址聚合。如果日志里包含 ua=

grep ' 504 ' /var/log/nginx/access.log \
  | sed -n 's/.* ua=\\([^ ]*\\).*/\\1/p' \
  | sort | uniq -c | sort -nr

如果只有某个接口超时,多半是业务逻辑、SQL 或依赖问题。如果集中在某台 upstream,则优先隔离该节点并检查它的系统指标。

4. 在 Nginx 机器上直连 upstream 验证

从 Nginx 机器直接访问后端,绕过外层 LB 和域名:

curl -sS -o /dev/null -w 'code=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
  http://10.0.2.18:8080/api/orders/export

字段怎么看:

  • time_connect 高:TCP 建连慢。
  • time_starttransfer 高:后端开始返回响应慢。
  • time_total 高但 time_starttransfer 低:响应体传输慢或响应很大。

如果直连也慢,问题基本不在 Nginx。如果直连快但经 Nginx 慢,再检查 Nginx 代理、缓冲、连接复用和负载均衡策略。

5. 检查 upstream 主机资源

在异常 upstream 上查看 CPU、内存、磁盘和连接状态:

top -H -p $(pgrep -f 'java|gunicorn|uwsgi|node' | head -n 1)
free -m
iostat -xz 1 5
ss -antp | awk '{print $1}' | sort | uniq -c

如果是 Linux 服务端监听队列问题:

ss -lnt sport = :8080
netstat -s | grep -i 'listen'

重点关注:

  • Recv-Q 是否持续大于 0。
  • listen queue overflowed 是否增长。
  • 应用进程线程数是否打满。
  • 磁盘 awaitutil 是否异常升高。

6. 结合应用日志定位请求链路

Nginx 日志里建议带 request_id,并透传给 upstream:

proxy_set_header X-Request-Id $request_id;

应用日志也打印同一个请求 ID。这样可以把一条 504 请求串起来:

grep 'reqid=0f4b7d2f9c2a' /var/log/nginx/access.log
grep '0f4b7d2f9c2a' /data/app/logs/app.log

重点看应用日志中的耗时拆分:

  • 参数校验耗时。
  • 查询数据库耗时。
  • 调用 Redis 耗时。
  • 调用第三方接口耗时。
  • 序列化和响应写出耗时。

如果应用没有耗时日志,至少应在入口、中间依赖调用、出口三处打点。

定位示例

某订单导出接口偶发 504,Nginx 配置如下:

location /api/ {
  proxy_pass http://order_backend;
  proxy_connect_timeout 3s;
  proxy_read_timeout 60s;
  proxy_send_timeout 60s;
}

access log 显示:

"GET /api/orders/export HTTP/1.1" 504 rt=60.001 uct=0.002 uht=- urt=60.000 us=504 ua=10.0.2.18:8080

判断:

  • uct=0.002,Nginx 到应用建连正常。
  • uht=-,应用在 60 秒内没有返回响应头。
  • urt=60.000,刚好命中 proxy_read_timeout 60s

继续查应用日志,发现导出接口执行了一条无分页的大范围查询:

SELECT *
FROM orders
WHERE created_at >= '2026-07-01'
ORDER BY created_at DESC;

数据库执行计划显示没有命中合适索引:

EXPLAIN SELECT *
FROM orders
WHERE created_at >= '2026-07-01'
ORDER BY created_at DESC;

结果中 type=ALLrows 很大,说明全表扫描。业务高峰期该查询超过 60 秒,Nginx 等不到响应头,于是返回 504。

修复方案

修复要分短期止血和长期治理。

短期止血

  1. 临时下线异常 upstream 节点

如果 504 集中在某台机器,先从 upstream 中摘除:

upstream order_backend {
  server 10.0.2.18:8080 down;
  server 10.0.2.19:8080;
}

然后执行:

nginx -t && nginx -s reload
  1. 对确实需要长时间处理的接口单独放宽超时

不要全局调大。只对导出、报表等接口设置更合适的超时:

location /api/orders/export {
  proxy_pass http://order_backend;
  proxy_connect_timeout 3s;
  proxy_send_timeout 30s;
  proxy_read_timeout 180s;
}

这只是避免网关提前断开,不代表性能问题已经解决。

  1. 限制高成本接口并发
limit_req_zone $binary_remote_addr zone=export_limit:10m rate=1r/s;

location /api/orders/export {
  limit_req zone=export_limit burst=2 nodelay;
  proxy_pass http://order_backend;
}

对于导出类接口,更推荐业务侧做异步任务,Nginx 限流只是保护措施。

长期治理

  1. 优化慢查询

为过滤和排序字段建立匹配索引:

CREATE INDEX idx_orders_created_at_id ON orders(created_at, id);

导出时避免一次性加载所有数据,改为游标或分页拉取:

SELECT id, order_no, amount, created_at
FROM orders
WHERE created_at >= ?
  AND id > ?
ORDER BY id
LIMIT 1000;
  1. 长任务异步化

导出、批量同步、复杂报表不要占用 HTTP 请求线程等待完成。可以改成:

  • 请求创建任务,立即返回任务 ID。
  • 后台 worker 生成文件。
  • 前端轮询任务状态或通过消息通知。
  • 文件生成后从对象存储下载。
  1. 配置 upstream keepalive

高并发短请求场景可启用 upstream keepalive,减少 TCP 建连开销:

upstream order_backend {
  server 10.0.2.18:8080 max_fails=3 fail_timeout=10s;
  server 10.0.2.19:8080 max_fails=3 fail_timeout=10s;
  keepalive 64;
}

location /api/ {
  proxy_http_version 1.1;
  proxy_set_header Connection "";
  proxy_pass http://order_backend;
}

注意:启用 keepalive 后要确认 upstream 应用支持长连接,并观察连接数、空闲连接和负载均衡效果。

  1. 建立超时分层

常见建议:

  • proxy_connect_timeout 设置较短,例如 1 到 3 秒。
  • 普通接口 proxy_read_timeout 不宜过长,例如 30 到 60 秒。
  • 长任务接口单独配置,不要污染全局。
  • 应用自身的数据库、Redis、HTTP 客户端超时应小于或等于网关超时,并能返回明确错误。

如果应用内部调用第三方接口没有超时,Nginx 最终只能看到 504,真正的阻塞点会被隐藏。

预防措施

  1. 日志字段标准化

Nginx access log 至少保留:

  • $request_time
  • $upstream_connect_time
  • $upstream_header_time
  • $upstream_response_time
  • $upstream_status
  • $upstream_addr
  • $request_id
  1. 建立 504 告警

按接口维度监控:

  • 504 数量。
  • 504 比例。
  • P95/P99 请求耗时。
  • upstream 节点维度错误率。

Prometheus Nginx exporter 或日志采集系统都可以实现,关键是不要只看总量,要能定位到接口和 upstream 节点。

  1. 应用侧输出耗时拆分

建议每个请求至少记录:

request_id=xxx path=/api/orders/export total=812ms db=620ms redis=8ms third_party=0ms status=200

一旦发生 504,可以直接判断是数据库、缓存、外部接口还是应用自身逻辑慢。

  1. 高成本接口做隔离

导出、批量导入、报表、对账等接口应和普通查询接口隔离:

  • 单独线程池。
  • 单独队列。
  • 单独限流。
  • 单独超时。
  • 必要时单独部署 worker。

这样可以避免一个慢接口拖垮整个应用。

总结

Nginx upstream timed out 的核心排查方法是先看超时阶段,再按接口、upstream 节点和请求链路逐层缩小范围。uct 高通常看网络和连接,uhturt 接近阈值通常看应用处理和依赖服务。

治理时不要简单全局调大超时。更稳妥的做法是:日志补齐 upstream 耗时字段,长任务异步化,高成本接口限流隔离,应用依赖设置明确超时,并建立按接口和节点维度的 504 告警。这样才能把一次 504 故障从“网关超时”定位到真正的慢点。