HTTP请求超时如何排查?客户端、代理层与后端服务超时设置

"接口偶尔超时""页面转圈很久最后报 504""调用第三方偶尔失败"——超时是线上最常见、也最难缠的一类问题。它难就难在:一次 HTTP 请求会穿过客户端、网络、负载均衡、反向代理、后端应用、数据库等多层,每一层都有自己的超时设置,任何一层设得比上游短,都会在那一层先"放弃"。更麻烦的是,超时具有偶发性和环境相关性,昨天好好的今天又犯,或者只有部分用户遇到。

这篇文章按"链路"的思路讲超时排查:先讲清一次请求中超时到底发生在哪几种形态(连接超时、读超时、写超时、空闲超时),再用一张表列出各层的典型超时参数和常见默认值,然后分别讲客户端、Nginx 代理层、后端应用与数据库的超时配置,重点给出分层超时的设计原则——上游超时要比下游略长,避免"内层还在算、外层先放弃"的资源浪费。最后给出一套定位"到底哪层超时"的实战方法,配合日志和 curl 复现。

核心原则先给出:超时值要分层设置、层层递减(从外到内逐层变长),且必须与业务的实际耗时匹配。所有层都用默认值、或者上层比下层还短,是超时问题的主要根源。

一、一次请求的超时链路

一个典型请求的旅程和它的"等待关系":

用户浏览器 (前端超时)
   │
   ▼
CDN / 负载均衡 (连接/读超时)
   │
   ▼
Nginx 反向代理 (proxy_connect/read/send_timeout)
   │
   ▼
后端应用 (脚本/请求超时) ──> 数据库 (连接/查询/事务超时)

每一层都在"等下游回应":前端等 Nginx,Nginx 等后端,后端等数据库。如果某一层的等待上限到了,它就会断开并返回错误,而上游看到的是"连接被中断"或"超时"。所以排查超时,本质是搞清楚"哪一层的等待先到点"。

各层超时后表现出的现象不同:

超时发生层 用户/上游看到的现象
前端自身超时 请求被前端主动 abort,可能无响应
负载均衡/CDN 502 或 504,源站可能没收到请求
Nginx 等后端超时 504 Gateway Timeout,error.log 有 upstream timed out
后端等数据库超时 后端返回 500,应用日志有数据库超时异常
后端自身执行超时 后端进程被杀,Nginx 侧看到 502

二、超时的几种形态

配置超时参数时,要分清这几种"超时"的具体含义,否则会设错位置:

  • 连接超时(connect timeout):建立 TCP 连接的最长等待时间。如果对端不响应 SYN,或端口没开,连不上就触发。
  • 读超时(read timeout):连接已建立,等待对端返回数据的最大间隔。它是"两个数据包之间"的最长等待,不是整个请求的总时长。
  • 写超时(send timeout):向对端发送数据的最大间隔,通常在大上传/大响应时触发。
  • 空闲超时(idle timeout):连接上没有数据流动超过一定时间就断开,常见于长连接(keepalive、WebSocket)。
  • 总时长限制:某些层限制的是"整个请求从开始到结束的总时长",而不是两次数据之间的间隔。要注意区分。

理解"读超时是两次数据之间的间隔"这一点非常关键:一个流式接口即使总耗时 10 分钟,只要每几秒就有数据输出,就不会触发读超时;反之一个"憋很久再一次性返回"的接口,即使后端在努力工作,也会因为中间没有数据而被读超时切断。

三、各层超时参数一览

下表汇总常见层的超时参数和常见默认值(默认值为经验值,不同版本/发行版可能不同,以实际配置为准):

层级 参数 常见默认 含义
Nginx proxy_connect_timeout 60s 连后端超时
Nginx proxy_read_timeout 60s 读后端响应超时
Nginx proxy_send_timeout 60s 向后端发请求超时
Nginx client_header_timeout 60s 读客户端请求头超时
Nginx client_body_timeout 60s 读客户端请求体超时
Nginx send_timeout 60s 向客户端发送响应超时
Nginx keepalive_timeout 75s 客户端长连接空闲超时
PHP max_execution_time 30s 脚本执行超时
PHP-FPM request_terminate_timeout 0 单请求最长执行
Java/Spring server.tomcat.connection-timeout 20s 连接建立等待
MySQL wait_timeout 28800s 空闲连接被断开
MySQL max_execution_time 0 查询执行上限(仅 SELECT)
JDBC socketTimeout 0 读数据包超时
HTTP 客户端 connectTimeout/readTimeout 视库而定 调用第三方超时

