CPU飙升排查案例

wen java案例 2

从15分钟到30秒:一次MySQL CPU飙升的实战排查全记录

目录导读

  1. 故障初现 – 生产环境CPU告警,服务响应迟缓
  2. 第一轮排查 – 系统层面与慢查询日志的初步定位
  3. 深入分析 – 通过SHOW PROCESSLISTEXPLAIN锁定嫌疑SQL
  4. 根因揭秘 – 索引失效与隐式类型转换的“组合拳”
  5. 优化方案 – 索引重构、SQL改写与参数调优
  6. 复盘与预防 – 监控体系、规范制定与压测演练
  7. 常见问题FAQ – 关于CPU飙升的5个高频疑问

故障初现

某天上午10:15,运维监控大屏突然飘红:核心订单库的CPU使用率从5%直线飙升至98%,持续超过15分钟,随之而来的是应用层超时告警,用户反馈“提交订单卡顿”,DBA群内瞬间炸锅。

CPU飙升排查案例

小知识:MySQL的CPU飙升通常由三类原因引起——①低效SQL频繁执行(占80%以上);②连接数爆满导致线程切换开销激增;③内部锁竞争(如元数据锁、行锁等待)。


第一轮排查:系统层面与慢查询日志

系统资源确认

top -Hp <mysqld_pid>   # 查看线程CPU占用

结果显示多个mysqld线程CPU占用超过80%,确认问题源于数据库内部。

慢查询日志分析

SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;  -- 记录超过1秒的SQL

查看日志后发现,一个高频出现的SQL平均执行时间3秒,被调用次数每分钟1200次

SELECT * FROM orders 
WHERE user_phone = '138****1234' 
AND order_status = 1 
ORDER BY create_time DESC 
LIMIT 20;

关键指标观察

  • Threads_running:持续在30-50之间(正常应<10)
  • Innodb_row_lock_waits:每秒钟增长数千次
  • Buffer_pool_hit_rate:98%(基本正常,排除内存问题)

深入分析:PROCESSLIST与EXPLAIN

抓取当前运行会话

SHOW FULL PROCESSLIST;

发现大量处于Sending data状态的会话,且Time列均超过3秒,执行的正是上述SQL。

执行计划解读(核心证据)

EXPLAIN SELECT * FROM orders 
WHERE user_phone = '138****1234' 
AND order_status = 1 
ORDER BY create_time DESC 
LIMIT 20;
列名 实际值 问题诊断
type ALL 全表扫描!(理想为refrange)
key NULL 未使用任何索引
rows 500万 扫描了整个订单表
Extra Using filesort 额外文件排序,性能雪上加霜

索引结构检查

SHOW INDEX FROM orders;

发现存在单列索引idx_user_phoneidx_order_status,但没有联合索引,更严重的是——user_phone字段定义为VARCHAR(20),而代码中传入的是数值类型(Java Long类型)。


根因揭秘:隐式类型转换 + 索引失效

为什么走了全表扫描?

MySQL优化器对WHERE user_phone = 138****1234(数字)与VARCHAR列比较时,会自动将列转换为数字,导致索引失效,验证如下:

EXPLAIN SELECT * FROM orders 
WHERE user_phone = 13800001234;  -- 数字类型触发隐式转换
-- type=ALL, rows=500万
EXPLAIN SELECT * FROM orders 
WHERE user_phone = '13800001234';  -- 字符串正确
-- type=ref, rows=20

额外的排序压力

即使user_phone正确走索引,ORDER BY create_time仍需对每个用户的所有订单排序,当某个大客户有数万订单时,Using filesort仍会导致性能瓶颈。

完整的性能损耗链条

  1. 全表扫描:5MB行扫描 → CPU大量用于行解析
  2. 隐式转换:每次比较都需将字符转数值 → 额外CPU计算
  3. 临时文件排序Using filesort触发磁盘临时表 → IO与CPU双重压力
  4. 高频调用:每分钟1200次 × 2.3秒 = 并发堆积

优化方案:三重组合拳

第一步:SQL改写(立即生效)

将代码中的参数类型强制为字符串:

// 错误示例(触发隐式转换)
orderMapper.selectByPhone(13800001234L);
// 正确示例
orderMapper.selectByPhone("13800001234");

同时增加覆盖索引,避免回表:

ALTER TABLE orders 
ADD INDEX idx_phone_status_time (user_phone, order_status, create_time);

第二步:SQL逻辑增强(消除filesort)

利用覆盖索引自带排序属性,去掉ORDER BY(因为联合索引已按create_time有序):

SELECT * FROM orders 
WHERE user_phone = '138****1234' 
AND order_status = 1 
LIMIT 20;  -- 联合索引天然有序,无需显式排序

第三步:参数与架构调优

  • 调整tmp_table_sizemax_heap_table_size,避免临时文件落盘
  • 对于超大客户的查询,增加分页游标机制,避免一次扫描全部订单
  • 引入Redis缓存热数据,降低数据库读压力

优化后效果对比

指标 优化前 优化后
执行时间 3秒 18毫秒
扫描行数 500万 20
CPU使用率 98% 12%
Threads_running 40+ 2

复盘与预防:构建三层防护

监控层

  • 建立CPU水位告警(阈值80%持续5分钟)
  • 新增performance_schema监控,记录高频SQL的执行计划变化
  • 设置索引失效告警:定期扫描sys.statements_with_full_table_scans

规范层

  • 代码审查强制要求:所有WHERE字段必须与表结构类型完全一致
  • 每一次新索引上线,必须通过EXPLAIN验证type级别至少为range
  • 订单表禁止无条件下SELECT *

压测层

  • 每季度进行全链路压测,模拟双11流量(QPS=5000)
  • 压测场景包含:模糊查询、多条件组合、深度分页等边界情况
  • 压测结果必须输出执行计划变更报告

常见问题FAQ

Q1:CPU飙升时,为什么不能直接kill掉SQL线程? A:kill只能终止执行中的查询,但如果是高频调用模式(如每秒钟100次),新请求会立即填补空缺,问题仍然存在,必须先定位根因并优化SQL或索引。

Q2:如何快速判断是索引问题还是锁问题? A:在SHOW PROCESSLIST中,如果大量线程是Sending dataTime长,多为索引或SQL问题;如果是Waiting for table metadata lock,则是锁冲突,也可观察Innodb_row_lock_current_waits指标。

Q3:隐式类型转换是否只发生在数值和字符串之间? A:不只,例如VARCHARDATETIME比较、CHARVARCHAR对比,都可能导致索引失效,最稳妥的方法是用EXPLAIN检查type列。

Q4:覆盖索引为什么能消除filesort? A:当索引包含所有查询字段时,索引本身就是有序的,例如(user_phone, order_status, create_time),优化器可以直接按索引顺序读取前20条记录,无需再额外排序。

Q5:优化后CPU正常,但偶尔还有轻微波动,正常吗? A:正常,MySQL的定期后台任务(如purge、统计信息更新)会短暂占用CPU,只要波动幅度在20%以内且快速回落,无需处理,可监控Threads_connected判断是否异常。


最后提醒:任何性能优化完成后,必须观察至少一个完整的业务周期(通常24小时),确认慢查询日志清零且CPU峰值显著下降,才算真正闭环,建议将本次案例沉淀为团队内部故障手册,避免同类问题二次发生。

抱歉,评论功能暂时关闭!