RDS慢查询把CPU打满后的排查记录

举报
yd_292017662 发表于 2026/10/20 10:36:43 2026/10/20
【摘要】 我记录了一次华为云 RDS for MySQL 夜间慢查询故障:CPU 冲到 98%,接口超时。排查里我先误判连接池,后面靠慢日志和执行计划定位到索引问题,做了两轮 DDL 与参数调整,避免了临时升配,也留下了分区和长尾 SQL 的后续任务。

周二凌晨 01:17,支付回调服务 5xx 比例从 0.2% 直接跳到 11.6%,CES 连续推了两条同类告警,都是 api-gateway p95 > 2000ms 持续 5 分钟。业务链路马上跟着抖,下游重试倍率从 1.1 涨到 2.7,Kafka 的 callback_result 主题积压到 3.4 万条,客服在 01:24 开始收“订单状态没更新”的工单。Grafana 上我又补看了两块,支付回调接口 QPS 从 320 掉到 190,超时请求数在 01:21 这个点到 1468/min,已经不是偶发抖动。

那天值班手机在 01:17 到 01:20 连着响了四次。

故障现场和我前二十分钟的误判

链路在华为云广州,应用跑在两台 ECS c6.large.2,2 vCPU/4 GiB,EulerOS 2.0 SP11,JDK 17.0.10,Spring Boot 3.2.4,mysql-connector-j 8.3.0。数据库是 RDS for MySQL 8.0.28 主备,4 vCPU/16 GiB,500 GiB 高IO,包月账单 1986 元,按小时算约 2.72 元。平时 CPU 在 25% 到 40%,出事后连续 8 分钟高于 96%,连接数从 180 冲到 512,实例上限 600,Threads_running 一度卡在 87 到 96。

[CES] rds_cpu_util: 98.21% (阈值 85%,3/3 周期)
[CES] rds_slow_queries: 463/min (阈值 50/min)
[CES] rds_active_connections: 512 (阈值 450)

我当时盯着应用日志,先入为主地以为是连接池打满。用这条命令抓最近 200 行:

grep -E 'HikariPool|CommunicationsException|SQLTransientConnectionException' /var/log/pay-callback/app.log | tail -n 200

2026-05-14 01:19:43.772 WARN 1 --- [nio-8080-exec-9] com.zaxxer.hikari.pool.HikariPool :
HikariPool-1 - Connection is not available, request timed out after 30000ms.
2026-05-14 01:19:43.775 ERROR 1 --- [nio-8080-exec-9] c.x.pay.callback.CallbackController :
com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure
2026-05-14 01:20:01.102 ERROR 1 --- [nio-8080-exec-3] o.s.jdbc.CannotGetJdbcConnectionException :
Failed to obtain JDBC Connection; nested exception is java.sql.SQLTransientConnectionException

我改了 application-prod.yml 里三处参数,改动是有记录的,不是口头“调大一点”:maximum-pool-size 40 改到 80,minimum-idle 10 改到 20,validation-timeout 5000 改到 2000,connection-timeout 维持 30000 不动。理由是我想先压缩拿连接时的排队时间,确认是否只是池子太小。发布方式是滚动重启,两台 ECS 分别重启 48 秒和 51 秒,整轮 3 分 12 秒。结果几乎没变,5xx 从 11.6% 只到 10.9%,网关 p95 还在 2.3s 以上,01:31 到 01:34 反而多出一次 12% 的毛刺。

这个方向我当时就是走偏了。

01:38 我把注意力拉回数据库,登到 RDS 执行 SHOW FULL PROCESSLIST;,一眼看到大量线程状态是 Sending data,Time 在 12 到 19 秒,SQL 文本都指向 orders 表同一条查询。再看 information_schema.innodb_trx,活跃事务并不多,说明不是锁等待主导。

Id: 392811  User: app_user  Host: 10.23.17.41:51236  db: paydb
Command: Query  Time: 17  State: Sending data
Info: SELECT id, tenant_id, status, amount, created_at FROM orders ... LIMIT 20

Id: 392845  User: app_user  Host: 10.23.17.42:48820  db: paydb
Command: Query  Time: 19  State: Sending data
Info: SELECT id, tenant_id, status, amount, created_at FROM orders ... LIMIT 20