头号误区:只配了 Nginx 的 proxy_read_timeout,却忘了后端 PHP 的 max_execution_time 还是 30 秒。结果后端 30 秒就把脚本杀了、返回 502,Nginx 那边还没到自己的 60 秒,用户看到的是 502 而不是 504。要顺着链路把每一层都对上。

四、客户端超时

前端 AJAX/fetch 的超时如果不设,某些浏览器会等很久才报错;如果设得太短,后端还在算就被前端放弃。

// fetch 没有内置 timeout,用 AbortController
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 15000);  // 15 秒
try {
  const res = await fetch('/api/slow', { signal: ctrl.signal });
} finally {
  clearTimeout(timer);
}

// axios 可直接设
axios.get('/api/slow', { timeout: 15000 });

后端服务之间调用第三方 API 时,一定要设连接和读超时,否则第三方卡住会拖垮自己的线程池(这是"雪崩"的常见起点)。

// Java HttpClient
HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(3))            // 连接超时
    .build();
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://third.party/api"))
    .timeout(Duration.ofSeconds(10))                  // 整体超时
    .build();

五、代理层(Nginx)超时

Nginx 侧的三个核心超时,方向要分清:proxy_connect_timeout 是连后端,proxy_read_timeout 是等后端回数据,proxy_send_timeout 是给后端发数据。

location /api/ {
    proxy_pass http://127.0.0.1:8080;

    proxy_connect_timeout 5s;      # 连后端 5 秒足够,连不上就是故障
    proxy_read_timeout    120s;    # 等后端响应,按业务最慢接口设
    proxy_send_timeout    120s;    # 给后端发请求体,大请求时放宽

    # 客户端侧
    send_timeout 60s;              # 给客户端发响应
    client_body_timeout 60s;       # 读客户端请求体
}

判断 504 的证据在 Nginx 错误日志:

grep -i 'upstream timed out\|timed out' /var/log/nginx/error.log | tail
# 典型记录:
# upstream timed out (110: Connection timed out) while reading response header from upstream,
#   client: 10.0.0.9, request: "GET /api/slow HTTP/1.1", upstream: "http://10.0.0.5:8080/api/slow"

这行会明确告诉你超时发生在读响应阶段,以及对端是谁、处理了多久。

六、后端应用与数据库超时

后端是超时的"重灾区",尤其涉及数据库慢查询和第三方调用。

PHP:

; php.ini
max_execution_time = 60        ; 脚本最长执行
; php-fpm www.conf
request_terminate_timeout = 60 ; FPM 层强制终止,须 >= max_execution_time

Java/Spring:

server:
  tomcat:
    connection-timeout: 20000   # 连接建立等待 20s

数据库和 JDBC 超时(以 MySQL 为例):

# my.cnf
wait_timeout = 600              # 空闲连接 10 分钟断开
interactive_timeout = 600
# JDBC 连接串:设置 socket 读超时,避免一条慢查询永久挂住
jdbc:mysql://db:3306/app?socketTimeout=30000&connectTimeout=5000

要点:数据库连接池的超时、连接最大存活时间、以及单条 SQL 的执行上限,都要和服务层超时协调。一条没有超时的慢查询,能把连接池占满,进而让所有请求超时——这是典型的级联故障。

七、分层超时的设计原则

这是避免超时问题的关键工程原则,务必记住:

从外到内,超时值应逐层变长。外层等待上限必须大于内层处理上限,否则外层会先放弃,内层白干。

举例(经验值,按业务调整):

层 建议超时 说明
前端 AJAX 15s 用户可接受的最长等待
Nginx proxy_read_timeout 30s 略大于后端上限
后端脚本执行上限 25s 略大于 DB 查询预算
单条数据库查询 20s 最慢的合理查询
DB 连接超时 5s 连不上要快速失败

这里的逻辑是:DB 20s 到点会返回错误给后端,后端在 25s 内拿到错误并返回 500 给 Nginx,Nginx 在 30s 内收到,前端 15s... 等等,注意这里前端 15s 反而最短,会导致前端先超时但后端还在算。

修正:前端超时应大于 Nginx 的读超时,否则前端先放弃、后端仍在处理,造成"用户看到失败但服务端还在跑"的浪费。更合理的是:

DB(20s) < 后端(25s) < Nginx(30s) < 前端/网关(35s)

内层短、外层长,这样每一层的失败都能被上层感知并妥善返回,不会出现"外层放弃、内层还在跑"的资源浪费,也不会出现"内层失败、外层傻等"的额外延迟。

