MySQL数据库cpu飙升到500%该如何处理?

34 阅读7分钟

CPU 500%意味着MySQL吃掉了好几个核,基本上是某些SQL在疯狂消耗计算资源。这种情况下不要急着重启——重启只是把问题藏起来了,过一会儿还会炸。

先灭火,再查因,最后防复发。按这个顺序来。

灭火:先找到罪魁祸首

登上服务器,第一件事不是看MySQL,是看操作系统。

top -Hp $(pidof mysqld)

这条命令列出mysqld进程下所有线程的CPU占用。找到CPU最高的那几个线程,记下它们的LWP(轻量级进程ID)。

然后进MySQL:

SHOW PROCESSLIST;

或者更详细的版本:

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之后,如果业务允许,直接杀掉:

KILL <process_id>;

这一步的目的是止血。CPU降下来之后,你才有余裕去分析根因。如果不先杀掉问题查询,服务器可能连登录都卡。

查因:为什么这条SQL吃这么多CPU

CPU飙升的原因,90%以上是这几种情况。

全表扫描。 一条查询没走索引,扫了几百万行甚至几千万行。每一行都要从磁盘读到内存(如果Buffer Pool装不下的话),然后逐行比较WHERE条件。CPU的消耗主要在"逐行比较"这一步。

拿到问题SQL之后,用EXPLAIN看一下:

EXPLAIN SELECT ... ;

如果 type 列是 ALLrows 列是几百万,那就是全表扫描。看 key 列是不是NULL——NULL说明没用上任何索引。

锁等待引发的连锁反应。 一条慢SQL持有行锁,后面的请求全部排队等锁。等待的连接越来越多,每个连接都占一个线程,MySQL的线程调度开销就上去了。这种情况下CPU高不是因为在"计算",而是因为在"调度"。

SELECT * FROM information_schema.innodb_trx 
ORDER BY trx_started ASC;

这条命令列出所有活跃事务,按开始时间排序。跑了最久的那个事务大概率是罪魁祸首——它持有锁不释放,后面的事务全部堵住了。

SELECT * FROM performance_schema.data_lock_waits;

这条(MySQL 8.0+)能看到谁在等谁的锁。如果 BLOCKING_ENGINE_TRANSACTION_ID 指向的事务已经跑了很久,考虑杀掉它。

排序和临时表。ORDER BYGROUP BY 的查询,如果排序字段没有索引,MySQL会在内存里(或者磁盘上)建临时表做排序。数据量一大,排序本身就是CPU密集型操作。

EXPLAIN里看到 Extra 列有 Using filesortUsing temporary,就是这个情况。

大量短连接。 某些应用没用连接池,每次请求都新建MySQL连接、用完就断开。MySQL创建和销毁连接的开销不小(要做认证、分配线程、初始化会话变量)。如果QPS很高,光是连接管理就能把CPU吃满。

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 工具:

pt-online-schema-change --alter "ADD INDEX idx_user_status(user_id, status)" \
  D=mydb,t=orders --execute

这个工具的原理是创建一张新表、加上索引、通过触发器同步数据、最后原子性地rename。对线上业务的影响比直接ALTER TABLE小很多。

场景二:大事务持有锁导致连锁堵塞

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,但事务还开着),直接杀掉对应的连接:

KILL <trx_mysql_thread_id>;

然后去排查应用代码——大概率是某个地方开了事务忘记commit/rollback,或者事务里做了不该做的事(比如在事务里调了外部HTTP接口,接口超时导致事务一直挂着)。

场景三:突发流量导致CPU飙升

不是某条SQL有问题,而是正常的SQL突然来了十倍的量。这种情况加索引没用,因为每条SQL本身都很快,只是量太大了。

短期应对:

SET GLOBAL max_connections = 500;

限制最大连接数,超出的请求直接拒绝,保护MySQL不被打死。比让所有请求都卡住要好——至少一部分请求能正常处理。

中期方案:应用层加限流、加缓存。把热点查询的结果缓存到Redis,大部分请求不打到MySQL。

长期方案:读写分离,读请求分散到从库。

预防:别等CPU飙了再处理

开慢查询日志。 这是最基本的。

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;

long_query_time 设成1秒。很多人设成10秒,那等于只能抓到"已经严重影响用户体验"的查询。1秒的阈值能让你提前发现潜在问题。

监控线程状态。 定期检查活跃连接数和长时间运行的查询:

SELECT COUNT(*) FROM information_schema.processlist WHERE command != 'Sleep';

活跃连接数突然飙升,往往是CPU飙升的前兆。配合Prometheus + Grafana做监控告警,活跃连接超过阈值就报警。

定期审查慢查询。 每周跑一次 pt-query-digest,看看有没有新出现的慢查询。很多CPU飙升事故不是突然发生的,而是某条SQL随着数据量增长越来越慢,从100ms慢到1秒,从1秒慢到10秒,最后某天数据量过了临界点,直接把CPU打满。

检查Buffer Pool命中率。

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%的时候最怕的操作就是慌了直接重启,重启完发现问题还在,又开始乱改配置,越改越乱。