慢日志定位、DDL执行和返工

慢日志阈值原本是 1 秒,我直接查 mysql.slow_log,5 分钟里同指纹 SQL 出现了 1321 次,最慢 18.732s,Rows_examined 1287344,返回只有 20 行。执行计划也很直白,命中 idx_tenant_id 后扫 1248731 行,再 filesort。orders 当时 1.9 亿行,tenant 90217 又是热点租户,晚高峰每分钟新增 1.2 万单,这条 SQL 在并发高的时候把 CPU 顶满。

SELECT start_time, query_time, lock_time, rows_sent, rows_examined, sql_text
FROM mysql.slow_log
WHERE start_time > NOW() - INTERVAL 10 MINUTE
ORDER BY query_time DESC
LIMIT 3;

# Query_time: 18.732418  Lock_time: 0.000231 Rows_sent: 20 Rows_examined: 1287344
SELECT id, tenant_id, status, amount, created_at
FROM orders
WHERE tenant_id = 90217
  AND status IN ('PAID','REFUNDING')
ORDER BY created_at DESC
LIMIT 20;

EXPLAIN FORMAT=TRADITIONAL ...
type: range
key: idx_tenant_id
rows: 1248731
Extra: Using where; Using filesort

01:52 我先在测试实例重放流量,规格 4 vCPU/8 GiB,MySQL 8.0.28,按量跑 6 小时花 8.52 元。流量重放不是全量抓包,我从生产提了 01:10 到 01:20 的 SQL digest,拼成 JMeter JDBC 脚本,200 线程压了 8 分钟,目标是复现 p95>2s。测试里我有一次失败尝试,想一步到位建覆盖索引,DDL 写成 idx_tenant_status_created_note (tenant_id, status, created_at DESC, note),执行直接报错:

ALTER TABLE orders
ADD INDEX idx_tenant_status_created_note (tenant_id, status, created_at DESC, note);
ERROR 1071 (42000): Specified key was too long; max key length is 3072 bytes

note 是 utf8mb4 varchar(1024)。我花了十几分钟算前缀,又试了 note(128),索引能建,但 explain 还是走 Using filesort,因为这个列既不参与过滤也不参与排序,等于白加,写入开销还会上去。我把这个方案撤了。

02:17 在生产执行真正的 DDL,只保留筛选列和排序列:

ALTER TABLE orders
ADD INDEX idx_tenant_status_created (tenant_id, status, created_at DESC),
ALGORITHM=INPLACE, LOCK=NONE;

在线 DDL 持续 11 分 43 秒。那段时间写入延迟从 7ms 抬到 35ms,binlog flush 从 4ms 到 12ms,没有触发复制延迟告警。索引生效后同 SQL 的 rows 估算降到 1836,Extra 里的 Using filesort 消失,单条 query_time 回到 40ms 到 90ms,p95 在压测里是 128ms。

参数侧我也补了一轮,这里我也踩了权限坑。

SET GLOBAL tmp_table_size=268435456;
ERROR 1227 (42000): Access denied; you need (at least one of) the SYSTEM_VARIABLES_ADMIN privilege(s) for this operation

后面改走 RDS 参数模板,tmp_table_size 和 max_heap_table_size 从 67108864 调到 268435456,long_query_time 从 1 调到 0.5,这三个是动态参数,控制台显示 2 分钟内下发完成。03:12 以后 CPU 回到 35% 到 45%,支付回调 p95 从 2.8s 降到 320ms,p99 还在 1.1s。慢查询从 463/min 降到 37/min,Kafka 积压 47 分钟清空,临时升配到 8 vCPU/32 GiB 的工单我直接取消了,按当时价格每月省 1270 元。

现在还卡着两件事,orders 这 1.9 亿行的冷热分布太偏,单一复合索引对长尾租户提升一般,我准备在影子库试按月分区并演练回滚;应用侧还有 3 条 SQL 会走临时表,query_time 600ms 到 900ms,我已经把 SQL digest、接口名、tenant_id 一起打进日志,打算先观察两周再决定要不要拆查询。

分区方案我现在还不敢直接上生产。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。