经验值:连接超时普遍设 3~5 秒(连不上就是故障,快速失败);读超时按业务最慢接口的 P99 再留 30%~50% 余量,普通接口 10~30 秒,报表/导出类可到几分钟;数据库连接超时 3~5 秒。

八、排查实战:定位到底哪层超时

一套可复用的方法:

# 1) 看 Nginx 错误日志,确认是否 504 及发生在哪个阶段
grep -i 'timed out' /var/log/nginx/error.log | tail -20

# 2) 结合访问日志的 $request_time / $upstream_response_time 判断瓶颈层
#    若 request_time 大而 upstream_response_time 小,慢在 Nginx/客户端侧
grep 'slow-endpoint' /var/log/nginx/access.log | awk '{print $NF}' | tail

# 3) 绕过 Nginx 直连后端,看是后端慢还是代理慢
curl -v -m 60 http://127.0.0.1:8080/api/slow

# 4) 用 -w 打印分段耗时,精确定位是连接、首字节还是总时长
curl -o /dev/null -s -w \
  'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
  https://api.example.com/api/slow

# 5) 后端日志 + 数据库慢查询日志
tail -f /var/log/myapp/app.log | grep -i 'timeout\|slow'
# MySQL 慢查询
tail -f /var/log/mysql/slow.log

curl -w 的分段耗时极其有用:connect 大说明建连慢(网络/后端负载),ttfb(首字节)大说明后端计算慢,total 大而 ttfb 小说明传输/响应体大。用它能一眼看出瓶颈在哪一段。

同时检查后端线程/连接池是否被打满——超时常常是"池耗尽"的伴生现象:

# 看后端连接数、线程数
ss -s | head
netstat -anp 2>/dev/null | grep :8080 | wc -l
# Java 线程栈
jstack  | grep -c 'http-nio'

常见问题

Q:只调大 Nginx 的 proxy_read_timeout 就能解决超时吗?
不一定。若超时根因在后端(脚本执行超时、数据库慢查询、第三方卡住),只调 Nginx 只会让用户等更久然后依然失败。正确做法是顺着链路找到真正的慢点:用 curl -w 看首字节耗时、看后端日志、看数据库慢查询,逐层解决。

Q:为什么 Nginx 返回 504,但后端日志显示请求还在正常执行?
说明 Nginx 的读超时到了就断开并返回 504,而后端并不知道,还在继续算,算完发现客户端已断(日志里可能报 broken pipe)。这正是"外层超时短于内层"的典型浪费。解法是把内层处理上限设得比外层短,或用异步任务解耦长耗时操作。

Q:proxy_read_timeout 是"整个请求"的上限吗?
不是。它是两次读到数据之间的最大间隔。流式接口只要持续有数据输出就不会超时;而一个"憋很久才返回"的接口即使总耗时没到,也可能因中间无数据而超时。

Q:接口偶尔超时、大部分正常,怎么查?
先看超时是否与流量高峰、特定接口、特定后端实例相关。对比多个后端实例的响应时间,看是否个别实例慢(硬件/负载差异);看数据库是否偶发慢查询或锁等待;看是否有 GC 停顿(Java)或资源竞争。偶发超时常常指向"资源接近饱和时的排队",而不是配置错误。

Q:前端超时设太短有什么坏处?
会有"用户看到失败,但服务端其实成功了"的问题。比如支付类接口,前端 5 秒超时了,但后端实际在 8 秒完成了扣款。这类写操作必须保证幂等,前端超时值也应给足,或改用异步查询结果的方式,避免不一致。

总结

一次请求的超时可能发生在客户端、CDN/负载均衡、Nginx、后端应用、数据库的任意一层,排查要先确定"哪一层的等待先到点",再看该层日志和用 curl -w 分段耗时定位。

区分超时形态很重要:连接超时、读超时(两次数据间的间隔,不是总时长)、写超时、空闲超时,含义不同,配置位置也不同。

分层设计原则是从外到内超时逐层变长(如 DB 20s < 后端 25s < Nginx 30s < 前端 35s),避免"外层先放弃、内层还在跑"的浪费和"内层已失败、外层还在等"的延迟。

最常见的误区是只调了 Nginx 超时却漏了后端脚本/数据库超时,导致现象从 504 变成 502 或 500;要顺着链路把每一层都对上,并给连接超时设短(3~5 秒快速失败)。

超时常常是资源饱和(连接池满、线程池满、慢查询堆积)的伴生现象,光调参数治标,优化后端处理和资源上限才是治本。