news 2026/7/16 9:16:39

接口突然慢了 10 倍,我用这 5 个命令定位到一条 SQL

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
接口突然慢了 10 倍,我用这 5 个命令定位到一条 SQL

接口突然慢了 10 倍,我用这 5 个命令定位到一条 SQL

一个原本 200ms 的订单接口,线上突然变成 2 秒,偶尔还会直接超时。

CPU 看起来不高,内存也没有爆,重启之后短暂恢复,过一会儿又开始变慢。

最后没有靠重启解决,而是顺着线程、连接池和数据库,定位到了一条没有索引的 SQL。


一、事故现场

下午业务高峰期,监控连续报警:

订单查询接口 P99:230ms -> 2.6s Tomcat 活跃线程:38 -> 196 数据库连接池 active:12 -> 50 数据库连接池 pending:0 -> 87 接口超时率:0.1% -> 6.4%

第一反应通常是看 CPU 和内存。

结果服务器上的指标并不夸张:

CPU:35% 堆内存:58% Full GC:没有明显增加 机器负载:略有升高

没有 CPU 打满,也没有 OOM。

但接口就是越来越慢。

这类事故最麻烦的地方在于:

服务看起来还活着,实际上处理请求的线程正在排队等待。

如果这时候只重启应用,连接池和线程池会暂时清空,接口确实可能恢复。

但真正的问题还在。

流量一回来,故障还会再次出现。

下面按一次典型排查过程,把整条链路走一遍。


二、第一步:用 top 判断是不是机器资源问题

先登录故障机器:

top

重点看这些指标:

%Cpu(s) load average RES 进程 CPU 排名

如果 Java 进程 CPU 已经接近 100%,优先排查死循环、频繁 GC、序列化、加密计算或者热点代码。

如果 CPU 不高,但接口仍然很慢,不能得出“应用没问题”的结论。

因为线程可能正在等待:

等待数据库返回 等待远程接口返回 等待 Redis 返回 等待获取数据库连接 等待获取锁

等待中的线程不一定消耗很多 CPU。

所以这次事故里,top的作用不是直接找到根因,而是先确定排查方向:

CPU 不高,优先检查线程都在等什么。

找到 Java 进程 PID:

jps-l

假设应用 PID 是18472


三、第二步:用 top -Hp 找热点线程

继续查看 Java 进程里的线程:

top-Hp18472

如果存在持续占用 CPU 的线程,记下线程 ID。

假设线程 ID 是:

18531

Java 线程栈中的线程编号是十六进制,需要转换:

printf"%x\n"18531

输出:

4863

后面就可以在jstack中搜索:

nid=0x4863

不过这次没有发现某个线程持续吃满 CPU。

反而发现大量线程 CPU 占用很低。

这更加符合“线程阻塞等待”的特征。


四、第三步:连续抓三次 jstack

不要只抓一次线程栈。

单次线程栈只能看到一个瞬间,容易把正常的短暂等待误判成故障。

建议间隔 5 到 10 秒连续抓三次:

jstack-l18472>/tmp/jstack-1.logsleep5jstack-l18472>/tmp/jstack-2.logsleep5jstack-l18472>/tmp/jstack-3.log

如果环境里的jstack无法正常附加,也可以使用:

jcmd18472Thread.print-l>/tmp/thread.log

检查线程状态分布:

grep"java.lang.Thread.State"/tmp/jstack-1.log|sort|uniq-c

这次看到大量请求线程处于:

java.lang.Thread.State: WAITING (parking)

继续查看堆栈,出现了大量类似内容:

"http-nio-8080-exec-173" java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.parkNanos(...) at com.zaxxer.hikari.util.ConcurrentBag.borrow(...) at com.zaxxer.hikari.pool.HikariPool.getConnection(...) at com.zaxxer.hikari.HikariDataSource.getConnection(...)

这段栈已经给出了非常明确的线索:

Tomcat 请求线程不是在执行慢代码,而是在等待数据库连接。

如果三份线程栈里,同一批线程一直停留在HikariPool.getConnection(),就不是瞬时抖动了。

连接池大概率已经被占满。


五、第四步:检查数据库连接池

应用使用的是 HikariCP,可以直接查看监控指标:

hikaricp_connections_active hikaricp_connections_idle hikaricp_connections_pending hikaricp_connections_timeout_total

事故发生时的数据是:

maximumPoolSize = 50 active = 50 idle = 0 pending = 87

它表达的意思很直接:

50 个连接全部被占用 没有空闲连接 87 个请求正在排队等连接

此时把连接池从 50 调到 100,可能让接口短暂恢复。

