1. 问题现象与初步判断
最近在维护线上MySQL数据库时,突然收到监控系统告警:某台主机的CPU使用率持续超过90%。登录服务器后,通过top命令确认mysqld进程确实占用了大量CPU资源。这种情况如果持续存在,轻则导致查询响应变慢,重则引发整个数据库服务不可用。
遇到这种问题时,我通常会按照"先现象后本质"的思路进行排查。首先确认CPU高负载的具体表现形态:
- 是短期尖峰还是持续高位?
- 用户线程(非系统线程)占用是否异常?
- 是否伴随其他资源(如内存、IO)的异常波动?
通过观察,发现这次的特点是:
- CPU使用率从凌晨3点开始逐步攀升
- user空间占用比例高达85%
- 内存使用量保持稳定
- 磁盘IO等待时间在正常范围内
这种特征通常指向SQL查询效率问题,而非单纯的硬件资源不足。接下来就需要深入MySQL内部寻找具体原因。
2. 排查工具与方法论
2.1 基础诊断命令
首先使用MySQL自带的诊断命令快速定位问题方向:
sql复制SHOW PROCESSLIST;
这个命令可以查看当前所有连接线程的状态。重点关注:
- State列显示"Sorting result"、"Sending data"等耗时操作
- Time列显示长时间运行的查询
- Info列展示具体的SQL语句
在我的案例中,发现有多个连接长时间处于"Sending data"状态,且执行时间都超过30秒。
2.2 性能模式(Performance Schema)
MySQL 5.6+版本提供了更强大的性能分析工具:
sql复制-- 查看哪些事件消耗最多CPU
SELECT event_name, count_star, sum_timer_wait/1000000000 as latency_sec
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE event_name LIKE 'wait/io/file/%'
ORDER BY sum_timer_wait DESC LIMIT 10;
-- 查看最耗时的SQL语句
SELECT digest_text, count_star, avg_timer_wait/1000000000 as avg_latency
FROM performance_schema.events_statements_summary_by_digest
ORDER BY avg_timer_wait DESC LIMIT 10;
2.3 慢查询日志分析
启用慢查询日志是定位性能问题的金标准:
sql复制-- 临时设置慢查询阈值(单位:秒)
SET GLOBAL long_query_time = 1;
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
-- 指定日志文件路径
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
分析慢查询日志时,我习惯使用pt-query-digest工具:
bash复制pt-query-digest /var/log/mysql/mysql-slow.log > slow_report.txt
这个工具会将相似的SQL归类统计,按响应时间排序,并给出优化建议。
3. 常见原因深度解析
3.1 索引缺失或失效
这是CPU高负载的最常见原因。当查询无法使用合适的索引时,MySQL不得不进行全表扫描,导致CPU使用率飙升。
典型症状:
- 执行计划(EXPLAIN)中type=ALL
- key_len值很小或为NULL
- rows列显示扫描行数异常高
案例重现:
sql复制-- 没有合适索引的查询
SELECT * FROM orders WHERE create_time > '2023-01-01';
-- 解决方案:添加索引
ALTER TABLE orders ADD INDEX idx_create_time(create_time);
注意:索引不是越多越好。每个额外的索引都会增加写操作的开销,需要权衡利弊。
3.2 锁竞争问题
当多个事务竞争同一资源时,会导致大量线程处于等待状态,间接推高CPU使用率。
排查方法:
sql复制-- 查看当前锁等待情况
SELECT * FROM sys.innodb_lock_waits;
-- 查看事务详情
SELECT * FROM information_schema.INNODB_TRX;
常见解决方案:
- 优化事务隔离级别
- 减少事务持续时间
- 合理设计索引减少锁范围
- 将大事务拆分为小事务
3.3 排序和临时表
当查询包含ORDER BY、GROUP BY或DISTINCT等操作时,如果无法使用索引排序,MySQL需要在内存或磁盘上创建临时表。
诊断方法:
sql复制-- 查看临时表使用情况
SHOW STATUS LIKE 'Created_tmp%';
优化方案:
- 为排序字段添加索引
- 增大tmp_table_size参数
- 避免SELECT *,只查询必要字段
- 对于复杂分组查询,考虑使用物化视图
3.4 连接池配置不当
连接池过小会导致大量连接等待,过大则会消耗过多CPU资源。
合理设置建议:
ini复制# my.cnf配置示例
[mysqld]
thread_cache_size = 16
table_open_cache = 4000
max_connections = 200
4. 系统级优化策略
4.1 参数调优
根据服务器配置调整关键参数:
ini复制# 缓冲池大小(建议为物理内存的50-75%)
innodb_buffer_pool_size = 12G
# 日志文件大小
innodb_log_file_size = 2G
# 并发线程数
innodb_thread_concurrency = 16
4.2 查询重写技巧
一些常见的查询优化模式:
sql复制-- 原查询(使用OR条件)
SELECT * FROM users WHERE age < 18 OR status = 'inactive';
-- 优化为UNION
SELECT * FROM users WHERE age < 18
UNION
SELECT * FROM users WHERE status = 'inactive';
-- 使用覆盖索引
-- 原查询
SELECT * FROM products WHERE category = 'electronics';
-- 优化查询
SELECT id, name FROM products WHERE category = 'electronics';
4.3 架构层面优化
当单机优化达到瓶颈时,需要考虑:
- 读写分离:将读请求分流到从库
- 分库分表:水平拆分大表
- 引入缓存:使用Redis缓存热点数据
- 升级硬件:使用SSD或更高配置服务器
5. 实战案例与解决方案
5.1 案例一:全表扫描导致CPU飙升
现象:
- CPU使用率持续90%+
- 大量简单查询响应变慢
排查:
sql复制EXPLAIN SELECT * FROM user_logs WHERE action_time > NOW() - INTERVAL 7 DAY;
结果显示type=ALL,扫描了1200万行数据。
解决方案:
- 为action_time字段添加索引
- 重写查询只获取必要字段
- 增加查询条件限制结果集大小
5.2 案例二:排序操作消耗大量CPU
现象:
- 高峰时段CPU使用率周期性飙升
- 慢查询日志显示大量排序操作
排查:
sql复制SHOW STATUS LIKE 'Sort%';
发现Sort_merge_passes值异常高。
解决方案:
- 增大sort_buffer_size参数
- 为ORDER BY字段添加索引
- 使用延迟关联优化:
sql复制-- 原查询
SELECT * FROM articles ORDER BY create_time DESC LIMIT 10000, 20;
-- 优化后
SELECT a.* FROM articles a
JOIN (SELECT id FROM articles ORDER BY create_time DESC LIMIT 10000, 20) b
ON a.id = b.id;
5.3 案例三:子查询导致的性能问题
现象:
- 复杂报表查询时CPU满载
- 查询执行时间超过30秒
排查:
sql复制EXPLAIN SELECT
u.name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) as order_count
FROM users u;
发现对users表的每一行都执行了子查询。
解决方案:
- 使用JOIN替代子查询:
sql复制SELECT
u.name,
COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;
- 考虑使用物化视图预计算统计结果
6. 长期监控与预防
6.1 监控指标体系建设
建议监控以下关键指标:
- CPU使用率(特别是user空间占比)
- QPS(每秒查询数)和TPS(每秒事务数)
- 慢查询数量
- 连接数使用情况
- 缓冲池命中率
6.2 自动化报警规则
配置合理的报警阈值:
- CPU使用率持续5分钟>80%
- 慢查询数量突然增加50%
- 连接数使用超过max_connections的80%
- 缓冲池命中率<95%
6.3 定期健康检查
建议每周执行以下检查:
sql复制-- 检查未使用的索引
SELECT * FROM sys.schema_unused_indexes;
-- 检查冗余索引
SELECT * FROM sys.schema_redundant_indexes;
-- 检查表碎片
SELECT table_name, data_free/1024/1024 as frag_mb
FROM information_schema.tables
WHERE data_free > 100*1024*1024;
7. 高级技巧与工具链
7.1 使用pt工具集
Percona Toolkit提供了一系列实用工具:
bash复制# 分析索引使用情况
pt-index-usage /var/log/mysql/mysql-slow.log
# 可视化展示MySQL状态
pt-mysql-summary --user=root --password=xxx
# 在线修改大表结构
pt-online-schema-change --alter "ADD INDEX idx_name (name)" D=mydb,t=mytable
7.2 性能压测方法
使用sysbench进行基准测试:
bash复制# 准备测试数据
sysbench oltp_read_write --db-driver=mysql --mysql-host=127.0.0.1 \
--mysql-port=3306 --mysql-user=root --mysql-password=xxx \
--mysql-db=sbtest --tables=10 --table-size=1000000 prepare
# 执行测试
sysbench oltp_read_write --db-driver=mysql --mysql-host=127.0.0.1 \
--threads=32 --time=300 --report-interval=10 run
7.3 火焰图分析
对于深层次的性能问题,可以使用火焰图定位热点:
bash复制# 采集性能数据
perf record -F 99 -p $(pidof mysqld) -g -- sleep 30
# 生成火焰图
perf script | stackcollapse-perf.pl | flamegraph.pl > mysql.svg
在实际工作中,我发现大多数CPU高负载问题都可以通过优化查询和索引来解决。但有时候,问题可能隐藏在更深层次的系统交互中。比如曾经遇到过一个案例,看起来是MySQL的CPU使用率高,实际却是由于Linux系统的透明大页(THP)配置不当导致的。这种情况下,就需要我们具备全栈的排查能力。