咱们今天不聊虚的,直接切入正题。你有没有遇到过那种时刻:业务高峰期,用户投诉网页转圈、APP加载慢,你急匆匆地跑去查服务器,CPU明明没爆满,内存也还有余量,但就是卡得像老牛拉破车?这种“幽灵般的卡顿”,往往不是资源不够,而是数据库在某个角落悄悄“叹气”了。
很多团队的做法是:出事了 -> 查慢查询日志 -> 手动分析SQL -> 优化索引 -> 上线。这套流程在低并发时还行,但在高并发场景下,慢查询日志就像是在大海捞针,而且具有滞后性。等你看到日志里那条耗时5秒的SQL时,故障可能已经持续半小时了。
所以,我们要做的,是从“事后诸葛亮”转变为“事前预警+实时透视”。今天,我就带你用 Prometheus 和 Grafana 搭建一套 MySQL 的健康诊断系统,配合慢查询日志的深度解析,彻底解决高并发下的响应延迟问题。这不仅仅是一套工具配置,更是一种运维思维的升级。
第一步:别只盯着 CPU,学会读懂 MySQL 的“体检报告”
在引入监控之前,你得先知道 MySQL 到底在忙什么。很多人以为看 SHOW PROCESSLIST 就够了,但这只是冰山一角。在高并发场景下,我们需要更细致的指标。
1.1 关键指标解读:不仅仅是 QPS
QPS(每秒查询数)和 TPS(每秒事务数)是最基础的,但它们有欺骗性。一个 QPS 很高的系统,如果大部分是简单的 SELECT *,那它可能很健康;但如果 QPS 不高,却伴随着大量的 Context Switches(上下文切换)和 Lock Waits(锁等待),那它就病得不轻。
你需要重点关注以下几个“灵魂指标”:
- Threads_running: 当前正在执行查询的线程数。如果这个值长期大于 CPU 核心数,说明数据库在疯狂切换线程,I/O 和 CPU 都在空转。
- Innodb_row_lock_waits: InnoDB 的行锁等待次数。每次等待都意味着事务在排队,这是高并发冲突的直接体现。
- Slow_queries: 慢查询数量。注意,这不是总数,而是单位时间内的增量。
- Handler_read_rnd_next: 全表扫描或索引失效导致的顺序读取次数。如果这个值飙升,说明大量 SQL 没有用到索引。
- Binlog_disk_usage: 二进制日志磁盘占用。主从复制延迟往往与此相关。
1.2 慢查询日志:被低估的金矿
慢查询日志(Slow Query Log)是 MySQL 自带的诊断工具,但它默认是关闭的,或者记录粒度太粗。在高并发下,开启全量慢查询日志会对性能产生轻微影响(主要是磁盘 I/O),所以我们必须聪明地使用它。
配置建议:
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
-- 设置慢查询阈值为 1 秒(根据实际情况调整,生产环境建议 0.5s 或更低)
SET GLOBAL long_query_time = 1;
-- 记录没有使用索引的查询(这是性能杀手)
SET GLOBAL log_queries_not_using_indexes = 'ON';
-- 将日志写入文件,方便后续分析,而不是只存在内存里
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
这里有个技巧:不要只依赖 long_query_time。有时候,一条 SQL 耗时 0.9 秒,看起来不快,但如果一秒钟执行 1000 次,它的总负载就比一条耗时 2 秒但只执行 1 次的 SQL 要大得多。因此,我们需要结合 Prometheus 的实时监控,找出那些“高频慢查询”。
第二步:搭建 Prometheus + MySQL Exporter 监控体系
光有日志不够,我们需要实时的数据流。Prometheus 是一个时序数据库,它擅长抓取和存储指标。而 MySQL Exporter 则是连接 MySQL 和 Prometheus 的桥梁。
2.1 部署 MySQL Exporter
MySQL Exporter 是一个轻量级的服务,它定期向 MySQL 发送 SHOW STATUS 和 SHOW ENGINE INNODB STATUS 等命令,并将结果暴露为 Prometheus 格式的 metrics。
Docker 部署方式(最简单):
docker run -d \
--name mysql_exporter \
--restart unless-stopped \
-p 9104:9104 \
-e DATA_SOURCE_NAME="user:password@tcp(localhost:3306)/" \
prom/mysqld-exporter
注意:你需要创建一个专门的监控用户,赋予最低权限,不要直接用 root。
CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'password';
GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO 'exporter'@'localhost';
FLUSH PRIVILEGES;
2.2 配置 Prometheus 抓取数据
编辑 prometheus.yml,添加一个新的 job:
scrape_configs:
- job_name: 'mysql'
static_configs:
- targets: ['localhost:9104']
# 增加超时时间,防止 MySQL 响应慢导致抓取失败
scrape_timeout: 10s
重启 Prometheus 后,你可以访问 http://localhost:9104/metrics 查看原始数据。你会看到成千上万行类似这样的内容:
# HELP mysqld_global_questions Total number of queries to the server.
# TYPE mysqld_global_questions counter
mysqld_global_questions 123456789
这些就是我们要分析的“原材料”。
第三步:Grafana 可视化——让数据“说话”
有了数据,接下来就是如何让这些数据变得直观。Grafana 是最佳选择。我们可以导入社区已有的 Dashboard,也可以自定义。
3.1 导入官方 Dashboard
Grafana 官方仓库有一个非常成熟的 MySQL Dashboard(ID: 7362)。导入后,你会看到一个包含 QPS、TPS、连接数、Buffer Pool 命中率等的综合视图。
但说实话,默认的 Dashboard 有点“大而全”,在高并发排查问题时,信息过载反而不好。我们需要定制化的视图。
3.2 定制高并发诊断看板
我建议创建一个名为 “MySQL High-Concurrency Diagnosis” 的新 Dashboard,包含以下几个关键面板:
面板 1:实时 QPS/TPS 趋势图
使用 rate(mysqld_global_questions[5m]) 和 rate(mysqld_global_transactions[5m])。这能帮你看出流量波峰。
面板 2:活跃连接数 vs 最大连接数
SELECT
current_connections,
max_connections,
(current_connections / max_connections) * 100 as usage_percent
FROM (
SELECT
SUM(CASE WHEN status = 'Active' THEN 1 ELSE 0 END) as current_connections,
@@max_connections as max_connections
FROM information_schema.processlist
) as t;
注意:在 Prometheus 中,我们通常使用 mysqld_global_threads_connected 和 mysqld_global_max_connections 来计算。
如果连接数使用率超过 80%,且伴随 Threads_running 升高,说明数据库即将达到瓶颈,需要立即扩容或优化。
面板 3:InnoDB 锁等待监控
这是高并发卡顿的头号嫌疑人。
mysqld_global_innodb_row_lock_waits
如果这个指标突然飙升,配合 mysqld_global_innodb_row_lock_time_avg(平均锁等待时间),你就能定位到哪些事务在互相打架。
面板 4:慢查询频率热力图
这是一个高级技巧。我们不只关心慢查询的总量,还关心它们的分布。通过 Grafana 的 Heatmap 面板,展示过去一小时慢查询的时间分布。如果发现在某几分钟内密集出现,就能关联到具体的业务操作(如定时任务、批量导入)。
第四步:深度解析——从监控报警到根因定位
当 Grafana 报警说“慢查询增多”或“锁等待升高”时,别慌。我们需要一套标准化的排查流程。
4.1 场景模拟:电商秒杀卡顿
假设你在做电商大促,突然收到报警:MySQL 响应延迟超过 2 秒,P99 延迟飙升。
步骤 1:查看 Grafana 面板
Threads_running从 10 飙升至 500。Innodb_row_lock_waits每分钟增加 10000+。QPS正常,但Slow_queries激增。
结论:这不是 CPU 瓶颈,也不是连接数瓶颈,而是行锁竞争。
步骤 2:分析慢查询日志 登录数据库,查看最近的慢查询:
SELECT * FROM orders WHERE user_id = 12345 AND status = 'pending' FOR UPDATE;
这条 SQL 使用了 FOR UPDATE,即排他锁。在高并发下,多个线程同时更新不同用户的订单,但如果索引设计不当,或者锁范围过大,就会导致严重的锁等待。
步骤 3:EXPLAIN 分析
对这条 SQL 执行 EXPLAIN:
EXPLAIN SELECT * FROM orders WHERE user_id = 12345 AND status = 'pending' FOR UPDATE;
如果发现 type 是 ALL(全表扫描)或者 key 为 NULL,说明没有走索引。即使有索引,如果 user_id 和 status 的联合索引顺序不对,也可能导致锁范围扩大。
步骤 4:解决方案
- 优化索引:确保
(user_id, status)上有联合索引。 - 缩小锁范围:避免
SELECT ... FOR UPDATE在大范围内锁定。考虑使用乐观锁(版本号机制)代替悲观锁,减少锁持有时间。 - 异步处理:对于非强一致性的状态更新,可以考虑先写 Redis 或消息队列,再异步同步到 MySQL。
4.2 代码层面的优化示例
如果确认是 SQL 问题,如何在代码层面避免?以下是一个 Java 伪代码示例,展示如何合理使用事务和锁:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 方法1:错误的做法 - 长事务 + 宽范围锁
public void updateOrderStatusWrong(Long userId, String newStatus) {
// 开启事务
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 查询所有待处理订单并加锁 - 这会锁住整个表或大量行!
List<Order> orders = orderMapper.selectPendingOrdersForUpdate(userId);
for (Order order : orders) {
order.setStatus(newStatus);
orderMapper.updateById(order);
}
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
// 方法2:正确的做法 - 精准锁 + 短事务
public void updateOrderStatusCorrect(Long orderId, String newStatus) {
// 只锁定特定的一行
int rowsAffected = orderMapper.updateStatusWithOptimisticLock(orderId, newStatus, 1);
if (rowsAffected == 0) {
throw new RuntimeException("Order updated by another process");
}
}
}
在 Mapper XML 中,使用乐观锁:
<update id="updateStatusWithOptimisticLock">
UPDATE orders
SET status = #{newStatus}, version = version + 1
WHERE id = #{orderId} AND version = #{version}
</update>
这种方法避免了 SELECT ... FOR UPDATE,在高并发下性能提升显著。
第五步:自动化与持续改进
监控不是一次性的工作。你需要建立闭环。
5.1 设置智能告警
在 Prometheus Alertmanager 中,设置分级告警:
- Warning:
rate(mysqld_global_slow_queries[5m]) > 10- 动作:发送钉钉/企业微信通知给开发团队,附带最近 5 条慢查询 SQL。
- Critical:
mysqld_global_threads_running > 100或mysql_up == 0- 动作:电话通知 DBA 和运维负责人,自动触发应急预案(如降级、限流)。
5.2 定期回顾与优化
每周花 30 分钟,导出 Prometheus 中的慢查询数据,生成周报。重点关注:
- 新增的慢查询模式。
- 被优化后再次变慢的 SQL。
- 索引使用情况:是否有从未使用的索引?
5.3 引入 pt-query-digest 进行深度分析
对于生产环境的慢查询日志,可以使用 Percona Toolkit 中的 pt-query-digest 进行分析。它能将慢查询日志按类型分组,找出 Top N 的“罪魁祸首”。
pt-query-digest /var/log/mysql/slow.log > report.txt
生成的报告不仅包含 SQL 文本,还包含执行时间分布、锁等待时间、扫描行数等详细信息,是优化 SQL 的绝佳依据。
结语:从被动救火到主动防御
回到最初的问题:如何解决高并发下的卡顿与响应延迟?
答案不是单一的。它需要一个组合拳:
- 基础层:合理的数据库设计、索引优化、SQL 规范。
- 监控层:Prometheus + MySQL Exporter 提供实时数据。
- 可视化层:Grafana 定制看板,让异常一目了然。
- 分析层:慢查询日志 + pt-query-digest,深入根因。
- 行动层:自动告警 + 代码级优化,快速响应。
这套方案的价值在于,它将“黑盒”的数据库变成了“白盒”。你不再需要猜测为什么慢,而是能看到数据告诉你哪里慢。当你能够实时看到每一行 SQL 的执行代价,每一次锁的竞争,每一次 I/O 的阻塞,你就拥有了掌控全局的能力。
记住,技术是为了服务于业务的。监控不是为了展示酷炫的图表,而是为了在用户感知到卡顿之前,我们就已经解决了问题。希望这套方案能帮助你打造出更稳健、更高效的 MySQL 数据库系统。如果有具体的报错或场景,欢迎随时交流,我们一起拆解。