但这通常只是把问题往数据库推。

如果某条 SQL 执行 3 秒:

50 个连接可以同时压 50 条慢 SQL 100 个连接可以同时压 100 条慢 SQL

连接池变大之后,数据库可能反而更快被拖垮。

所以连接池占满只是现象,还要继续查:

这些连接到底在执行什么?


六、第五步:从数据库找出正在执行的 SQL

先看当前数据库会话:

SHOWFULLPROCESSLIST;

也可以通过performance_schema查询:

SELECTPROCESSLIST_ID,PROCESSLIST_USER,PROCESSLIST_HOST,PROCESSLIST_DB,PROCESSLIST_TIME,PROCESSLIST_STATE,PROCESSLIST_INFOFROMperformance_schema.threadsWHERETYPE='FOREGROUND'ANDPROCESSLIST_COMMAND<>'Sleep'ORDERBYPROCESSLIST_TIMEDESC;

当时出现了大量相同 SQL:

SELECTid,order_no,user_id,status,amount,create_timeFROMt_orderWHEREuser_id=10086ANDstatus=1ORDERBYcreate_timeDESCLIMIT20;

单看 SQL 并不复杂。

但执行计划暴露了问题:

EXPLAINSELECTid,order_no,user_id,status,amount,create_timeFROMt_orderWHEREuser_id=10086ANDstatus=1ORDERBYcreate_timeDESCLIMIT20;

结果中的关键信息:

type: ALL key: NULL rows: 2865341 Extra: Using where; Using filesort

也就是说,这条看起来普通的查询做了这些事:

扫描大量订单数据 按 user_id 和 status 过滤 再按 create_time 排序 最后只返回 20 条

数据量小时,它可能只执行几十毫秒。

当订单表增长到几百万行,高峰期再被并发调用,执行时间就从几十毫秒涨到了几秒。

慢 SQL 长时间占着数据库连接不释放,最终形成完整的故障链路:

SQL 没有合适索引 ↓ 单次查询时间变长 ↓ 数据库连接长期被占用 ↓ 连接池 active 达到上限 ↓ Tomcat 线程等待连接 ↓ 请求排队,接口 RT 飙升 ↓ 上游重试,流量进一步放大

这也解释了为什么 CPU 并不高,接口却慢了 10 倍。


七、真正的修复不是重启,也不是扩大连接池

根据查询条件和排序方式,补充联合索引:

ALTERTABLEt_orderADDINDEXidx_user_status_create_time(user_id,status,create_time);

再次检查执行计划:

type: ref key: idx_user_status_create_time rows: 32 Extra: Using index condition

索引上线后,接口逐步恢复:

SQL 平均耗时:2.1s -> 18ms 连接池 active:50 -> 9 连接池 pending:87 -> 0 接口 P99:2.6s -> 240ms

这里需要注意:

联合索引不能只看这一条 SQL 就直接在生产创建。

还要评估:

表的数据量 现有索引是否重复 写入成本是否可接受 索引创建是否会影响线上业务 查询字段和排序方向 不同 user_id、status 的数据分布

MySQL 8.0 可以先确认具体版本与 DDL 能力,再选择合适的上线方式。

数据量很大的表,应该在业务低峰操作,并准备监控和回退方案。


八、为什么测试环境一直没发现?

因为测试环境通常不具备三个条件。

1. 数据量不够

测试表可能只有几千条数据。

即使全表扫描,执行也非常快。

生产表有几百万甚至上亿条数据,同一条 SQL 的成本完全不同。

2. 并发量不够

一条 SQL 执行 2 秒,不一定立刻拖垮服务。

但 50 条慢 SQL 同时执行,就可能占满连接池。

3. 数据分布不一样

测试数据往往非常均匀。

生产里可能存在大客户、热门用户和特殊状态,单个条件就能命中几十万行。

所以慢 SQL 排查不能只问:

在我本地执行快不快?

还要问:

在生产数据量、真实数据分布和高峰并发下,它会占用多少资源?


九、线上接口变慢,我一般按这个顺序查

遇到接口 RT 突然升高,可以先保存下面这份顺序。

第一步:确认影响范围

单个接口还是全部接口 单台机器还是全部实例 持续变慢还是偶发尖刺 是否刚发布或变更配置

第二步:看机器资源

top

检查 CPU、负载、内存和 Java 进程。

第三步:看线程在做什么

top-Hp<pid>jstack-l<pid>

连续抓三次,区分 CPU 热点、锁等待、连接池等待和远程调用阻塞。

第四步:看资源池

