线上接口突然超时,怎么判断卡在 Nginx、线程池、连接池还是 SQL?

线上接口突然超时,怎么判断卡在 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=maxpending>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、数据库账号和业务数据。

相关推荐
Java内核笔记1 小时前
容错能力进入 spring-core:Spring Boot 4 原生重试机制全解析
java·后端
风卿1 小时前
知识库双路召回:BM25 关键词与语义向量 RRF 融合,附指标实测
后端
未秃头的程序猿1 小时前
虚拟线程上线一周后翻车了——pinning问题排查实录
java·后端·架构
AI_paid_community1 小时前
如何使用 Claude 在 AI 时代快速入局新的行业?(经验贴)
前端·javascript·后端
AI多Agent协作实战派1 小时前
AI多Agent协作系统实战(三十六):代码里明明写了,编译完怎么没了?
后端
柠檬味拥抱1 小时前
代码看腻了,我让 Seed Evolving 把整个仓库搓成了一座能走进去的 3D 城市
后端
mit6.8242 小时前
archived
后端·python·flask
用户298698530142 小时前
效率工具分享:3 款免费 Markdown 转 Word 在线转换器
人工智能·后端
程序员天天困2 小时前
Spring Boot i18n 国际化实战:从资源文件到多语言接口完整指南
spring boot·后端·编程语言