记得刚转岗做后端架构那会儿,生产环境里最让人心跳骤停的声响,不是报警短信的震动,而是运维群里突然甩出来的一行:“DB 连接数爆了,业务全卡死。” 那时我就像个无头苍蝇,对着满屏红色的 Grafana 面板发呆,手里攥着 SHOW PROCESSLIST 的截图,脑子里只有一个念头:这锅到底是谁的?
三年过去了,从最初对着 top 命令抓瞎,到现在能凭借几秒内的指标波动精准定位到某条执行计划畸变的 SQL,我手里攒下了一整套“排雷手册”。今天不想给你整那些教科书式的定义,就想把你拉进我那间深夜加班的办公室,聊聊我们是怎么把 Prometheus 和 Grafana 这两把手术刀,磨得越来越锋利的。
第一刀:别等炸了再修,把监控当成“生命体征”监测仪
很多团队做监控有个误区,觉得只要监控 CPU 和内存就够了。错!大错特错。 MySQL 是个多面手,它的状态远比这两个数字复杂。如果你只盯着 CPU,当业务量突然翻倍,CPU 飙到 80%,你只会觉得“哦,正常负载”;但当 CPU 是 90%,而 TPS(每秒事务处理量)却跌成了零,这时候的 CPU 可不是在干活,而是在“空转”——那通常意味着死锁等待或者大量的上下文切换。
我们团队的做法是,在 Prometheus 里建立一个分层监控体系。首先,我们要明确一点:Prometheus 自己就是第一个被监控的对象。如果 Prometheus 挂了,或者它的抓取间隔延迟了,你的监控面板就是一堆废墟。
我们引入了 mysql_exporter 作为数据源。这个小小的神器会去连接 MySQL,收集上百个关键指标。比如 mysql_global_status_Threads_connected(当前连接数)、mysql_global_status_Threads_running(正在运行的线程数)、mysql_global_status_Innodb_buffer_pool_reads(物理读次数)等等。
记得有个凌晨两点,监控面板上 mysql_global_status_Threads_connected 突然从 500 飙升到 2000,但 mysql_global_status_Threads_running 却只有 5。这就像什么呢?就像高速公路上堵了 2000 辆车,但每辆车都在原地不动。这时候如果你只看 CPU,可能还在正常区间,但你已经知道出大事了——连接池满了,新请求进不来,老请求动不了。这就是典型的“连接风暴”前兆。
为了更细致地监控,我们还配置了 my.cnf 中的慢查询日志,并配合 mysqld_exporter 开启 collect.global_status 和 collect.global_variables。这时候,Grafana 里就有了一个名为 “MySQL Overview” 的面板,它不像是一个冷冰冰的数据表,更像是一个病人的实时心电图。我们给它设定了多重阈值:
- 警告(Warning):连接数超过阈值的 70%,慢查询数每分钟超过 10 条。
- 严重(Critical):连接数超过 90%,或者有查询超过 10 秒仍未完成。
这种细粒度的阈值设定,让我们从“事后诸葛亮”变成了“事前预警机”。
第二刀:慢查询不是原罪,执行计划才是“真凶”
慢查询日志(Slow Query Log)是 MySQL 自带的“黑匣子”,记录了所有执行时间超过 long_query_time 的 SQL。很多工程师看到慢查询,第一反应是把 SQL 跑一遍,看看能不能优化索引。但这只是治标。
真正的排查高手,会看 EXPLAIN 结果。在 Grafana 里,我们没有直接展示 SQL 文本,因为那太嘈杂了。我们设计了一个专门的面板,用来展示 Top 10 慢查询的执行计划变化趋势。
这里分享一个真实的案例。去年双十一预热期间,我们的订单服务出现了明显的响应延迟。Prometheus 报警显示 CPU 使用率持续在 85% 以上波动。运维同学第一时间重启了 MySQL 实例,虽然暂时恢复了,但半小时后问题重现。
我介入后,没有急着看 SQL,而是先看了 Grafana 上的 mysql_global_status_Threads_running 曲线,发现它在剧烈震荡,峰值接近了最大连接数。接着,我查了慢查询日志,发现有一条更新订单状态的 SQL 经常上榜:
UPDATE order_table SET status = 'SHIPPED' WHERE user_id = 12345 AND create_time > '2023-10-01';
看起来很正常?我执行了 EXPLAIN,结果让我倒吸一口凉气:
id: 1
select_type: UPDATE
table: order_table
partitions: NULL
type: ALL <-- 全表扫描!
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 15000000 <-- 扫描了 1500 万行!
filtered: 1.00
Extra: Using where
这条 SQL 全表扫描了 1500 万行数据,而且没有用到任何索引。为什么?因为 user_id 和 create_time 虽然都有索引,但这条 SQL 的逻辑是“批量更新”,优化器可能认为全表扫描比回表更快(虽然这次判断错了)。更糟糕的是,由于数据量巨大,这把扫描锁住了大量的页,导致其他事务排队等待,形成了连锁反应,CPU 飙升就是因为大量的磁盘 I/O 和内存页置换。
我们当时做的紧急处理,是在 Grafana 上配置了查询规则的告警,一旦发现有全表扫描且扫描行数超过 10 万的 SQL,立即发送钉钉通知。这次事件后,我们给这张表加了复合索引 (user_id, create_time),并在应用层做了分页限制,才彻底解决了这个问题。
第三刀:CPU 飙升的迷宫,如何找到那条“隐形”的线程
CPU 高是 MySQL 最常见的病症,但也是最难诊断的。因为 CPU 高可能源于计算密集型 SQL,也可能源于锁等待,还可能源于操作系统层面的问题。
我整理了一套“三步走”排查法,这套方法在过去三年里救了我们不止一次。
第一步:确认是 SQL 问题还是系统问题。
在 Linux 服务器上,使用 top 命令。如果 wa(iowait)很高,说明是磁盘 I/O 瓶颈,MySQL 在等数据,这时候 CPU 虽然高,但瓶颈不在计算。如果 us(user space)很高,说明是 MySQL 进程在疯狂计算。我们遇到过一次,us 高达 90%,但 wa 只有 5%。这明确指向了计算密集型问题。
第二步:定位到具体的线程。
使用 pt-top 工具(Percona Toolkit 的一部分)。它能动态显示每个线程的 CPU 使用情况。想象一下,你站在一个繁忙的十字路口,看哪辆车跑得最快。pt-top 就是那个红绿灯,它能告诉你哪个线程最占 CPU。
pt-top --user root --password xxx --host 127.0.0.1 --interval 1
运行这个命令时,你会发现某个线程的 CPU 占用极高,而且一直在运行。记下这个线程 ID(比如 12345)。
第三步:关联到具体的 SQL。
拿到线程 ID 后,回到 MySQL:
SHOW FULL PROCESSLIST;
找到 ID 为 12345 的那条记录,看它的 Info 字段,那就是罪魁祸首。有时候,你会发现这个线程没有执行任何 SQL,状态是 Sleep。这怎么办?别急,继续下一步。
第四部:如果线程是 Sleep 状态且 CPU 高,检查锁等待。
使用 SHOW ENGINE INNODB STATUS\G。这个命令会输出大量的 InnoDB 引擎状态信息。重点看 LATEST DETECTED DEADLOCK 和 SEMAPHORES 部分。如果看到大量的 MySQL thread id ... block ...,说明有线程在等待锁,而等待锁的过程会消耗 CPU 资源(自旋锁)。
记得有一次,我们的 CPU 监控报警,但 pt-top 查不到明显的 SQL 线程。全是 Sleep 状态的连接。我们查了 SHOW ENGINE INNODB STATUS,发现有一张表被一个长事务锁住了,其他所有尝试修改这张表的请求都在等待,导致 CPU 空转在自旋上。最终定位到是一个老版本的报表生成任务,持有锁长达 10 分钟,才导致了这次“无源之CPU”危机。
第四刀:Grafana 可视化,让数据“说话”
工具再厉害,如果数据呈现得晦涩难懂,也是白搭。我们团队在 Grafana 上花了很多心思,打造了一个“驾驶舱”式的监控面板。
我不喜欢那种密密麻麻的表格,我喜欢趋势图和热力图。
比如,我们有一个 “Query Latency Heatmap”(查询延迟热力图)。横轴是时间,纵轴是查询耗时区间,颜色深浅代表查询次数。通过这个图,我们可以一眼看出:在某个时间点,是否有大量查询突然集中在高延迟区间。这种可视化比单纯的平均值更有说服力,因为它能揭示出“长尾效应”——95% 的查询很快,但 5% 的查询极慢,而这 5% 才是导致用户体验崩溃的元凶。
另一个神器是 “Connection Pool Saturation”(连接池饱和度)。我们监控了 Threads_connected 和 Threads_running 的比值。这个比值越高,说明连接池越紧张。我们给这个指标设定了一个动画效果:当比值超过 0.8 时,背景色从绿色渐变为黄色,超过 0.9 时变为红色,并且出现闪烁。这种视觉冲击力,能让值班人员瞬间警觉。
我们还集成了一些“故障模拟”面板。比如,我们可以手动插入一个 “Slow Query”,然后观察 Prometheus 中各项指标的变化曲线。通过这种对比,我们训练了团队对异常指标的敏感度。就像飞行员在模拟舱里训练一样,只有见过“故障”的样子,真出故障时才不会慌。
第五刀:从监控到治理,建立闭环
监控只是第一步,发现问题后如何解决,才是关键。我们建立了一套“监控-诊断-修复-复盘”的闭环机制。
监控:Prometheus 报警。 诊断:值班人员登录 Grafana,查看相关指标,定位到具体 SQL 或线程。 修复:如果是慢 SQL,优先 Kill 掉异常线程,然后优化 SQL 或索引。如果是连接数问题,扩容或调整连接池配置。 复盘:每周开会,分析这周发生的 Top 3 问题,讨论是否有更优的监控策略或预防手段。
比如,有一次我们发现某条 SQL 在特定业务高峰期频繁慢查询。经过复盘,我们发现是因为业务代码在循环中频繁查询数据库,造成了“N+1”问题。这不仅仅是 SQL 优化的问题,更是架构设计的问题。于是,我们推动了团队在代码评审(Code Review)阶段,强制要求对批量操作进行 Review,并引入了批量查询的最佳实践。
这种闭环机制,让监控不再是一个孤立的运维工作,而是成为了驱动整个研发团队提升系统质量的重要杠杆。
结语:监控是一场没有终点的修行
三年下来,我最大的感受是:监控没有最好的,只有最适合的。 Prometheus 和 Grafana 给了我们强大的工具,但工具不会自己思考。我们需要不断地观察、假设、验证、调整。
数据库卡顿和崩溃,往往是冰山一角。水面下的东西,才是真正需要我们去挖掘的。希望我的这些实战经验,能帮你在面对数据库危机时,多一份从容,少一份慌乱。
最后,送给大家一句话:“不要相信你的眼睛,要相信数据;不要依赖经验,要依赖监控。” 愿你的 MySQL,永远健康,永远优雅。
