CPU 500%意味着MySQL吃掉了好几个核,基本上是某些SQL在疯狂消耗计算资源。这种情况下不要急着重启------重启只是把问题藏起来了,过一会儿还会炸。
先灭火,再查因,最后防复发。按这个顺序来。
灭火:先找到罪魁祸首
登上服务器,第一件事不是看MySQL,是看操作系统。
bash
top -Hp $(pidof mysqld)
这条命令列出mysqld进程下所有线程的CPU占用。找到CPU最高的那几个线程,记下它们的LWP(轻量级进程ID)。
然后进MySQL:
sql
SHOW PROCESSLIST;
或者更详细的版本:
sql
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command != 'Sleep'
ORDER BY time DESC;
重点看 command 不是Sleep的连接------Sleep的连接是空闲的,不吃CPU。看 time 列,跑了几百秒甚至几千秒的查询大概率就是凶手。info 列显示正在执行的SQL。
找到了问题SQL之后,如果业务允许,直接杀掉:
sql
KILL <process_id>;
这一步的目的是止血。CPU降下来之后,你才有余裕去分析根因。如果不先杀掉问题查询,服务器可能连登录都卡。
查因:为什么这条SQL吃这么多CPU
CPU飙升的原因,90%以上是这几种情况。
全表扫描。 一条查询没走索引,扫了几百万行甚至几千万行。每一行都要从磁盘读到内存(如果Buffer Pool装不下的话),然后逐行比较WHERE条件。CPU的消耗主要在"逐行比较"这一步。
拿到问题SQL之后,用EXPLAIN看一下:
sql
EXPLAIN SELECT ... ;
如果 type 列是 ALL,rows 列是几百万,那就是全表扫描。看 key 列是不是NULL------NULL说明没用上任何索引。
锁等待引发的连锁反应。 一条慢SQL持有行锁,后面的请求全部排队等锁。等待的连接越来越多,每个连接都占一个线程,MySQL的线程调度开销就上去了。这种情况下CPU高不是因为在"计算",而是因为在"调度"。
sql
SELECT * FROM information_schema.innodb_trx
ORDER BY trx_started ASC;
这条命令列出所有活跃事务,按开始时间排序。跑了最久的那个事务大概率是罪魁祸首------它持有锁不释放,后面的事务全部堵住了。
sql
SELECT * FROM performance_schema.data_lock_waits;
这条(MySQL 8.0+)能看到谁在等谁的锁。如果 BLOCKING_ENGINE_TRANSACTION_ID 指向的事务已经跑了很久,考虑杀掉它。
排序和临时表。 带 ORDER BY 或 GROUP BY 的查询,如果排序字段没有索引,MySQL会在内存里(或者磁盘上)建临时表做排序。数据量一大,排序本身就是CPU密集型操作。
EXPLAIN里看到 Extra 列有 Using filesort 或 Using temporary,就是这个情况。
大量短连接。 某些应用没用连接池,每次请求都新建MySQL连接、用完就断开。MySQL创建和销毁连接的开销不小(要做认证、分配线程、初始化会话变量)。如果QPS很高,光是连接管理就能把CPU吃满。
sql
SHOW GLOBAL STATUS LIKE 'Threads_created';
SHOW GLOBAL STATUS LIKE 'Connections';
如果 Threads_created 的值很高且在快速增长,说明在频繁创建新线程。正常情况下,连接池会复用线程,Threads_created 应该增长很慢。
常见场景的具体处理
场景一:某条慢SQL导致CPU飙升
这是最常见的情况。处理步骤:
杀掉问题查询 → EXPLAIN分析 → 加索引或改写SQL → 验证。
加索引的时候注意:在生产环境给大表加索引,MySQL 5.6之前会锁表,5.6之后支持Online DDL,但仍然会消耗大量IO。如果表有几千万行,加索引可能要跑几分钟到几十分钟,期间会影响写入性能。
建议在业务低峰期操作,或者用 pt-online-schema-change 工具:
bash
pt-online-schema-change --alter "ADD INDEX idx_user_status(user_id, status)" \
D=mydb,t=orders --execute
这个工具的原理是创建一张新表、加上索引、通过触发器同步数据、最后原子性地rename。对线上业务的影响比直接ALTER TABLE小很多。
场景二:大事务持有锁导致连锁堵塞
sql
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
WHERE trx_started < NOW() - INTERVAL 60 SECOND;
找到跑了超过60秒的事务,看它在干什么。如果是一个忘记提交的事务(trx_query 为NULL说明当前没在执行SQL,但事务还开着),直接杀掉对应的连接:
sql
KILL <trx_mysql_thread_id>;
然后去排查应用代码------大概率是某个地方开了事务忘记commit/rollback,或者事务里做了不该做的事(比如在事务里调了外部HTTP接口,接口超时导致事务一直挂着)。
场景三:突发流量导致CPU飙升
不是某条SQL有问题,而是正常的SQL突然来了十倍的量。这种情况加索引没用,因为每条SQL本身都很快,只是量太大了。
短期应对:
sql
SET GLOBAL max_connections = 500;
限制最大连接数,超出的请求直接拒绝,保护MySQL不被打死。比让所有请求都卡住要好------至少一部分请求能正常处理。
中期方案:应用层加限流、加缓存。把热点查询的结果缓存到Redis,大部分请求不打到MySQL。
长期方案:读写分离,读请求分散到从库。
预防:别等CPU飙了再处理
开慢查询日志。 这是最基本的。
sql
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;
long_query_time 设成1秒。很多人设成10秒,那等于只能抓到"已经严重影响用户体验"的查询。1秒的阈值能让你提前发现潜在问题。
监控线程状态。 定期检查活跃连接数和长时间运行的查询:
sql
SELECT COUNT(*) FROM information_schema.processlist WHERE command != 'Sleep';
活跃连接数突然飙升,往往是CPU飙升的前兆。配合Prometheus + Grafana做监控告警,活跃连接超过阈值就报警。
定期审查慢查询。 每周跑一次 pt-query-digest,看看有没有新出现的慢查询。很多CPU飙升事故不是突然发生的,而是某条SQL随着数据量增长越来越慢,从100ms慢到1秒,从1秒慢到10秒,最后某天数据量过了临界点,直接把CPU打满。
检查Buffer Pool命中率。
sql
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
用 Innodb_buffer_pool_read_requests(逻辑读)和 Innodb_buffer_pool_reads(物理读,即磁盘读)算命中率:1 - 物理读/逻辑读。正常应该在99%以上。如果低于95%,说明Buffer Pool太小,大量数据要从磁盘读,CPU花在IO等待上的时间就多了。调大 innodb_buffer_pool_size,一般设成物理内存的60%-80%。
一个容易忽略的点
MySQL的CPU飙升有时候不是MySQL本身的问题。
检查一下服务器上是不是还跑着别的东西------有些运维图省事,把应用服务和MySQL部署在同一台机器上。应用服务突然吃了大量CPU,MySQL分到的CPU时间片就少了,本来100ms能跑完的查询变成了500ms,连接堆积,恶性循环。
top 命令看一眼整体CPU分布,如果mysqld不是CPU占用最高的进程,那问题可能根本不在MySQL。
还有一种情况是OOM Killer。Linux内核在内存不足的时候会杀掉占内存最多的进程,MySQL经常是第一个被杀的。杀完之后MySQL重启,Buffer Pool是冷的,所有查询都要从磁盘读数据,CPU和IO同时飙升。看 dmesg | grep -i oom 能确认是不是被OOM Killer干掉过。
线上数据库出问题的时候,最重要的不是你知道多少优化技巧,而是能不能在压力下保持冷静、按步骤排查。先止血、再查因、最后防复发------这三步的顺序不能乱。CPU飙到500%的时候最怕的操作就是慌了直接重启,重启完发现问题还在,又开始乱改配置,越改越乱。