MySQL CPU 100%?10分钟定位“隐形杀手”SQL

36 阅读5分钟

55b1cfd7c15ca7fa6ba3e625fa33381af1631c9f68847f92cbf37fecd9d0a427_1766064900540.jpg

现象:监控一度吓人,QPS 反而没涨

你有没有过这种经历?

凌晨 2 点,Slack 突然弹出告警——CPU 从 30% 直线拉升到 100%,整个集群噤若寒蝉。最恐怖的是,应用的 QPS 曲线却毫无波澜,反而还在稳定运行。这一刻,你的心态就崩了:到底是哪个鬼东西在疯狂烧 CPU?是慢查询?是死锁?还是什么看不见摸不着的东西?

我来告诉你,这 90% 是某个隐藏 SQL 的全表扫描搞的鬼。关键是你得在 10 分钟内找到它,否则业务就真的凉了。

秒级定位:从 top 到线程句柄的"黑科技"链路

这里才是真正的干货,很多人都走错了方向。

第一步,top 看不出来。你打开 top 只会看到 mysqld 这个大块头在烧 CPU,但 32 个核心里到底哪个线程在捣乱?top 展示不了。

第二步,直接上 Linux 层面的线程。用这个命令:


ps -mp $(pgrep -f mysqld) -o THREAD,,tid,%cpu | sort -k5 -rn | head -20

这会给你秒级看到 TID(线程 ID)+ CPU 占用。记住烧 CPU 最猛的那个 TID,比如 12345

第三步,TID 怎么映射到 MySQL 会话? 这是关键。MySQL 8.0 的 performance_schema.threads 表里有个字段叫 THREAD_OS_ID,就是这个 Linux TID。执行:


SELECT THREAD_ID, THREAD_OS_ID, PROCESSLIST_ID, PROCESSLIST_USER, PROCESSLIST_HOST FROM performance_schema.threads  WHERE THREAD_OS_ID = 12345;

一秒钟,你就拿到了 PROCESSLIST_ID。这就是杀死进程、查 SQL 的钥匙。

这套链路速度贼快,从告警到定位可能只需要 30 秒

一键抓 SQL:看谁在偷偷扫表

拿到 PROCESSLIST_ID 后,下一步该看这个会话到底在跑什么 SQL,以及它有多疯狂。

sys.session 这个视图,你能看到连接级别的各项指标:


SELECT * FROM sys.session  WHERE PROCESSLIST_ID = 123 \G

重点关注这几列:

  • rows_examined:这个数字突然暴涨,说明在全表扫描。正常查询可能扫描几百行,坏 SQL 能扫描百万级。

  • rows_sent:返回给客户端的行数。如果 rows_examined 是 1000 万,rows_sent 只有 10 行,那就是妥妥的垃圾查询。

  • lock_time:如果这个数字也很大,说明还加锁了,那就更惨。

你能看到类似这样的数据:


rows_examined: 15000000 rows_sent: 45 lock_time: 234.12s

一眼就知道——这哥们在 1500 万行里翻江倒海,最后只要 45 行数据。这就是你的凶手!

sql

SHOW PROCESSLIST;

查看完整的 SQL 语句。如果被截断了,用:


SELECT * FROM performance_schema.events_statements_current  WHERE PROCESSLIST_ID = 123 \G

验证:执行计划暴露全表扫描的真相

找到 SQL 后,别急着改,先验证一下到底是怎么坏的。

EXPLAIN FORMAT=JSON 看详细的执行计划:


EXPLAIN FORMAT=JSON SELECT * FROM orders  WHERE created_at_str = '2025-12-18'  AND user_id = 456 \G

输出会展示:

  • 是不是全表扫描(type: ALL

  • 用没用上索引(key: NULL

  • 预期扫描多少行(rows: 500000

99% 的情况下,问题出在这两个地方:

  1. 隐式类型转换:你的列是 VARCHAR,SQL 里用的是数字。MySQL 会自动转换,导致索引失效。比如 WHERE user_id = '456' 明明 user_id 是整数,你传了个字符串进去,全表扫。

  2. 字符集/排序规则不匹配:多个表 JOIN,一个是 utf8mb4,一个是 utf8,MySQL 也会放弃索引,直接全表扫。

执行计划里会清楚地显示 Using whereUsing filesort 这样的标记,那就是你的定位点。

解决:在线加索引,秒级秒回

好消息是,如果真的就是缺索引或者字符集问题,MySQL 8.0 的在线 DDL 能救你

假设问题就是 created_at_str 这个字段没索引,执行:


ALTER TABLE orders  ADD INDEX idx_created_at_str (created_at_str), ALGORITHM=INPLACE,  LOCK=NONE;

关键是这两个参数:

  • ALGORITHM=INPLACE:直接在表上改,不用拷贝整张表。

  • LOCK=NONE:不加锁,DML 照样跑。

如果是字符集问题,需要统一字符集的索引:


ALTER TABLE orders  ADD INDEX idx_user_varchar (user_id_varchar(50)) CHARACTER SET utf8mb4, ALGORITHM=INPLACE, LOCK=NONE;

这个操作通常秒级完成,而且不会阻塞读写。CPU 立刻从 100% 掉下来,业务瞬间恢复。我亲眼见过一个 500GB 的表,加索引就花了 8 秒。

好索引救命,坏查询要命。

预防:让 CPU 告急再也不来敲门

一次两次侥幸躲过不算本事,关键是以后别再出这档子事。

第一招:打开 performance_schema

确保配置里有:


[mysqld] 
performance_schema = ON 
performance_schema_max_table_instances = 12500

这样才能用 sys.sessionperformance_schema.threads 这些救命工具。

第二招:定期做性能基线

每周或每月跑一次 sys.ps_setup_save(),把当前的 performance_schema 配置和统计信息保存下来。后面出问题时,对比基线就能看出异常来了。


CALL sys.ps_setup_save(@defaults); -- 过段时间后对比 SELECT * FROM sys.ps_setup_defaults;

第三招:主动监测慢查询

long_query_time 调到 0.1 秒,主动抓这种接近坏的 SQL,别等到烧 CPU 时才知道。


这个故事的结局是,那哥们从被 On-Call 惊醒到完全修复,也就花了 15 分钟。索引加上去,CPU 回到 30%,各个监控曲线都重新开心地跳舞。

当你有了这套定位链路和在线 DDL 这个大杀器,线上 CPU 飙升就再也不是无脑告急了。反而是个捞经验、展示操作的好机会。

下次再遇到这个问题,你就是那个 5 分钟搞定、被团队膜拜的人。

您的关注是对我写作最大的鼓励,期待和您一起深入 MySQL 坑!


声明:本文内容 90% 为本人原创,少量素材经 AI 辅助生成,且所有内容均经本人严格复核;图片素材均源自真实素材或 AI 原创。文章旨在倡导正能量,无低俗不良引导,敬请读者知悉。