1. 问题现象与初步判断
上周五凌晨2点,我被一阵急促的报警短信惊醒——生产环境MySQL服务器的CPU使用率突然飙升至98%。作为经历过多次数据库性能危机的DBA,我立即打开笔记本开始远程排查。这种突发的CPU高负载往往意味着严重的性能问题,如果不及时处理,轻则导致查询响应变慢,重则引发整个数据库服务崩溃。
通过SSH连接到服务器后,我首先用top命令确认了确实是mysqld进程占用了大量CPU资源。这种情况通常由以下几种原因导致:
- 存在大量高消耗的SQL查询
- 数据库出现锁争用
- 服务器资源不足
- 配置参数不合理
- 系统存在异常连接
重要提示:在开始深入排查前,建议先检查服务器基础指标(CPU核数、内存大小、磁盘IO等),排除硬件资源不足的根本性问题。我曾经遇到过因为物理服务器老化导致CPU性能下降的案例,单纯优化SQL无法根本解决问题。
2. 系统级排查与监控分析
2.1 实时性能监控
我习惯使用一套组合命令快速获取系统状态:
bash复制# 查看整体负载
uptime
# 查看CPU使用明细
mpstat -P ALL 1 5
# 查看磁盘IO情况
iostat -xm 1 5
# 查看内存使用
free -h
这次监控数据显示:
- 系统平均负载达到15.8(8核机器)
- 用户态CPU占用92%,内核态仅6%
- 磁盘IO等待时间在正常范围
- 内存仍有30%剩余
这些数据表明:CPU高使用率确实是由MySQL自身的计算操作导致,而非系统级问题。
2.2 MySQL全局状态分析
登录MySQL后,我首先查看全局状态:
sql复制SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Questions';
SHOW GLOBAL STATUS LIKE 'Slow_queries';
关键指标:
- Threads_running: 48(正常应<CPU核数×2)
- Questions: 12,345/s(明显高于平日基准值)
- Slow_queries: 1,234(5分钟内)
这证实了存在大量并发查询,且慢查询数量异常。
3. SQL层深度排查
3.1 活跃会话分析
使用以下命令查看当前执行的SQL:
sql复制SELECT * FROM information_schema.processlist
WHERE COMMAND != 'Sleep'
ORDER BY TIME DESC LIMIT 20;
发现大量相似的SELECT语句在执行,特点是:
- 都访问同一张用户订单表
- 都包含WHERE status='processing'条件
- 平均执行时间超过8秒
3.2 慢查询日志分析
启用慢查询日志(如果尚未开启):
sql复制SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';
分析日志发现:
bash复制mysqldumpslow -s t -t 20 /var/log/mysql/mysql-slow.log
排名前几的慢查询都涉及以下模式:
sql复制SELECT * FROM orders
WHERE user_id = ? AND status = 'processing'
ORDER BY create_time DESC;
3.3 执行计划解析
对问题SQL执行EXPLAIN:
sql复制EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'processing';
结果显示:
- 使用了全表扫描(type=ALL)
- 扫描行数:2,345,678
- 实际返回行数:3
显然缺少合适的索引。
4. 根本原因定位
综合以上分析,问题根源在于:
- 缺少复合索引导致全表扫描
- 现有索引只有PRIMARY KEY(id)
- 高频查询条件(user_id, status)无索引支持
- 应用层存在循环查询
- 代码显示每处理一个用户就执行一次该查询
- 高峰期用户量激增导致查询暴增
- 查询结果集过大
- 虽然WHERE条件筛选后数据量小
- 但SELECT * 导致传输大量无用字段
5. 解决方案实施
5.1 紧急缓解措施
立即通过kill命令终止最耗资源的会话:
sql复制SELECT CONCAT('KILL ',id,';')
FROM information_schema.processlist
WHERE INFO LIKE '%orders%status=processing%'
INTO OUTFILE '/tmp/kill.sql';
SOURCE /tmp/kill.sql;
设置并发连接限制:
sql复制SET GLOBAL max_connections = 200;
SET GLOBAL max_user_connections = 50;
5.2 索引优化
创建复合索引:
sql复制ALTER TABLE orders
ADD INDEX idx_user_status (user_id, status);
优化后的EXPLAIN显示:
- 使用索引范围扫描(type=range)
- 扫描行数降至15
- 执行时间从8s→0.02s
5.3 查询重构
建议应用层修改为:
- 只查询必要字段
- 使用批量查询替代循环单条查询
- 增加结果缓存
示例改造:
sql复制SELECT id, order_no, amount
FROM orders
WHERE user_id IN (?,?,?)
AND status = 'processing';
5.4 配置调优
调整关键参数:
sql复制SET GLOBAL innodb_buffer_pool_size = 12G; # 调整为物理内存的70%
SET GLOBAL innodb_io_capacity = 2000;
SET GLOBAL innodb_read_io_threads = 8;
6. 验证与监控
实施优化后:
- CPU使用率从98%降至35%
- 平均查询响应时间从1200ms降至45ms
- 并发连接数稳定在30以下
建立长期监控:
sql复制# 定期检查索引使用情况
SELECT * FROM sys.schema_unused_indexes;
# 设置性能监控仪表盘
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;
7. 预防措施与经验总结
-
索引设计原则:
- 高频查询条件必须建立索引
- 遵循最左前缀原则
- 避免过度索引影响写入性能
-
查询编写规范:
- 禁止SELECT *
- 批量操作替代循环单条
- 合理使用LIMIT
-
日常监控要点:
- 慢查询日志每日分析
- 定期检查执行计划
- 监控连接数波动
血泪教训:曾经有一次忽略了一个看似微小的全表扫描查询,结果在促销活动时导致整个数据库雪崩。现在我对任何没有索引支持的WHERE条件都保持高度警惕。
这次事件后,我整理了完整的SQL审核清单,任何上线的SQL都必须经过:
- EXPLAIN验证
- 索引覆盖检查
- 批量操作测试
- 高并发压测
这套方法在我们后续的618大促中成功保持了数据库CPU使用率稳定在60%以下。