线上接口突然超时,怎么判断卡在 Nginx、线程池、连接池还是 SQL?
接口超时时,Nginx 说上游没响应,Java 日志里没有异常,数据库 CPU 也不高。
大家都觉得自己这一层没问题,最后只能先重启应用。
其实不用把所有日志翻一遍。沿着请求链路找 4 组证据,通常十几分钟就能判断请求到底卡在哪一层。
一、先看懂这条请求链路
一个普通的 Spring Boot 接口,至少要经过下面几层:
text
客户端
↓
Nginx
↓
Tomcat 请求线程
↓
HikariCP 数据库连接池
↓
MySQL 执行 SQL
其中任何一层发生排队,用户看到的都可能只是两个字:
text
超时
但修复方式完全不同:
text
Nginx 连不上应用 -> 查地址、端口、网络和实例状态
Tomcat 请求线程耗尽 -> 查线程都在执行或等待什么
HikariCP 连接池耗尽 -> 查连接为什么长期不归还
MySQL 执行变慢 -> 查慢 SQL、锁等待和执行计划
如果一上来就重启、扩线程池或者加大超时时间,很容易把现场冲掉,还可能把压力继续传给下游。
正确思路不是"把所有东西都检查一遍",而是:
从外到内,每次只回答一个问题:请求有没有到这一层?到了以后,在这里停了多久?
二、第一步:先确定超时发生在调用方还是服务端
先从与用户相同的入口发起请求:
bash
curl -v \
--connect-timeout 2 \
--max-time 10 \
-o /dev/null \
-s \
-w 'code=%{http_code} connect=%{time_connect}s start=%{time_starttransfer}s total=%{time_total}s\n' \
'https://api.example.com/orders/10001'
例如输出:
text
code=504 connect=0.018s start=3.006s total=3.006s
这组数据至少说明:
text
TCP/TLS 连接很快建立
真正的等待发生在连接建立以后
3 秒左右由网关返回了 504
如果 connect 本身就很高,优先检查 DNS、网络、负载均衡和入口端口。
如果 connect 很快,但 starttransfer 很高,说明请求已经进入服务端链路,只是迟迟没有收到响应头。
不要只在自己的电脑上测一次。最好同时从内网机器、Nginx 节点和应用所在网络分别测试,区分是单个调用方问题还是服务端公共问题。
三、第二步:用 Nginx 耗时判断上游有没有接住请求
Nginx access log 如果只有状态码,定位价值非常有限。
建议增加一份带 upstream 耗时的日志格式:
nginx
log_format upstream_timing
'$request_id $remote_addr "$request" status=$status '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time '
'upstream=$upstream_addr';
access_log /var/log/nginx/access.log upstream_timing;
遇到故障时查看:
bash
tail -n 200 /var/log/nginx/access.log
tail -n 100 /var/log/nginx/error.log
重点看这四个值:
text
rt = Nginx 处理整个请求的时间
uct = Nginx 与上游建立连接的时间
uht = Nginx 等到上游响应头的时间
urt = Nginx 与上游交互的响应时间
情况 1:uct 为空,错误日志显示 connection refused
text
connect() failed (111: Connection refused) while connecting to upstream
请求还没有进入 Java,优先检查:
text
upstream 地址和端口
应用是否监听
容器网络和服务名
实例是否正在重启
情况 2:uct 很小,但 uht 很大
例如:
text
status=504 rt=3.001 uct=0.001 uht=3.000 urt=3.000
这说明 Nginx 很快连接到了 Spring Boot,但上游迟迟没有返回响应头。
排查重点应该进入 Java 应用,而不是继续调整 Nginx 网络配置。
情况 3:状态码是 499
499 是 Nginx 常见的非标准状态码,表示客户端在 Nginx 返回响应前关闭了连接。
它不一定说明 Nginx 有问题,可能是客户端 3 秒超时,而服务端第 5 秒才返回。
此时继续看 upstream 耗时和应用日志,找到是谁比客户端的超时预算更慢。
需要注意,proxy_connect_timeout 控制的是与上游建立连接的超时;proxy_read_timeout 控制的是两次读取上游数据之间允许等待的时间,并不等同于整个接口的总执行时间。
四、第三步:看 Tomcat 请求线程到底在忙什么
如果 Nginx 已经成功连接上游,下一步不要先猜 CPU、GC 或 SQL。
直接看 Java 请求线程。
先找到进程:
bash
jps -l
假设 PID 是 18472,连续抓三次线程栈:
bash
jcmd 18472 Thread.print -l > /tmp/thread-1.log
sleep 5
jcmd 18472 Thread.print -l > /tmp/thread-2.log
sleep 5
jcmd 18472 Thread.print -l > /tmp/thread-3.log
不要只抓一次。连续三份线程栈里相同线程都停在同一个位置,才更能说明它是持续等待点。
重点搜索 Tomcat 请求线程:
bash
grep -n 'http-nio-.*-exec' /tmp/thread-1.log
常见结果可以这样判断。
1. 大量线程停在 HikariPool.getConnection()
text
at com.zaxxer.hikari.pool.HikariPool.getConnection(...)
at com.zaxxer.hikari.HikariDataSource.getConnection(...)
说明请求线程在等数据库连接,下一步检查 HikariCP。
2. 大量线程停在 Socket 读取
text
at java.net.Socket$SocketInputStream.read(...)
继续结合上层调用栈判断是在等 Redis、HTTP 下游、数据库还是其他网络服务。
3. 大量线程停在锁上
text
java.lang.Thread.State: BLOCKED
- waiting to lock <0x...>
说明应用内部可能存在锁竞争。需要找到持有同一把锁的线程,而不是增加 Tomcat 线程数。
4. 大量线程长期处于 RUNNABLE
如果同一段业务代码、序列化、加密或循环逻辑反复出现在多份栈中,同时进程 CPU 升高,再沿着 CPU 热点排查。
Spring Boot Actuator 默认会为 Web 请求生成 http.server.requests 指标。生产环境接入 Prometheus 等监控后,可以同时查看接口 P95/P99、请求量和异常率;/actuator/metrics 更适合诊断当前注册了哪些指标,不建议把它当成生产监控后端。
五、第四步:用 4 个数判断是不是数据库连接池
如果线程栈停在 HikariCP,立刻查看:
text
active 正在使用的连接数
idle 空闲连接数
pending 正在等待连接的线程数
max 连接池最大连接数
典型的连接池耗尽现场是:
text
active = 30
idle = 0
pending = 74
max = 30
它表示:
text
30 个连接全部被占用
没有连接可以立即借出
74 个请求线程正在等待
HikariCP 在连接池达到 maximumPoolSize 且没有空闲连接时,getConnection() 最多等待 connectionTimeout,超时后抛出异常。
因此,日志里经常能看到:
text
Connection is not available, request timed out after 30000ms
但连接池满不是根因,只是拥堵位置。
还要继续回答:连接为什么一直不归还?
text
SQL 执行慢
事务范围太大
事务里调用了远程接口
数据库发生锁等待
代码没有正确关闭连接
数据库本身可用连接数不足
这时直接把连接池从 30 调到 100,可能只是让 100 条慢 SQL 同时压向数据库。
正确动作是先记录连接池指标和线程栈,再进入 MySQL 找正在占用连接的会话。
六、第五步:确认 MySQL 是执行慢,还是在等锁
最直接的入口:
sql
SHOW FULL PROCESSLIST;
重点关注:
text
Time 当前状态持续了多久
State 正在执行、发送数据还是等待锁
Info 当前执行的 SQL
MySQL 8.4 官方文档建议优先考虑基于 Performance Schema 的 processlist 实现。也可以查询当前语句事件:
sql
SELECT
t.PROCESSLIST_ID,
t.PROCESSLIST_USER,
t.PROCESSLIST_HOST,
ROUND(es.TIMER_WAIT / 1000000000000, 3) AS seconds,
es.ROWS_EXAMINED,
es.NO_INDEX_USED,
es.SQL_TEXT
FROM performance_schema.events_statements_current es
JOIN performance_schema.threads t
ON t.THREAD_ID = es.THREAD_ID
WHERE es.SQL_TEXT IS NOT NULL
AND es.END_EVENT_ID IS NULL
ORDER BY es.TIMER_WAIT DESC;
如果大量会话都在执行同一条 SQL,继续检查执行计划:
sql
EXPLAIN
SELECT ...;
重点看:
text
访问类型
实际使用的索引
预估扫描行数
是否出现临时表或额外排序
如果会话状态指向锁等待,先确认阻塞者和被阻塞者,再决定终止会话、回滚事务还是处理业务代码。
EXPLAIN ANALYZE 会真正执行语句。对于写 SQL、大表查询或生产高峰,不要为了看计划直接运行,先在安全环境评估影响。
七、5 分钟快速判断表
| 现场证据 | 请求可能卡在哪里 | 下一步 |
|---|---|---|
Nginx connection refused |
upstream 地址、端口或实例 | 检查监听、容器网络和服务状态 |
Nginx uct 高 |
建立上游连接 | 检查网络、DNS、连接和实例负载 |
Nginx uct 低、uht 高 |
Java 或 Java 的下游 | 进入应用抓线程栈 |
| Tomcat 线程大量等待 HikariCP | 数据库连接池 | 查看 active、idle、pending、max |
| Tomcat 线程大量等待 Socket | Redis、HTTP 或数据库网络调用 | 根据完整调用栈定位具体客户端 |
HikariCP active=max、pending>0 |
连接池已经排队 | 查慢 SQL、长事务、锁等待和泄漏 |
| MySQL 同类 SQL 长时间执行 | SQL 或索引 | 查执行计划、扫描行数和数据分布 |
| MySQL 大量会话等待锁 | 长事务或锁冲突 | 定位阻塞会话和事务范围 |
这张表的价值不是代替完整排查,而是先决定应该把时间花在哪一层。
八、三个最容易把事故越修越大的动作
1. 直接扩大所有池子
Tomcat 线程池、数据库连接池和下游连接池是一条链。
只把上游池子调大,可能让更多请求同时挤进已经过载的下游。
2. 把所有超时都改成 60 秒
超时不是越大越稳定。
如果调用方只愿意等待 3 秒,Nginx 和 Java 却允许下游执行 60 秒,用户早已离开,服务端仍在消耗线程和连接。
超时应该从整个调用链的预算出发,逐层设置,并避免无上限重试。
3. 还没保存现场就重启
重启会清空线程、连接和部分临时状态。
如果业务允许,至少先保存:
text
故障时间和影响接口
Nginx access/error log
三份线程栈
连接池指标
MySQL 当前会话
最近发布和配置变更
保存完证据再止损,复盘时才不会只剩一句"重启后好了"。
九、我会照着执行的排查顺序
text
[ ] 用 curl 记录 connect、starttransfer、total
[ ] 看 Nginx 状态码、错误日志和 upstream 耗时
[ ] 确认请求是否到达 Spring Boot
[ ] 连续抓 3 份 Java 线程栈
[ ] 判断 Tomcat 线程在运行、等锁还是等下游
[ ] 查看 HikariCP active、idle、pending、max
[ ] 查询 MySQL 当前会话和锁等待
[ ] 对可疑 SQL 检查执行计划
[ ] 核对最近发布、配置和流量变化
[ ] 保存现场后再决定限流、摘实例、回滚或重启
整条排查主线可以压缩成一句话:
Nginx 耗时确定方向,线程栈找到等待点,资源池确认拥堵,MySQL 继续追根因。
总结
接口超时时,不要先问"是不是数据库慢了"。
先依次回答四个问题:
text
Nginx 有没有连上应用?
Tomcat 请求线程停在哪里?
连接池有没有开始排队?
MySQL 正在执行还是等待锁?
只要每一层都有可验证的证据,排查就不会变成多个团队互相猜测。
更重要的是,不要把"调大线程数、调大连接池、调大超时"当成通用修复。
池子和超时只能控制压力如何排队,真正的根因仍然可能是慢 SQL、长事务、锁竞争、远程调用或者错误的容量设计。
如果你正在处理接口超时、Nginx 504、线程池排队、连接池耗尽或慢 SQL,可以关注我把脱敏后的错误日志、线程栈和关键配置发给我,简单问题和初步定位可以免费帮忙看一下。生产信息请先隐藏域名、IP、密码、Token、数据库账号和业务数据。