Tomcat 线程池 数据库连接池 业务线程池 HTTP 连接池 Redis 连接池

第五步:顺着等待点查下游

慢 SQL 和执行计划 远程接口 RT Redis 慢查询 消息队列积压 磁盘和网络

不要一看到接口慢就先改 JVM 参数,也不要一看到连接池满就直接扩容。

先找到线程在等待什么,排查会快很多。


十、总结

这次事故最容易误导人的地方是:

CPU 不高 内存正常 重启后能恢复

但真正的故障链路是:

一条 SQL 没有合适索引 -> 查询越来越慢 -> 数据库连接被占满 -> 请求线程排队 -> 接口整体超时

所以,线上接口变慢时,先别急着重启。

记住这条排查主线:

机器资源看方向,线程栈看等待点,连接池看拥堵,数据库看根因。


遇到线上问题,也可以来找我一起看

如果你正在处理这些问题:

Java 服务启动失败、接口超时、CPU 或内存异常 Spring Boot、MySQL、Redis、Nginx 安装与部署 Linux 环境配置、服务上线、日志和故障排查 线程池、连接池、慢 SQL、死锁等线上问题

可以关注公众号云技纵横,后台发送“排查”联系我。

为了更快判断问题,建议同时提供:

1. 出现了什么现象 2. 完整报错或脱敏日志 3. 系统、JDK 和中间件版本 4. 最近做过哪些发布或配置变更 5. 已经尝试过哪些处理方式

简单问题、基础安装和初步排查,我可以免费帮忙看一下;涉及长期运维、生产环境操作或完整项目实施,会先说明范围和风险,再确认是否需要进一步处理。

生产日志请先脱敏,不要发送密码、Token、数据库账号、服务器私钥和用户隐私数据。


下一篇继续复现:

线上频繁 Full GC,堆内存明明还有空间,为什么接口还是卡死?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/16 9:16:13

OpenCode Go接入Vscode Copilot:Go语言AI编程工作流重构指南

1. 项目概述&#xff1a;这不是“换模型”&#xff0c;而是重构本地AI编程工作流OpenCode Go 接入 Vscode Copilot——看到这个标题&#xff0c;很多人第一反应是“又一个换模型的教程”。但实际动手做过的人会立刻意识到&#xff1a;这根本不是简单改个API地址的事。它是一次对…

作者头像 李华
网站建设 2026/7/16 9:14:53

ATL的使用办法

ATL使用方法 #include <iostream> #include <atlbase.h> #include <atlwin.h>template<class T> class Edoyun { public:void show() {T* p static_cast<T*>(this);p->Name();} public:void Name() {std::cout << "Edoyun" …

作者头像 李华
网站建设 2026/7/16 9:08:18

Grok 4.3 AI助手技术解析:多模态能力与焚决功能实践指南

如果你最近在关注AI助手领域&#xff0c;可能会注意到一个有趣的现象&#xff1a;虽然市面上已经有ChatGPT、Claude、Gemini等成熟产品&#xff0c;但Grok这个名字依然频繁出现在技术讨论中。特别是当提到"免费"、"最新版本"、"功能更新"这些关键…

作者头像 李华
网站建设 2026/7/16 9:08:07

memdir:面向IDE的本地化会话状态持久化系统

1. 项目概述&#xff1a;这不是一个“增强记忆”的插件&#xff0c;而是一套可落地的会话状态持久化系统“又一款封神级插件开源&#xff01;彻底解决 Claude Code 记忆短板问题&#xff01;”——看到这个标题&#xff0c;我第一反应不是点开&#xff0c;而是把键盘敲得更响了…

作者头像 李华
网站建设 2026/7/16 9:05:48

Claude Fable在教育内容生成中的优势与实操指南

1. 先搞清楚 Claude Fable 在教育内容生成上的实际优势 Claude Fable 在教育内容生成上超越 Opus&#xff0c;这个结论不是凭空得出的&#xff0c;而是基于它在处理复杂教育场景时的几个关键能力差异。如果你在教育领域工作&#xff0c;无论是课程设计、教学材料生成还是学习路…

作者头像 李华
网站建设 2026/7/16 9:05:19

OpenClaw本地AI工作流引擎:Docker一键部署与API-Key实战指南

1. 项目概述&#xff1a;这不是“装个软件”&#xff0c;而是给你的本地AI大脑装上可编程的爪子 OpenClaw&#xff08;常被社区简称为Clawdbot&#xff09;不是另一个聊天窗口&#xff0c;它是一套面向开发者和进阶用户设计的 本地化AI工作流编排引擎 。你可以把它理解成一个…

作者头像 李华