MySQL慢查询把系统拖垮后,我做了这5件事

admin 2026-07-05 05:18:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍了MySQL慢查询治理的系统方法,包括打开慢日志、定位高消耗SQL、索引和SQL改造、BufferPool和IO参数调整、建立监控与自动化回归。通过5个步骤,P99延迟从4.8s降至380ms,慢查询从日均30万降至1.5万。适合DBA和应用开发者参考。 综合评分: 85 文章分类: 实战经验,安全建设


cover_image

MySQL 慢查询把系统拖垮后,我做了这 5 件事

点击关注 👉 点击关注 👉

马哥Linux运维

2026年7月1日 18:31 广东

在小说阅读器读本章

去阅读

MySQL 慢查询把系统拖垮后,我做了这 5 件事

一、问题背景

我记得那个晚上,新订单服务上线第二天凌晨 0 点 30 分,告警群先炸出来”DB CPU 90%+”,2 分钟后打到 100%,3 分钟后调用方全量超时。值班的我连上第一台 DB 实例,SHOW PROCESSLIST 拉出来就看得呆住了:单实例上挂着 2000+ 状态为 Sending dataOpening tablesSorting resultWriting to net 的会话,EXPLAIN 几条”出问题”的 SQL,几乎每条都带 Using filesortUsing temporary,有些甚至直接出 ALL 全表扫。备份从库延迟超过 600 秒,binlog 写入打满,业务方把订单创建接口的回包时间全部超时。

事后复盘,那次故障里 MySQL 慢查询治理犯了 5 个明显错误:没有把 slow log 打开到能被业务侧使用的程度、定位慢查询纯靠猜、改造 SQL 只看 SQL 不看 Schema、把 Buffer Pool 当字段改、慢查询治理完没有自动回归。事后我们连着干了一周,按”定位 → 索引 → 改造 → 资源 → 监控预防”5 件事做下来,系统的 P99 延迟从 4.8s 降到了 380ms,慢查询数量从日均 30 万条降到 1.5 万条,DB CPU 从 90% 回到了 45%。

这篇文章就从那 5 件事开始讲,每个动作都给出”现象 → 命令 → 步骤 → 验证 → 风险 → 回滚”,版本差异(MySQL 5.7 与 MySQL 8.0)单独标出。文中所有的阈值、配置项、SQL 写法都核对过源码、官方手册与社区多年经验,不写 MySQL 不支持的语法,禁止混用 PostgreSQL/Oracle/SQL Server 的语法。

二、适用场景

本文适合以下读者和场景:

  1. MySQL 5.7 / 8.0

    维护者(单机、主从、MHA、Group Replication 都不限制,但以 InnoDB 引擎为例)。

  2. DBA 或上游应用开发者

    ,希望”在不改应用代码的情况下”治理慢查询。

  3. 刚接手了一个慢查询从没被治理过的 MySQL 实例,需要从零起步。

  4. 准备从 5.7 升级到 8.0,希望把慢查询治理一起做掉。

  5. 业务正面临规模增长,原本 1ms 的 SQL 突然到 500ms。

不适合:

  • 把 MySQL 当 KV 用的 NoSQL 场景,本文专注于 SQL 引擎下慢查询。
  • 不写 SQL 也不维护的纯 SRE,本文重点在 SQL 层和 Schema。

三、核心知识点

慢查询治理是一个分层工程。下面把它分成 7 个层面,每一层都有自己的定位和责任。

3.1 引擎层:InnoDB

  • Buffer Pool 与磁盘 IO 的边界。innodb_buffer_pool_size 是数据库性能最敏感的一档。
  • Redo Log、Undo Log、Binlog 与”事务一致性”的关系。
  • 索引 B+Tree 与 Buffer Pool 的关系:访问页 → Buffer Pool 命中 → 没有则磁盘 IO。

3.2 索引层:聚簇索引与二级索引

  • 聚簇索引决定数据物理顺序;二级索引叶子节点存的是 PK。
  • 复合索引的最左前缀、覆盖索引、索引下推 ICP(Index Condition Pushdown)。
  • 不可见索引(MySQL 8.0 INVISIBLE INDEX)、降序索引(MySQL 8.0 DESC)。
  • 函数/表达式上的”索引”(MySQL 8.0.13 后支持函数索引)。

3.3 SQL 层:解析器与优化器

  • 解析器把 SQL 拆成 AST,优化器再生成执行计划。
  • 优化器不是免费的:复杂 join、IN 子查询、SELECT *ORDER BY RAND() 等会显著拖慢优化器。
  • 优化器成本估算依赖统计信息(mysql.innodb_table_stats / mysql.innodb_index_stats),统计信息过期会让优化器做出”用错索引”的选择。

3.4 锁层:行锁、间隙锁、死锁

  • 行锁看 information_schema.innodb_trxinnodb_locksinnodb_lock_waits
  • 长事务是慢查询的最大帮凶之一:它把 undo 撑大,让 purge 跟不上,让 Buffer Pool 老数据不能淘汰。
  • 死锁由 InnoDB 自动检测,遇到错误码 1213,应用层要做事务重试。

3.5 复制层:主从同步

  • 异步复制:写入主库即可返回,从库单线程回放(5.7 默认),重做效率受限于 slave_parallel_workers

  • 半同步:rpl_semi_sync_master_wait_for_slave_count 与 rpl_semi_sync_master_timeout

  • Seconds_Behind_Master

    :注意从 MySQL 8.0.22 开始,SHOW SLAVE STATUS 输出字段已逐步弃用,开始用 SHOW REPLICA STATUS,输出字段命名也发生变化(如 Seconds_Behind_Source)。

风险提醒:5.7 与 8.0 在复制相关指令上命名差异较大:5.7 用 MASTER/SLAVE,8.0 用 SOURCE/REPLICA,旧字段保留但新建主从流程推荐新名字。

3.6 监控层:performance_schema / sys schema

  • performance_schema

    是 MySQL 5.6 起逐步引入的运行时自观测系统,5.7 起步已成熟。

  • sys

    schema 是基于 performance_schema 的”好查”视图(5.7.9 起步)。

  • 慢查询主战场:events_statements_summary_by_digest / events_statements_history_long / file_summary_by_event_name / events_waits_summary_global_by_event_name

3.7 业务层:索引心智与 SQL 心智

  • 业务方要会用最左前缀、覆盖索引;DBA 要会 EXPLAIN。
  • ORM 生成的 SQL 常常劣化:自动加 IS NOT NULL、自动加 LIMIT,复杂 where 变成 OR。
  • 应用连接池参数(如 Druid/HikariCP):maxWaitvalidationQuerypreparedStatementCacheSize,都和 SQL 不带参数执行计划稳定性有关。

四、整体排查或实施思路

把慢查询治理总结成”5 件事 + 1 个闭环”。以下是操作顺序。

4.1 5 件事概述

第 1 件:打开、收集、解析慢日志。 把 slow_query_log=ONlong_query_time=0.5(业务可接受的情况下)、log_output=FILE/TABLE 打开,用 mysqldumpslowpt-query-digest 解析。

第 2 件:定位高消耗 SQL,看 EXPLAIN,找出根因。 用 SHOW PROCESSLIST 看活跃会话、events_statements_summary_by_digest 看历史共识,对每条 SQL 都做 EXPLAIN,理解是否走索引、走了什么索引、回表/临时表/排序成本。

第 3 件:索引改造 + SQL 改造 + 锁治理。 加复合索引、覆盖索引;重写 OR 为 UNION ALL;避免 SELECT *;避免深分页;批量插入分批;事务短小可控。

第 4 件:Buffer Pool、redo log、IO 参数治理。innodb_buffer_pool_sizeinnodb_log_file_sizeinnodb_io_capacityinnodb_flush_neighborsinnodb_change_buffering

第 5 件:把治理沉淀为监控与自动化回归。 接慢查询告警、接 pt-query-digest 周报、接 Schema 审查、接 SQL Review、写回 PR Template。

4.2 治理闭环

                  +--------------------------+
                  |  1. 打开慢日志          |
                  |     slow_query_log      |
                  |     long_query_time     |
                  +-----------+--------------+
                              |
                              v
                  +--------------------------+
                  |  2. pt-query-digest 解析  |
                  |     + events_statements |
                  +-----------+--------------+
                              |
                              v
                  +--------------------------+
                  |  3. EXPLAIN 三件套       |
                  |     索引 / 临时表 / 排序 |
                  +-----------+--------------+
                              |
                              v
                  +--------------------------+
                  |  4. 索引 + SQL 改造      |
                  |     索引/重写/锁治理     |
                  +-----------+--------------+
                              |
                              v
                  +--------------------------+
                  |  5. Buffer Pool/IO 参数  |
                  |     + 重做日志/flush    |
                  +-----------+--------------+
                              |
                              v
                  +--------------------------+
                  |  6. 监控告警 + 自动化    |
                  |     PR Review/Schema    |
                  +-----------+--------------+
                              |
                              v
                  +--------------------------+
                  |  7. 持续回归与压测      |
                  |     每周慢查询报告     |
                  +--------------------------+

4.3 关键判断逻辑

每一步骤都要明确”判断逻辑”:

  • 看到 P99 突然上涨,先看 SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_wait_free' 是否非线性增长,判断是不是 Buffer Pool 不够。
  • 看 Threads_running 是否 > 50(5.7/8.0 经验值,需结合业务调整),判断是不是并发堵塞。
  • 看 Com_select / Questions 与 Innodb_rows_read 的比例,判断是不是全表扫描多。
  • 看 Created_tmp_disk_tables / Created_tmp_tables 的比例,判断是不是临时表落到磁盘。
  • 看 Innodb_log_waits 是否非线性,判断是不是 redo log 不够。

五、实战步骤(5 件事)

下面按顺序展开每一件事,每一件事都给出”命令 + 步骤 + 验证 + 风险 + 回滚”。

5.1 第 1 件:打开、收集、解析慢日志

5.1.1 打开慢日志

sql

-- 全局打开
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time  = 0.5;   -- 单位秒,业务可接受的下界
SET GLOBAL log_output      = 'FILE,TABLE';   -- 同时写文件和 mysql.slow_log 表

-- 5.7 与 8.0 差异:
-- 5.7:long_query_time 是精度 1ms 级别,min_examined_row_limit 阈值可以限制只记大查询
-- 8.0:同 5.7,并新增 log_slow_extra,可记录更多信息(如物理读、平均 IO 等)
SET GLOBAL log_slow_extra  = 'ON';   -- MySQL 8.0.14+ 才有

-- 改 my.cnf 让参数持久化
-- /etc/my.cnf
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 0.5
log_output = FILE
log_slow_extra = ON                -- 8.0.14+
log_timestamps = SYSTEM            -- 避免 UTC 困惑
min_examined_row_limit = 1000

风险提醒:

  • SET GLOBAL

    是临时生效,必须改 my.cnf 否则重启丢失

  • long_query_time

    调到 0.5s 看着不大,但高 QPS 业务下日均慢日志会达到 GB 级别,要确认磁盘余量。

  • mysql.slow_log

    (当 log_output=TABLE)默认走 CSV 引擎,不要让它膨胀,最好用 FILE 模式转 pt-query-digest

5.1.2 解析慢日志

bash

# 先确认日志存在
ls -lh /var/lib/mysql/slow.log

# 用 mysqldumpslow 排序 Top SQL
mysqldumpslow -s t -t 20 /var/lib/mysql/slow.log

# 用 pt-query-digest 详细分析
pt-query-digest --limit=200 \
                --order-by="Query_time:sum" \
                /var/lib/mysql/slow.log > /tmp/slow_report.txt

# 看报告
less /tmp/slow_report.txt

报告的结构(pt-query-digest):

  • Overall:总查询数、总时间、平均时间、P95。
  • Top N queries:每条 SQL 的总次数、平均时间、占比、锁时间、Lock time、Rows sent、Rows examined。

这一步的目的:把 SQL 按”总耗时”或”平均耗时”排序,找到 Top SQL;不要被”出现次数最多但平均很短的”骗到。

风险提醒:pt-query-digest 默认会从网络读取 SHOW PROCESSLIST,要确认能连上主库;如果要 --no-connect 仅分析日志文件,避免对生产有副作用。

5.1.3 对 Top SQL 做三步记录

bash

# 1) 抽取 SQL 指纹
pt-query-digest --print --no-tee \
                --filter='$event->{arg} =~ m/SELECT.*WHERE/s' \
                /var/lib/mysql/slow.log > /tmp/top_select.txt

# 2) 收集到仓库
mv /tmp/slow_report.txt /var/lib/mysql/digest/digest-$(date +%Y%m%d-%H%M).txt
5.1.4 验证与回滚
  • 验证

    SELECT @@slow_query_log, @@long_query_time, @@log_output, @@log_slow_extra; 应该看到自己设置的值。

  • 回滚

    :直接 SET GLOBAL slow_query_log = 'OFF';,改 my.cnf 即可。回滚后数据库正常工作,无业务影响。

5.2 第 2 件:定位高消耗 SQL,看 EXPLAIN,找出根因

5.2.1 实时观察活跃会话

sql

SHOW FULL PROCESSLIST;

或者更精确:

sql

SELECT id, user, host, db, command, time, state, info
FROM information_schema.PROCESSLIST
WHERE command != 'Sleep'
ORDER BY time DESC
LIMIT 50;

5.2.2 拉历史共识(推荐)

sql

-- MySQL 8.0 +
SELECT digest, schema_name, digest_text,
       count_star AS execs,
       sum_timer_wait/1e9 AS total_ms,
       avg_timer_wait/1e9 AS avg_ms,
       sum_lock_time/1e9 AS total_lock_ms,
       sum_rows_examined AS rows_examined,
       sum_rows_sent    AS rows_sent
FROM performance_schema.events_statements_summary_by_digest
WHERE schema_name = 'your_db'
ORDER BY sum_timer_wait DESC
LIMIT 50;
sql

-- MySQL 5.7 同上,但字段名可能略有差异,注意 sum_timer_wait 单位是皮秒

风险提醒:performance_schema.events_statements_summary_by_digest 默认是 INSTRUMENTED=YES,要确认 setup_instruments 中 statement/sql/* 全部 ENABLED。

5.2.3 EXPLAIN 三件套

sql

EXPLAIN SELECT ...
EXPLAIN FORMAT=JSON SELECT ...                -- 8.0 起的 JSON 形式
EXPLAIN FORMAT=TREE SELECT ...                -- 8.0.16+ 树形输出

最常用的列:

| 列 | 含义 | 关注点 | | — | — | — | | id | SELECT 序号 | 越大优先级越低 | | select_type | 类型 | SIMPLE/PRIMARY/SUBQUERY/UNION/DEPENDENT SUBQUERY | | table | 物理表 | 驱动表通常是 id 最小那行 | | partitions | 命中分区 | 没分区时 NULL | | type | 连接类型 | 从优到差:system/const/eq_ref/ref/range/index/ALL | | possible_keys | 可选索引 | 评估覆盖程度 | | key | 实际使用 | NULL 即坏味道 | | key_len | 索引使用长度 | 与联合索引最左前缀匹配 | | ref | 索引比较对象 | const / db.tbl.col | | rows | 扫描行数 | 估算值 | | filtered | 过滤比例 | 5.7 后才有 | | Extra | 额外信息 | 是否 using filesort、temporary、index condition pushdown |

注意:

  • EXPLAIN

    的 rows 是估算值,实际行数要看 EXPLAIN ANALYZE(8.0 起步)或 EXPLAIN FORMAT=JSON 的 query_block.iteration_cost

  • type=ALL

    全表扫描在百万级以上表里几乎肯定是问题;一千行小表有时无所谓。

  • Extra=Using filesort

    与 Using temporary 是常见慢查询特征,但不一定是死罪;要看是否频繁出现、单次是否高耗时。

5.2.4 验证与回滚

  • 验证

    :将历史共识列表与 Top SQL 列表对齐。

  • 回滚

    :本步骤是只读,无回滚概念,只是审计与决策。

5.3 第 3 件:索引改造 + SQL 改造 + 锁治理

5.3.1 索引改造

5.3.1.1 复合索引的最左前缀
sql

ALTER TABLE orders ADD INDEX idx_user_status_created(user_id, status, created_at);

注意:复合索引列顺序决定能否用上,不要把它当成”(a,c)“能用”(a,c) 和 (a) 但不能用 (a, c, b)”的线性关系去理解。建议把”区分度高、等值查询用、范围放最后”作为通用规则。

5.3.1.2 覆盖索引

让 SELECT 用到的列都在索引里,避免回表:

sql

-- 假设有 (user_id, status, amount),经常需要
SELECT user_id, status FROM orders WHERE user_id = ?;
-- 若索引已经覆盖,就不用回表看 amount 列

判断:EXPLAIN 的 Extra=Using index 是覆盖索引命中。

5.3.1.3 不可见索引(MySQL 8.0)

先建索引,标记为 INVISIBLE,让优化器不使用;验证业务不回归后再 ALTER VISIBLE:

sql

ALTER TABLE orders ADD INDEX idx_status_created(status, created_at), INVISIBLE;
ALTER TABLE orders ALTER INDEX idx_status_created VISIBLE;   -- 上线
ALTER TABLE orders ALTER INDEX idx_status_created INVISIBLE; -- 下线
5.3.1.4 降序索引(MySQL 8.0)
sql

ALTER TABLE t ADD INDEX idx_a_b(a ASC, b DESC);

8.0 才支持降序索引,5.7 不支持。优化器在 ORDER BY a ASC, b DESC 时可以直接命中。

5.3.1.5 函数索引(MySQL 8.0.13+)
sql

ALTER TABLE t ADD INDEX idx_lower_name((LOWER(name)));

注意:LOWER(name) 是表达式,索引键是 LOWER(name)查询必须保持表达式一致才能命中:

sql

SELECT * FROM t WHERE LOWER(name) = 'abc';      -- 命中
SELECT * FROM t WHERE name = 'abc';              -- 不命中
5.3.1.6 删除不再使用的索引
sql

SELECT * FROM sys.schema_unused_indexes;
SELECT * FROM sys.schema_redundant_indexes;

风险提醒:schema_unused_indexes 只看 performance_schema,对长尾索引可能误判;生产环境删除索引前要”INVISIBLE 一段时间再删”。schema_redundant_indexes 给的是”A 索引已经是 B 索引的前缀”这种;不要直接 DROP,先确认业务方不再按 A 索引的列做查询。

5.3.2 SQL 改造

5.3.2.1 避免 SELECT *
sql

SELECT a, b, c FROM orders WHERE user_id = ?;

如果表里有 TEXT/BLOB 字段,且仅为部分业务读,写具体列能省物理 IO。

5.3.2.2 OR 改 UNION ALL
sql

-- 改前
SELECT * FROM t WHERE a = 1 OR b = 2;

-- 改后
SELECT * FROM t WHERE a = 1 UNION ALL SELECT * FROM t WHERE b = 2;
5.3.2.3 深分页优化
sql

-- 改前
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;

-- 改后(让排序成本降到只 seek 一次)
SELECT&nbsp;*&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;id&nbsp;>&nbsp;<last_seen_id>&nbsp;ORDER&nbsp;BY&nbsp;id LIMIT&nbsp;20;

或者延迟关联:

sql

SELECT&nbsp;*&nbsp;FROM&nbsp;orders o
INNER&nbsp;JOIN&nbsp;(SELECT&nbsp;id&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;...&nbsp;ORDER&nbsp;BY&nbsp;id LIMIT&nbsp;100000,&nbsp;20)&nbsp;AS&nbsp;ids
ON&nbsp;o.id&nbsp;=&nbsp;ids.id;
5.3.2.4 IN 子查询转 JOIN
sql

-- 改前
SELECT&nbsp;*&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;user_id&nbsp;IN&nbsp;(SELECT&nbsp;id&nbsp;FROM&nbsp;users&nbsp;WHERE&nbsp;level&nbsp;=&nbsp;'vip');

-- 改后
SELECT&nbsp;orders.*&nbsp;FROM&nbsp;orders&nbsp;INNER&nbsp;JOIN&nbsp;users&nbsp;ON&nbsp;orders.user_id&nbsp;=&nbsp;users.id
WHERE&nbsp;users.level&nbsp;=&nbsp;'vip';
5.3.2.5 关联字段类型一致
sql

-- 关联字段一个是 varchar(10),另一个是 varchar(20),结果就是关联不上索引
-- 关联前要 DDL 统一
5.3.2.6 批量插入分批
sql

-- 一万行的 INSERT 拆成 100 行 * 100 批
INSERT INTO&nbsp;orders (...)&nbsp;VALUES&nbsp;(...), (...), ...;
5.3.2.7 Limit 0 实际查询审计
sql

SELECT&nbsp;*&nbsp;FROM&nbsp;t&nbsp;WHERE&nbsp;...&nbsp;ORDER&nbsp;BY&nbsp;RAND() LIMIT&nbsp;10;

ORDER BY RAND() 在 100 万行表上是 O(N log N) 排序,不要用。

5.3.3 锁治理

5.3.3.1 避免长事务
sql

-- 长事务是 undo 撑大、purge 缓慢、Buffer Pool 老数据不能淘汰的元凶
-- 经验值:单事务 < 5 万行 / < 5 秒(视业务而定)
SELECT&nbsp;*&nbsp;FROM&nbsp;information_schema.innodb_trx&nbsp;ORDER&nbsp;BY&nbsp;trx_started&nbsp;ASC;
5.3.3.2 死锁处理
sql

SHOW&nbsp;ENGINE INNODB STATUS\G

取 LATEST DETECTED DEADLOCK 段,按应用层做事务重试策略(应用层捕获 SQLSTATE 40001 / errno 1213 / 1205)。

5.3.3.3 索引失效的坑
  • 字段上有函数:WHERE LOWER(name) = 'abc'
  • 隐式转换:WHERE id = '123',其中 id 是 BIGINT,无法命中。
  • 字符串加 = 比较但左边是数值:触发隐式转换。
  • 不等于 != 和 NOT IN 在某些版本上不走索引(多数版本还行)。

5.3.4 验证与回滚

  • 验证

    :EXPLAIN 对比改造前后。

  • 回滚

  • 索引改造:ALTER TABLE t DROP INDEX idx_...

  • SQL 改造:上线前代码 review / feature flag 控制;回滚由应用版本控制。

风险提醒:

  • ALTER TABLE ADD INDEX

    在 5.7 / 8.0 上是”在线”或”原地”操作,但仍会短期持有 MDL 写锁;大表建议用 pt-online-schema-change 或 gh-ost

  • 跨表的索引改造要按业务低峰期执行。

5.4 第 4 件:Buffer Pool、Redo、IO 参数治理

5.4.1 Buffer Pool

ini

# /etc/my.cnf
[mysqld]
innodb_buffer_pool_size&nbsp; &nbsp; &nbsp; &nbsp; = 物理内存的&nbsp;60%-70%(单机独占时)
innodb_buffer_pool_instances&nbsp; =&nbsp;8&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-- 把 buffer pool 拆为多个 instance,减少锁争用
innodb_buffer_pool_chunk_size&nbsp;=&nbsp;128M &nbsp; &nbsp; --&nbsp;8.0&nbsp;起步;动态调节内存

风险提醒:Buffer Pool 实例数 / 大小一旦修改,需要重启生效。从 8.0.3 起步,innodb_buffer_pool_size 是动态可调的,但只能增加;调整时 observer 影响小。 在线调节:

SET GLOBAL innodb_buffer_pool_size = 8 * 1024 * 1024 * 1024; &nbsp; -- 8GB

注意只有按 chunk_size 整数倍调节才会生效。

5.4.1.1 Buffer Pool 状态观察
sql

SHOW&nbsp;ENGINE INNODB STATUS\G

BUFFER POOL AND MEMORY 段:当前 Buffer Pool 使用率、Pages made young / Pages made not youngPages read / Pages written。如果 Pages made young 接近 Pages made not young,Buffer Pool 命中率高。

sql

SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Innodb_buffer_pool_read%';
  • Innodb_buffer_pool_read_requests

    :请求数。

  • Innodb_buffer_pool_reads

    :磁盘读取次数。

disk_reads / total_requests 越小越好,行业经验是命中率 > 99% 算健康。

5.4.2 Redo Log

ini

innodb_log_file_size&nbsp; &nbsp; =&nbsp;4G &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -- 单个 redo 文件大小
innodb_log_files_in_group&nbsp;=&nbsp;4&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-- redo 文件个数
innodb_log_buffer_size&nbsp; =&nbsp;64M
innodb_flush_log_at_trx_commit&nbsp;=&nbsp;1&nbsp; &nbsp; &nbsp;--&nbsp;1:每次提交 flush;2:每秒 flush;0:1&nbsp;秒 flush 一次

风险提醒:

  • innodb_flush_log_at_trx_commit = 2

    提升性能,但崩溃后可能丢 1 秒数据。

  • innodb_log_file_size

    修改要重启(删除旧 redo 文件 + 新建)。

5.4.3 IO 容量与刷盘行为

ini

innodb_io_capacity&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;2000
innodb_io_capacity_max&nbsp; &nbsp; &nbsp; =&nbsp;4000
innodb_flush_neighbors&nbsp; &nbsp; &nbsp; =&nbsp;1
innodb_flush_method&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= O_DIRECT &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-- Linux 文件系统 O_DIRECT
innodb_change_buffering&nbsp; &nbsp; &nbsp;= all &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; --&nbsp;8.0&nbsp;默认 all
innodb_change_buffer_max_size&nbsp;=&nbsp;25

innodb_io_capacity 依硬件调整:SSD 大约 2000~4000;机械盘大约 200~600。低于实际能力时 InnoDB 会推后刷盘;过高时刷盘过激。

5.4.4 binlog 与磁盘

ini

log_bin&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= /var/lib/mysql/binlog/mysql-bin
expire_logs_days&nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;7
max_binlog_size&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;1G
binlog_format&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= ROW &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-- 默认 ROW,性能差但安全
sync_binlog&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;1&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; -- 写完 binlog 即刷盘

风险提醒:sync_binlog = 1 对吞吐有 10%~30% 影响,丢失概率低;sync_binlog = 0 性能好但是崩了可能丢数据。生产强烈推荐 1,除非接受丢失部分事务。

5.4.5 5.7 与 8.0 重要参数差异

| 参数 | 5.7 | 8.0 | | — | — | — | | 缓冲池动态调整 | 部分 | 完整 | | innodb_log_writer_threads | 不支持 | 8.0.11+ 支持多日志写线程 | | innodb_deadlock_detect | 默认开启 | 默认开启,可关闭让应用处理 | | innodb_online_alter_log_max_size | 上限 1GB | 8.0 提升可更大 | | innodb_print_ddl_logs | 不支持 | 8.0.21+ 支持 | | innodb_idle_flush_pct | 默认 100 | 8.0 默认 100 |

5.4.6 验证与回滚

  • 验证

    SHOW GLOBAL VARIABLES LIKE 'innodb_%' 对比前后;SHOW ENGINE INNODB STATUS\G 看 Buffer Pool 命中率、Log Sequence Number 增速。

  • 回滚

    :保留 my.cnf.bak,重启回到旧值。

5.5 第 5 件:把治理沉淀为监控告警与持续回归

5.5.1 慢查询监控告警

sql

-- 在 sys schema 里直接拿到 Top 50 慢查询 hash
SELECT&nbsp;*&nbsp;FROM&nbsp;sys.statement_analysis&nbsp;ORDER&nbsp;BY&nbsp;total_latency&nbsp;DESC&nbsp;LIMIT&nbsp;50;

通过 mysql_exporter + Prometheus + Alertmanager:

yaml

# /etc/prometheus/rules/mysql.yaml
groups:
&nbsp;&nbsp;-&nbsp;name:&nbsp;mysql.slow
&nbsp; &nbsp;&nbsp;rules:
&nbsp; &nbsp; &nbsp;&nbsp;-&nbsp;alert:&nbsp;MysqlSlowQueriesRising
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;expr:&nbsp;increase(mysql_global_status_slow_queries_total[5m])&nbsp;>&nbsp;100
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for:&nbsp;2m
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;labels:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;severity:&nbsp;warning
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;annotations:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;summary:&nbsp;"Slow queries growing"
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;description:&nbsp;"Slow query count increased by 100 in last 5 min."

风险提醒:阈值要”基于业务基线”,不要硬编码成 100;夜间业务低谷、压测、业务上线都可能触发。

5.5.2 慢查询报告周报化

bash

#!/usr/bin/env bash
set&nbsp;-euo pipefail
LOG_DIR=/var/lib/mysql/slow
OUT_DIR=/var/lib/mysql/digest
mkdir&nbsp;-p&nbsp;"$OUT_DIR"

for&nbsp;f&nbsp;in&nbsp;$LOG_DIR/slow.log.$(date&nbsp;-d&nbsp;'-7 days'&nbsp;+%Y%m%d);&nbsp;do
&nbsp; &nbsp; pt-query-digest --limit=200&nbsp;"$f"&nbsp;>&nbsp;"$OUT_DIR/digest-$(basename $f).txt"
done

把每周的报告归档到 WIKI 或对象存储。

5.5.3 SQL Review 与 Schema Review PR Template

text

# SQL / Schema Review Template

## Review Checklist
- [ ] EXPLAIN 前后对比已附上
- [ ] `Using filesort` / `Using temporary` 已规避或已说明
- [ ] `SELECT *` 不使用
- [ ] 关联字段类型一致
- [ ] 索引设计给覆盖索引或最左前缀
- [ ] 锁等待/长事务处理
- [ ] 灰度回滚路径明确

## Performance Baseline
- 改造前 P99: xxx ms
- 改造后 P99: yyy ms

## Rollback
- SQL: revert commit
- 索引: ALTER TABLE xxx DROP INDEX xxx

5.5.4 治理的回归测试

bash

#!/usr/bin/env bash
# sysbench-prep.sh:准备一个能压出来"原始健康基线"的库
sysbench --db-driver=mysql --mysql-host=127.0.0.1 \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--mysql-port=3306 --mysql-user=root \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--mysql-password=$MYSQL_PASS&nbsp;--mysql-db=sbtest \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--table-size=2000000 \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--tables=10 oltp_read_write prepare

sysbench --db-driver=mysql --mysql-host=127.0.0.1 \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--mysql-port=3306 --mysql-user=root \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--mysql-password=$MYSQL_PASS&nbsp;--mysql-db=sbtest \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--time=120 --threads=64 \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--report-interval=5 oltp_read_write run > /tmp/sysbench.txt

风险提醒:sysbench 写入数据量大,要提前确认磁盘余量;正式回归前要保证数据已被切到独立 schema,不能搞坏生产。

5.5.5 验证与回滚

  • 验证

    :监控曲线对比、SQL Review 覆盖率、A/B 验证。

  • 回滚

    :监控基线变更要配 version,回滚到上一个基线值。

六、常用命令

下面是文中频繁用到的命令(直接 copy 即用)。

6.1 慢查询相关

sql

-- 全局 / 会话参数
SHOW&nbsp;VARIABLES&nbsp;LIKE&nbsp;'slow_query_log';
SHOW&nbsp;VARIABLES&nbsp;LIKE&nbsp;'long_query_time';
SHOW&nbsp;VARIABLES&nbsp;LIKE&nbsp;'log_output';
SHOW&nbsp;VARIABLES&nbsp;LIKE&nbsp;'log_slow_extra';

-- 设置
SET&nbsp;GLOBAL&nbsp;slow_query_log&nbsp;=&nbsp;'ON';
SET&nbsp;GLOBAL&nbsp;long_query_time&nbsp;=&nbsp;0.5;

-- 看 PROCESSLIST
SELECT&nbsp;id,&nbsp;user, host, db, command,&nbsp;time, state, info
FROM&nbsp;information_schema.PROCESSLIST
WHERE&nbsp;command&nbsp;!=&nbsp;'Sleep';

-- 历史共识(8.0)
SELECT&nbsp;digest, schema_name, digest_text,
&nbsp; &nbsp; &nbsp; &nbsp;count_star&nbsp;AS&nbsp;execs,
&nbsp; &nbsp; &nbsp; &nbsp;sum_timer_wait/1e9&nbsp;AS&nbsp;total_ms,
&nbsp; &nbsp; &nbsp; &nbsp;avg_timer_wait/1e9&nbsp;AS&nbsp;avg_ms,
&nbsp; &nbsp; &nbsp; &nbsp;sum_lock_time/1e9&nbsp;AS&nbsp;total_lock_ms,
&nbsp; &nbsp; &nbsp; &nbsp;sum_rows_examined, sum_rows_sent
FROM&nbsp;performance_schema.events_statements_summary_by_digest
ORDER&nbsp;BY&nbsp;sum_timer_wait&nbsp;DESC&nbsp;LIMIT&nbsp;50;

-- sys 视图:Top SQL
SELECT&nbsp;*&nbsp;FROM&nbsp;sys.statement_analysis&nbsp;ORDER&nbsp;BY&nbsp;total_latency&nbsp;DESC&nbsp;LIMIT&nbsp;50;
SELECT&nbsp;*&nbsp;FROM&nbsp;sys.statements_with_runtimes_in_95th_percentile;
SELECT&nbsp;*&nbsp;FROM&nbsp;sys.statements_with_sorting;
SELECT&nbsp;*&nbsp;FROM&nbsp;sys.statements_with_temp_tables;

6.2 EXPLAIN / EXPLAIN ANALYZE

sql

EXPLAIN&nbsp;SELECT&nbsp;...;
EXPLAIN FORMAT=JSON&nbsp;SELECT&nbsp;...; &nbsp; &nbsp;&nbsp;-- 8.0
EXPLAIN FORMAT=TREE&nbsp;SELECT&nbsp;...; &nbsp; &nbsp;&nbsp;-- 8.0.16+
EXPLAIN ANALYZE&nbsp;SELECT&nbsp;...; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 8.0.18+,需要 optimizer trace ENABLED

6.3 索引与表

sql

-- 创建索引
ALTER TABLE&nbsp;t&nbsp;ADD&nbsp;INDEX idx_a(a);
ALTER TABLE&nbsp;t&nbsp;ADD&nbsp;UNIQUE&nbsp;KEY uniq_b(b);

-- 8.0 特性
ALTER TABLE&nbsp;t&nbsp;ADD&nbsp;INDEX idx_a_b(a&nbsp;ASC, b&nbsp;DESC), INVISIBLE;
ALTER TABLE&nbsp;t&nbsp;ADD&nbsp;INDEX idx_lname((LOWER(name)));

-- 看索引是否生效
SHOW&nbsp;INDEX&nbsp;FROM&nbsp;t;
SELECT&nbsp;*&nbsp;FROM&nbsp;sys.schema_unused_indexes;
SELECT&nbsp;*&nbsp;FROM&nbsp;sys.schema_redundant_indexes;

-- 看统计信息
SELECT&nbsp;*&nbsp;FROM&nbsp;mysql.innodb_table_stats&nbsp;WHERE&nbsp;table_name&nbsp;=&nbsp;'t';
SELECT&nbsp;*&nbsp;FROM&nbsp;mysql.innodb_index_stats&nbsp;WHERE&nbsp;table_name&nbsp;=&nbsp;'t';
ANALYZE&nbsp;TABLE&nbsp;t; &nbsp; &nbsp; &nbsp;&nbsp;-- 重算统计

风险提醒:ANALYZE TABLE 是只读元数据更新,不会锁全表,但会短时间持有 MDL 读锁。

6.4 锁与事务

sql

-- 当前活跃事务
SELECT&nbsp;trx_id, trx_state, trx_started, trx_query, trx_rows_locked, trx_rows_modified
FROM&nbsp;information_schema.innodb_trx
ORDER&nbsp;BY&nbsp;trx_started&nbsp;ASC;

-- 看锁等待
SELECT&nbsp;*&nbsp;FROM&nbsp;sys.innodb_lock_waits;
SELECT&nbsp;*&nbsp;FROM&nbsp;performance_schema.data_locks; &nbsp; &nbsp; &nbsp; &nbsp;-- 8.0+
SELECT&nbsp;*&nbsp;FROM&nbsp;performance_schema.data_lock_waits; &nbsp;&nbsp;-- 8.0+

-- KILL 句柄(生产慎用,参考风险提醒)
SELECT&nbsp;id,&nbsp;time, info&nbsp;FROM&nbsp;information_schema.PROCESSLIST&nbsp;WHERE&nbsp;time&nbsp;>&nbsp;60;
KILL&nbsp;<id>; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;-- MySQL 客户端命令,非 SQL

风险提醒:KILL 一个大事务会让它回滚并产生大量 undo log;杀前先评估回滚时间。生产推荐”先 KILL QUERY <id> 终止当前 SQL”,让事务自然提交或回滚。

6.5 性能指标拉取

sql

SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Innodb_buffer_pool_read%';
SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Innodb_log_waits';
SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Threads_running';
SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Created_tmp%';
SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Slow_queries';
SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Com_select';
SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Innodb_rows_read';
SHOW&nbsp;ENGINE INNODB STATUS\G

6.6 复制相关

sql

-- 5.7
SHOW&nbsp;SLAVE STATUS\G

-- 8.0.22+
SHOW&nbsp;REPLICA STATUS\G

-- 主从切换(生产慎用)
STOP SLAVE; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 5.7
STOP REPLICA; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;-- 8.0+
CHANGE MASTER&nbsp;TO&nbsp;MASTER_HOST='10.0.0.x'; &nbsp;-- 5.7
CHANGE REPLICATION SOURCE&nbsp;TO&nbsp;SOURCE_HOST='10.0.0.x'; &nbsp;-- 8.0+
START&nbsp;REPLICA;

风险提醒:主从切换有数据一致性风险,需要前置 FLUSH TABLES WITH READ LOCK、确认复制延迟为 0、确认 Relay_Log_File 和 Exec_Master_Log_Pos

6.7 pt 工具集常用命令

bash

# 慢查询分析
pt-query-digest /var/lib/mysql/slow.log > slow_report.txt

# online DDL
pt-online-schema-change --alter&nbsp;"ADD INDEX idx_a(a)"&nbsp;D=mydb,t=t --execute

# DDL 前后 diff(5.7 / 8.0 通用)
pt-table-checksums h=master,u=root,p=$P,H=replica

# 主从同步延迟
pt-heartbeat --master h=master,u=root --replica h=replica,u=root --interval=1 --run-time=10

风险提醒:pt-online-schema-change 会触发 trigger,会对某些业务行为产生微妙影响;停用 trigger 前要做完整测试。

6.8 常用 EXPLAIN JSON 输出解读

json

{
&nbsp;&nbsp;"query_block":&nbsp;{
&nbsp; &nbsp;&nbsp;"select_id":&nbsp;1,
&nbsp; &nbsp;&nbsp;"cost_info":&nbsp;{&nbsp;"query_cost":&nbsp;"5.12"&nbsp;},
&nbsp; &nbsp;&nbsp;"table":&nbsp;{
&nbsp; &nbsp; &nbsp;&nbsp;"table_name":&nbsp;"t",
&nbsp; &nbsp; &nbsp;&nbsp;"access_type":&nbsp;"range",
&nbsp; &nbsp; &nbsp;&nbsp;"possible_keys":&nbsp;["idx_a"],
&nbsp; &nbsp; &nbsp;&nbsp;"key":&nbsp;"idx_a",
&nbsp; &nbsp; &nbsp;&nbsp;"rows_examined_per_scan":&nbsp;100,
&nbsp; &nbsp; &nbsp;&nbsp;"rows_produced_per_join":&nbsp;95,
&nbsp; &nbsp; &nbsp;&nbsp;"filtered":&nbsp;"95.00",
&nbsp; &nbsp; &nbsp;&nbsp;"cost_info":&nbsp;{&nbsp;"read_cost":&nbsp;"0.50",&nbsp;"eval_cost":&nbsp;"9.50",&nbsp;"prefix_cost":&nbsp;"5.00"&nbsp;}
&nbsp; &nbsp;&nbsp;}
&nbsp;&nbsp;}
}

字段含义:

  • access_type

    : 连接类型。

  • rows_examined_per_scan

    : 估算扫描行数。

  • filtered

    : 估算过滤后剩余比例。

  • cost_info.eval_cost

    : 优化器对扫描后处理的成本估算。

  • prefix_cost

    : 累计成本(含本表)。

判断逻辑:

  • 如果 cost_info.eval_cost / cost_info.prefix_cost 很大,说明扫描不是瓶颈;瓶颈在扫描后处理。
  • 如果 access_type=ALL 但 rows_examined_per_scan 很小,是小表——可以接受。
  • 如果 access_type=range 但 key 是 NULL,可以建议补索引。

七、配置示例

7.1 一个”基线” my.cnf(5.7 / 8.0 通用)

ini

# /etc/my.cnf

[client]
default-character-set&nbsp;= utf8mb4

[mysqld]
user&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = mysql
port&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;3306
datadir&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= /var/lib/mysql
socket&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = /var/lib/mysql/mysql.sock
character-set-server&nbsp; &nbsp; = utf8mb4
collation-server&nbsp; &nbsp; &nbsp; &nbsp; = utf8mb4_unicode_ci
default-storage-engine&nbsp; = InnoDB
skip-name-resolve
performance-schema&nbsp; &nbsp; &nbsp; =&nbsp;ON

# 慢查询
slow_query_log&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;1
slow_query_log_file&nbsp; &nbsp; &nbsp;= /var/lib/mysql/slow.log
long_query_time&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;0.5
log_output&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = FILE
log_timestamps&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = SYSTEM
min_examined_row_limit&nbsp; =&nbsp;1000

# 连接
max_connections&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;2000
max_user_connections&nbsp; &nbsp; =&nbsp;1000
wait_timeout&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;600
interactive_timeout&nbsp; &nbsp; &nbsp;=&nbsp;600

# 缓冲池
innodb_buffer_pool_size&nbsp; &nbsp; &nbsp; &nbsp; = 物理内存的&nbsp;60%
innodb_buffer_pool_instances&nbsp; =&nbsp;8
innodb_buffer_pool_chunk_size&nbsp;=&nbsp;128M

# Redo
innodb_log_file_size&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;4G
innodb_log_files_in_group&nbsp; &nbsp; &nbsp; =&nbsp;4
innodb_log_buffer_size&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;64M
innodb_flush_log_at_trx_commit&nbsp;=&nbsp;1

# IO
innodb_io_capacity&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;2000
innodb_io_capacity_max&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;4000
innodb_flush_neighbors&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;1
innodb_flush_method&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = O_DIRECT
innodb_change_buffering&nbsp; &nbsp; &nbsp; &nbsp; = all

# binlog
log_bin&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = /var/lib/mysql/binlog/mysql-bin
binlog_format&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; = ROW
max_binlog_size&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;1G
expire_logs_days&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;=&nbsp;7
sync_binlog&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;1

# 复制(5.7)
server-id&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;1
binlog-row-image&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= FULL
gtid-mode&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; =&nbsp;ON
enforce-gtid-consistency&nbsp;=&nbsp;ON
slave_parallel_workers&nbsp; &nbsp;=&nbsp;8
slave_parallel_type&nbsp; &nbsp; &nbsp; = LOGICAL_CLOCK &nbsp; --&nbsp;5.7&nbsp;引入
# 8.0 是 source/replica 命名,下面给 8.0 兼容写法
master_info_repository&nbsp; &nbsp;= TABLE
relay_log_info_repository&nbsp;= TABLE

7.2 GTID 主从

sql

CHANGE MASTER&nbsp;TO
&nbsp; MASTER_HOST='10.0.0.21',
&nbsp; MASTER_USER='repl',
&nbsp; MASTER_PASSWORD='***', &nbsp; &nbsp;&nbsp;-- 密码要走密钥系统
&nbsp; MASTER_PORT=3306,
&nbsp; MASTER_AUTO_POSITION=1;
START&nbsp;SLAVE;

-- 8.0.22+
CHANGE REPLICATION SOURCE&nbsp;TO
&nbsp; SOURCE_HOST='10.0.0.21',
&nbsp; SOURCE_USER='repl',
&nbsp; SOURCE_PASSWORD='***',
&nbsp; SOURCE_PORT=3306,
&nbsp; SOURCE_AUTO_POSITION=1;
START&nbsp;REPLICA;

7.3 在线 DDL(MySQL 8.0)

sql

ALTER TABLE&nbsp;t&nbsp;ADD&nbsp;INDEX idx_a(a), ALGORITHM=INPLACE, LOCK=NONE;

参数含义:

  • ALGORITHM=INPLACE

    :原地算法,避免拷贝表。

  • LOCK=NONE

    :在 ALTER 期间允许读写;对大表推荐。

风险与回滚:

  • 真要”拷表”的 ALTER(如修改列类型)即使是 INPLACE,也会做表的临时拷贝,会撑爆磁盘,生产推荐用 gh-ost 或 pt-online-schema-change
  • 回滚:保留 ALTER 反向语句,下一个变更窗口执行。

7.4 Online DDL(pt-osc)

bash

pt-online-schema-change \
&nbsp; --alter&nbsp;"ADD INDEX idx_a(a)"&nbsp;\
&nbsp; --alter-foreign-keys-method auto \
&nbsp; --chunk-size 1000 \
&nbsp; --max-load Threads_running=50 \
&nbsp; --critical-load Threads_running=100 \
&nbsp; --set-vars wait_timeout=10000 \
&nbsp; D=db,t=t \
&nbsp; --execute

参数含义:

  • --chunk-size

    :一次操作多少行。

  • --max-load

    :运营阈值,达到则暂停。

  • --critical-load

    :超过立即停止。

  • --set-vars

    :按会话级别临时设置参数。

风险提醒:

  • pt-online-schema-change

    会创建 trigger,存在一段时间;某些 8.0 ROW 格式复制下 trigger 会让 GTID 出现一些副作用,需要 --no-drop-old-table 等选项特殊处理。

  • --critical-load

    阈值要”基于业务基线”,否则一压测就被感知为”停止”。

7.5 连接池配置(应用侧示例)

properties

# HikariCP
spring.datasource.hikari.maximum-pool-size=64
spring.datasource.hikari.minimum-idle=8
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000

# Druid
druid.initial-size=8
druid.min-idle=8
druid.max-active=64
druid.max-wait=30000
druid.pool-prepared-statements=true
druid.max-pool-prepared-statement-per-connection-size=20

连接数配置原则:

  • DB 端 max_connections 设置合理,避免连接风暴把 DB 打满。
  • 应用连接池大小按”DB max_connections / 应用实例数 / 2~4“为基线,再压测找最优值。
  • 启用连接超时(wait_timeoutsocket_timeout),防止沉睡连接占用。

风险提醒:连接池设太大反而会引起问题——DB 被连接数打满,应用线程撞墙,体验变差。

八、日志或指标观察方法

8.1 慢查询日志的读取位置

bash

# 通用读
less /var/lib/mysql/slow.log

# 按时间过滤
awk&nbsp;'/^# Time:/ {keep = ($0 ~ "2024-06-15/")?1:0} keep {print}'&nbsp;/var/lib/mysql/slow.log

# 找 TOP 慢查询指纹
grep&nbsp;"Query_time:"&nbsp;/var/lib/mysql/slow.log |&nbsp;sort&nbsp;-rn |&nbsp;head

8.2 performance_schema 关键事件表

| 表 | 用途 | | — | — | | events_statements_current | 实时当前 SQL | | events_statements_history | 最近执行(按 thread) | | events_statements_history_long | 最近执行(实例级,FIFO) | | events_statements_summary_by_digest | 按 digest 汇总 | | events_waits_summary_global_by_event_name | IO 等待事件总览 | | file_summary_by_event_name | 文件级 IO | | table_io_waits_summary_by_table | 表级 IO | | events_transactions_current | 实时当前事务 |

打开相关 instruments / consumers:

sql

-- 持续打开
UPDATE&nbsp;performance_schema.setup_instruments&nbsp;SET&nbsp;enabled&nbsp;=&nbsp;'YES', timed&nbsp;=&nbsp;'YES'
&nbsp;&nbsp;WHERE&nbsp;name&nbsp;LIKE&nbsp;'statement/sql/%';

UPDATE&nbsp;performance_schema.setup_consumers&nbsp;SET&nbsp;enabled&nbsp;=&nbsp;'YES'
&nbsp;&nbsp;WHERE&nbsp;name&nbsp;LIKE&nbsp;'events_statements_%';

风险提醒:performance_schema 自身消耗,按执行频率与事务粒度调整。建议默认打开,若发现重启后内存增长过快,调整 max_digest_lengthperformance_schema_max_digest_length 等。

8.3 sys schema 视图速查

| 视图 | 用途 | | — | — | | sys.host_summary | 按 host 汇总 | | sys.io_by_latency | 慢 IO 文件排名 | | sys.io_global_by_wait_by_latency | 全局 IO 类型排名 | | sys.memory_by_thread_by_current_bytes | 各 thread 占用 | | sys.statement_analysis | Top SQL | | sys.statements_with_runtimes_in_95th_percentile | 95th 慢 SQL | | sys.statements_with_sorting | 用到 filesort 的 SQL | | sys.statements_with_temp_tables | 用到临时表的 SQL | | sys.schema_unused_indexes | 未使用的索引 | | sys.schema_redundant_indexes | 冗余索引 | | sys.innodb_lock_waits | 锁等待 | | sys.schema_tables_with_full_table_scans | 走全表扫的表 |

8.4 关键监控指标

promql

# QPS
mysql_global_status_questions_total

# 慢查询增长
increase(mysql_global_status_slow_queries_total[5m])

# Buffer Pool 命中率
1 - (mysql_global_status_innodb_buffer_pool_reads_total /
&nbsp; &nbsp; &nbsp;mysql_global_status_innodb_buffer_pool_read_requests_total)

# 长事务
mysql_info_schema_threads_seconds_total

# 锁等待
mysql_global_status_innodb_row_lock_waits_total

# 复制延迟
mysql_slave_status_seconds_behind_master

# 主从 5.7 / 8.0 兼容
mysql_slave_status_seconds_behind_source &nbsp; # 8.0

8.5 我的观察习惯

  1. SHOW GLOBAL STATUS LIKE 'Slow_queries';

    5 秒一次取一次,看增速。

  2. SHOW ENGINE INNODB STATUS\G

    每次重大变化前后取一次,做 diff。

  3. 慢日志每天归档

    ,按业务日清空、当月可查。

  4. MySQL 主库必有主从延迟主面板

    Seconds_Behind_Master(或 Seconds_Behind_Source,8.0)。

  5. 所有 SSQL 改动都看 events_statements_summary_by_digest

    改造前后的 count_staravg_timer_wait

九、排查路径

下面是一份”慢查询出现时怎么排查”的标准路径。

9.1 故障信号分类

  • Threads_running

    大量增加 → 高并发导致。

  • Threads_connected

    接近 max_connections → 连接风暴。

  • Innodb_log_waits

    非线性增长 → redo log 配置不足。

  • Innodb_buffer_pool_wait_free

    增长 → Buffer Pool 不足。

  • Created_tmp_disk_tables / Created_tmp_tables

    比例高 → 临时表落到磁盘。

  • Slow_queries

    增长 → 触发慢查询告警。

9.2 高效排查流

  1. 确认业务现象

    :调用方 P99 上升 / 错误码。

  2. 拉集群状态

    SHOW GLOBAL STATUS LIKE 'Threads%'Innodb_buffer_pool_wait_free

  3. 拉活跃会话

    SELECT * FROM information_schema.PROCESSLIST WHERE command != 'Sleep' ORDER BY time DESC LIMIT 50;

  4. 拉锁等待

    SELECT * FROM sys.innodb_lock_waits; 与 SELECT * FROM sys.io_global_by_wait_by_latency LIMIT 10;

  5. 拉共识 SQL

    SELECT * FROM sys.statement_analysis LIMIT 50; 看 Top SQL。

  6. 对 Top SQL 做 EXPLAIN

  7. 看慢日志 fingerprint

    pt-query-digest /var/lib/mysql/slow.log | head -200

  8. 看主从延迟 / 异常

    SHOW SLAVE STATUS\G / SHOW REPLICA STATUS\G

  9. 看磁盘 / IO

    iostat -xz 1dstat

  10. 快速缓解

    KILL QUERY <id>(不 KILL 连接);调大 Buffer Pool;抓 Schema 改造。

9.3 内存与 CPU 维度

bash

top -bn1 -p $(pgrep -d, mysql)
ps -o pid,nlwp,pcpu,pmem,etime -p $(pgrep mysql)
bash

# 看 IO 行为
iostat -xz 1
vmstat 1
sar -n DEV 1

9.4 长事务专项排查

sql

SELECT&nbsp;trx_id, trx_state, trx_started, trx_query, trx_rows_locked, trx_rows_modified
FROM&nbsp;information_schema.innodb_trx
WHERE&nbsp;TIMESTAMPDIFF(MINUTE, trx_started, NOW())&nbsp;>&nbsp;5
ORDER&nbsp;BY&nbsp;trx_started&nbsp;ASC;

按 trx_query 看具体 SQL 与 trx_rows_locked,如果 trx_rows_locked 巨大且 trx_state 是 RUNNING,基本可以确认业务方写了一段长事务。

处理建议:通知业务方 + 应用层捕获 KILL 信号前的回滚判断。不要直接 KILL 事务,避免”回滚风暴”。

9.5 真实排查路径示例

故障:凌晨 2 点订单服务 P99 上升。

  1. SHOW PROCESSLIST

    看到 200+ 条 UPDATE orders SET ... 状态 Sleeping 但有事务,疑似长事务。

  2. SELECT trx_id, trx_started FROM information_schema.innodb_trx;

    看到 5 个事务 > 30 分钟。

  3. 业务侧查日志:for update 之后代码 Thread.sleep(1000) 再做 IO。

  4. 业务侧修改:移除 sleep,改为异步。

  5. KILL <trx_mysql_thread_id>

    业务侧先回滚事务。

  6. 长事务删除后 P99 立即降下。

  7. 写一份事故复盘,沉淀到 PR Template。

十、风险提醒

下面列出 12 个最常见的”治理错或者治理过猛”风险。

10.1 KILL <id> 而不是 KILL QUERY <id>

  • KILL <id>

    终止连接,可能让事务无法回滚完成。

  • KILL QUERY <id>

    仅终止当前 SQL,让事务自然提交或回滚。生产推荐。

10.2 OPTIMIZE TABLE 反噬

  • OPTIMIZE TABLE t

    实际是 ALTER TABLE 的某种形式,会拷贝表;大表上很慢。

  • 5.7 与 8.0 默认对 InnoDB 表 OPTIMIZE TABLE 退化为 ALTER TABLE ... FORCE,对应 ALGORITHM=COPY。

  • 推荐做法:定期做 ALTER TABLE t ENGINE=InnoDB; 来”重建表”,同样效果但控制更细致;或交给 pt-online-schema-change。

10.3 大 ALTER TABLE ADD INDEX

  • 单次 ADD INDEX 是 INPLACE,5.7/8.0 默认不会锁整表写,但仍受限于 redo log、临时空间。
  • 推荐 pt-osc 或 gh-ost,对应”撑爆磁盘””主从延迟”在白天发生概率高。

10.4 大 SET GLOBAL ... 在线参数调整

  • SET GLOBAL innodb_buffer_pool_size = 24*1024*1024*1024;

    看似 5 秒调整到位,但实际按 chunk 推进。

  • 注意 IO 抖动:调整过程会重建 LRU 数据。

  • 回滚:再次 SET GLOBAL 调到原值。

10.5 关闭二进制日志再切回

  • SET sql_log_bin = 0

    会让当前会话的 SQL 不写 binlog,仅管理员账户能用;临时做回滚 DDL 时不要让普通开发账户碰。

  • 切回前必须显示 SET sql_log_bin = 1,否则业务代码可能会再次使用 0。

10.6 直接 DROP TABLE

  • 没有备份/没有审计/没有回滚脚本。

  • DROP TABLE

    是 DDL,不可逆;先 ALTER TABLE t RENAME TO t_20240615

10.7 修改主键 / 修改列类型

  • 8.0 已经能做部分 ALTER INPLACE,但有些列类型改变(比如 VARCHAR(64) → VARCHAR(256))仍可能 ALGORITHM=COPY。
  • 生产前必须 SHOW CREATE TABLE t\GEXPLAIN ALTER TABLE ...

10.8 在线开启 general_log

  • 5.7 默认 general_log=OFF;在线打开会写所有 SQL 到磁盘,IO 立刻打满。
  • 调试时用临时会话打开:
sql

SET&nbsp;SESSION general_log&nbsp;=&nbsp;'ON';
SET&nbsp;SESSION log_output&nbsp;=&nbsp;'TABLE';

打开后关闭。

10.9 binlog_format = MIXED 隐性坑

  • 5.7 默认 ROW,但早期 5.6 部署的实例可能是 MIXED。
  • ROW 格式下 binlog size 大,主从重放压力大,需要更宽的 redo 和磁盘。

10.10 optimizer_search_depth

  • 优化器在 join 重排时搜索深度的参数。值过大时优化器耗时长,planning 出现慢。
  • 5.7 默认 62;8.0 默认 62。可以下调到 10~20 看是否缓解某些 join planning 慢。

10.11 启用 SQL 缓存(query_cache_type)

  • 5.7 之前官方默认建议关闭。
  • 5.7 起 query_cache_type 默认 0,8.0 已彻底移除。
  • 5.6 / 早期 5.7 实例升级到 8.0 后还要清掉 query_cache_size

10.12 大事务回滚

  • 业务杀事务后立刻 abort,但回滚本身要 undo,把 short-lived 长事务拆成短事务是根治之道。

十一、验证方式

每件事都要做验证,验证的标准也要写明。

11.1 慢查询治理验证

  • 指标

    :慢查询数量、Buffer Pool 命中率、Threads_running P99、Com_select 与 Innodb_rows_read 比例、Created_tmp_disk_tables / Created_tmp_tables

  • 持续时间

    :至少 7 天,覆盖业务周一到周日。

  • 样本

    :每次对比时取 24h 窗口均值,避免抖动。

11.2 索引治理验证

  • EXPLAIN

    :每条改造过的 SQL 都有 EXPLAIN 前后对比。

  • 回归测试

    :业务侧使用 sqlmock + 真实数据集覆盖率 100%。

  • 压测

    :sysbench / 业务脚本的 P99 都回到原基线 + 业务提升。

11.3 资源参数治理验证

  • SHOW GLOBAL STATUS

    :指标走势连续观测 7 天。

  • iostat

    :IO 利用率比之前平缓。

  • 错误日志

    InnoDB: page_cleaner 等信息不再出现。

11.4 监控告警验证

  • 告警回放

    :在测试环境模拟慢查询爆发,看 Prometheus / Alertmanager 是否按预期告警。

  • 覆盖率

    :监控项与 DBA 自动化脚本交叉验证。

11.5 治理后业务侧验证

  • 调用方接口

    :P99 / P95 / ErrorRate 三指标。

  • DB CPU / QPS

    :DB CPU 至少下降 30%(视业务而定)。

  • 慢查询数量

    :下降到基线的 50%。

十二、回滚方案

每件事的治理都要有回滚手段,下面按动作列。

12.1 慢日志相关回滚

sql

-- 关闭慢日志
SET&nbsp;GLOBAL&nbsp;slow_query_log&nbsp;=&nbsp;'OFF';

-- 修改 /etc/my.cnf 恢复到原来的 long_query_time、log_output
systemctl restart mysqld

风险提醒:重启会让 MySQL 重启,会有短暂的不可用时间窗。

12.2 索引与 Schema 回滚

sql

-- 删除刚刚加的索引
ALTER TABLE&nbsp;t&nbsp;DROP&nbsp;INDEX idx_x;

-- 把列回退
ALTER TABLE&nbsp;t&nbsp;DROP&nbsp;COLUMN&nbsp;c;

业务代码同样要在分支上保留回滚代码。

12.3 SQL 改造回滚

  • 由应用层做版本回滚(代码或 feature flag)。
  • 数据未变,不会影响数据。

12.4 参数治理回滚

bash

cp&nbsp;/etc/my.cnf.bak /etc/my.cnf
systemctl restart mysqld

或者在线回滚:

sql

SET&nbsp;GLOBAL&nbsp;innodb_buffer_pool_size&nbsp;=&nbsp;<原值>;
SET&nbsp;GLOBAL&nbsp;innodb_io_capacity&nbsp;=&nbsp;<原值>;

风险提醒:部分参数在线修改是只读的(SET GLOBAL 改不了),必须重启。

12.5 监控告警回滚

  • 把上一版的 yml 文件通过 git revert。
  • 通过 prometheus reload 生效。

12.6 业务回归回滚

  • 通过业务代码回滚版本。
  • 保留 ALTER 前后的 DDL 字符串,避免 offline 后忘记重做。

十三、生产环境注意事项

13.1 不要随便改 sql_mode

sql_mode 决定 SQL 的严格性;改一次会让看似正常的 SQL 行为变化。建议先空跑的语句级别用 SET sql_mode = '<具体模式>' 验证。

13.2 升级与跨版本变更

  • 5.7 → 8.0 升级要参考官方”升级检查指南”:

  • 跑 mysql_upgrade

  • 注意 residual privileges(5.7 → 8.0 会有 schema 移除)。

  • 注意保留密码字段的认证方式(5.7 → 8.0 推荐 caching_sha2_password)。

  • 8.0 → 8.4 等小版本升级要先看 release notes。

13.3 严格区分 DDL 与 DML 操作窗口

  • DDL:业务低峰(凌晨 1~5 点)或用 gh-ost 白天做。
  • DML 变更:所有 ALTER 通过 PR,权限到团队级。

13.4 性能回归测试

  • 任何对 1000 万行以上的表做 DDL 后必须跑性能回归。
  • 性能测试脚本、流量 replay 与环境一致。

13.5 灾备与多版本并发

  • 准备 secondary 实例用于 schema 差异验证。
  • 生产做一次 DDL 必须经过 dev / staging 实例的对照测试。

13.6 备份

  • 慢查询治理前的数据要保留 7~30 天的物理备份 + binlog:
  bash

  innobackupex --user=root --password=$PASS&nbsp;/data/backup/xb_$(date&nbsp;+%Y%m%d)
  • 备份本身要 smoke tested:定期 mysqlbackup --prepare 验证。

13.7 Binlog 与审计

  • 长期保留 binlog 7~30 天,配合审计。
  • binlog 切割点:保留 SHOW MASTER STATUS / SHOW BINARY LOG STATUS 的当前点。
  • 8.0.22+ 使用 SHOW BINARY LOG STATUS

13.8 临时表空间管理

  • innodb_temp_tablespaces_dir

    默认在 datadir 下。

  • 出现 /tmp 临时表很多,可能是 GROUP BY/HAVING/ORDER BY RAND 之类的复杂 SQL。

  • 8.0 增加了 TempTable 引擎做优化。

13.9 连接数与连接池监控

  • Threads_connected

    接近 max_connections 时,提前预警。

  • 应用连接池 keepalive 与 DB wait_timeout 要协调,建议 DB 端设大(默认 28800s),连接池 idle timeout 设小。

13.10 GC 回收与磁盘

  • 大表的 ALTER TABLE ... ENGINE=InnoDB 在 5.7 默认会拷贝表到 tmpdir,tmpdir 要大。

  • tmpdir

    在 /tmp 是不够的。

  • innodb_temp_tablespaces_dir

    也要分独立分区。

十四、5.7 与 8.0 的关键参数与命令差异表

下面把 5.7 与 8.0 在慢查询治理中真正用到的命令差异整理成表,便于升级路径对照。

14.1 复制 / binlog 相关

| 5.7 | 8.0 | | — | — | | SHOW MASTER STATUS | SHOW BINARY LOG STATUS (部分版本兼容) | | SHOW SLAVE STATUS | SHOW REPLICA STATUS (8.0.22+) | | CHANGE MASTER TO | CHANGE REPLICATION SOURCE TO (8.0.22+) | | START SLAVE | START REPLICA | | STOP SLAVE | STOP REPLICA | | Relay_Master_Log_File | Relay_Source_Log_File | | Seconds_Behind_Master | Seconds_Behind_Source | | Master_Log_File | Source_Log_File | | Master_UUID | Source_UUID | | Replicate_Wild_Do_Table | Replicate_Wild_Do_Table (不变) |

风险提醒:升级路径上,8.0 仍然兼容 5.7 命令,但行为会带”warning”日志;新建集群推荐用 8.0 命名(SOURCE/REPLICA)。

14.2 性能视图差异

| 5.7 | 8.0 | | — | — | | information_schema.innodb_locks | 移除;改用 performance_schema.data_locks | | information_schema.innodb_lock_waits | 移除;改用 performance_schema.data_lock_waits | | events_statements_summary_by_digest | 同名,更高采样精度 | | sys.statement_analysis | 同名 | | performance_schema.events_stages_history | 增加 stages 阶段信息 | | EXPLAIN | EXPLAIN FORMAT=JSONEXPLAIN FORMAT=TREEEXPLAIN ANALYZE | | optimizer_trace | 增强 | | OPTIMIZER_TRACE_MAX_LEN | 默认 size 调整 |

14.3 索引与 DDL 差异

| 5.7 | 8.0 | | — | — | | 索引默认 VISIBLE | 可用 INVISIBLE | | 不支持降序索引 | 支持 DESC 索引 | | 不支持函数索引 | 函数索引 | | 不支持 hash join(默认) | 默认开启 hash join | | 不支持 hash 索引适配 | 适配增强 | | ALGORITHM=INPLACE 部分 ALTER 受限 | 范围更宽 |

14.4 安全与认证差异

| 5.7 | 8.0 | | — | — | | mysql_native_password 默认 | caching_sha2_password 默认 | | ALTER USER ... IDENTIFIED WITH ... 兼容 | 推荐 caching_sha2_password | | validate_password_* 插件可选 | 开箱即用 |

十五、慢查询治理的工业级案例

下面给出 4 个在生产里跑过的真实场景,每个都是”做完事后看明白为什么”的样本。

15.1 案例一:SELECT * 与隐式转换

现象:移动端订单列表 P99 上升到 800ms。

SQL

sql

SELECT&nbsp;*&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;user_id&nbsp;=&nbsp;'1024';

问题点:

  1. user_id

    是 BIGINT,字符串常量触发隐式转换。

  2. SELECT *

    把 body 这种 TEXT 都拉了出来。

修复

sql

SELECT&nbsp;id, status, created_at, amount&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;user_id&nbsp;=&nbsp;1024;

验证

sql

EXPLAIN&nbsp;SELECT&nbsp;id, status, created_at, amount&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;user_id&nbsp;=&nbsp;1024;

改造前 Extra 可能含 Using where,改造后会直接命中 PRIMARY,ref=const

回滚:上线控制使用开关,业务放量没问题后立即 SELECT * 替换。

15.2 案例二:OR 转 UNION ALL

现象:复杂搜索接口 P99 1.5s。

SQL

sql

SELECT&nbsp;*&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;user_id&nbsp;=&nbsp;1024&nbsp;OR&nbsp;created_at&nbsp;BETWEEN&nbsp;'2024-06-01'&nbsp;AND&nbsp;'2024-06-15';

修复

sql

(SELECT&nbsp;*&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;user_id&nbsp;=&nbsp;1024)
UNION&nbsp;ALL
(SELECT&nbsp;*&nbsp;FROM&nbsp;orders&nbsp;WHERE&nbsp;created_at&nbsp;BETWEEN&nbsp;'2024-06-01'&nbsp;AND&nbsp;'2024-06-15');

验证:EXPLAIN 显示两个独立索引范围扫描,最终合并。

回滚:应用代码按版本切换。

15.3 案例三:长事务回滚风暴

现象:上游应用写了一段 for update ... Thread.sleep(...) 然后 HTTP I/O。KILL QUERY 之后长事务让 DB Threads_running 上升。

根因:应用代码失误。

修复

  • 把 I/O 拆到事务外。
  • 在代码上增加 ThreadLocal<TransactionBoundary>
  • 加测试用例,验证事务与 HTTP 调用不交叉。

验证

sql

SELECT&nbsp;trx_id, TIMESTAMPDIFF(MINUTE, trx_started, NOW())&nbsp;AS&nbsp;age_min
FROM&nbsp;information_schema.innodb_trx
ORDER&nbsp;BY&nbsp;age_min&nbsp;DESC;

业务侧观察 0 条 > 5 分钟。

回滚:应用代码回滚。

15.4 案例四:redo log 撑爆

现象:DB CPU 不高但写入吞吐抖,redo log 每秒 wait 高。

修复

  • 把 innodb_log_file_size 从 2G 调到 4G

  • 把 innodb_log_files_in_group 从 2 调到 4,做成 4 × 4G = 16GB

  • innodb_flush_log_at_trx_commit = 1

    保持不变。

验证

sql

SHOW&nbsp;GLOBAL&nbsp;STATUS&nbsp;LIKE&nbsp;'Innodb_log%';

Innodb_log_waits 应从非零回落为 0。

回滚:保留旧的 my.cnf,重启。

十六、慢查询治理复盘与周报模板

16.1 周报模板

text

# 慢查询治理周报 YYYY-MM-DD ~ YYYY-MM-DD

## 1. Top 5 SQL
| 排名 | SQL 指纹 | 总耗时(ms) | 次数 | 平均耗时(ms) | 优化状态 |
| --- | --- | --- | --- | --- | --- |
| 1 | SELECT orders ... WHERE ... | xxx | xxx | xx | 已加索引 |
| 2 | UPDATE ... | ... | ... | ... | 改造中 |

## 2. 关键指标趋势
- Slow_queries: x → y, 趋势 ↓
- Buffer Pool 命中率: x%, 趋势 →
- Threads_running P99: x, 趋势 ↓
- 复制延迟 P99: x 秒, 趋势 →

## 3. 已完成动作
- 新增索引: ...
- 改造 SQL: ...
- 调整参数: ...

## 4. 待办
- ...

## 5. 风险点
- ...

16.2 复盘文档结构

text

# 复盘:标题

## 时间
## 现象
## 影响范围
## 根因分析
&nbsp; ## 直接原因
&nbsp; ## 间接原因
## 处理过程
## 修复
## 验证
## 回滚
## 行动项
&nbsp; ## 短期
&nbsp; ## 中期
&nbsp; ## 长期

16.3 常见慢查询根因类型

  • 应用侧:索引缺失、SQL 写法、OR 写错、In 子查询、深分页、SELECT *
  • Schema 侧:列类型不匹配、冗余/缺失索引、统计信息过期。
  • 配置侧:Buffer Pool 不足、redo log 不足、IO 容量小、连接池过小/过大。
  • 资源侧:CPU 争抢、IO 抖动、网络抖动、磁盘满、连接满。
  • 复制侧:主从延迟、binlog 阻塞、Relay 错位。
  • 监控侧:阈值错误、告警缺失、告警疲劳。

16.4 沟通模板

text

# 业务方需要做的事

应用版本: x.x
DB 影响: 慢查询 30 万/天 → 1.5 万/天
修改内容: 应用层 SQL
执行窗口: 凌晨 0-3 点
灰度比例: 5% → 25% → 50% → 100%
回滚方式: 应用层版本回滚
紧急联系人: ...

十七、长期演进

17.1 从慢查询治理到 SLO 治理

把”慢查询治理”抽象成”SLO 治理”:

  • SLO:99% 调用 P99 < 200ms,DB CPU < 60%。
  • 监控:调用方耗时直方图;DB CPU / QPS。
  • 告警:5 分钟 P99 > 300ms。
  • 容量预估:模型驱动。

17.2 从人工到平台化

把慢查询治理做成基础设施:

  • SQL 自动审计(CI 阶段 EXPLAIN);
  • Schema 自动审核(PT-OSC 工作流化);
  • 慢查询报告自动生成(cron + pt-query-digest);
  • 告警与 SLO 自动关联(Prometheus + Alertmanager)。

17.3 SRE 视角的治理节奏

  • 每日:当日慢查询摘要 + SLO 趋势。
  • 每周:周报 + Top 10 SQL 跟踪表。
  • 每月:根因类比(按”应用侧、Schema、配置、资源”分类)。
  • 每季:参数基线审视 + 容量评估。

17.4 工具链最佳组合

  • 慢查询采集:slow_query_log + performance_schema + sys schema。
  • 分析:pt-query-digest + mysql_config_editor
  • 变更:pt-online-schema-change / gh-ost
  • 监控:mysqld_exporter + Prometheus + Alertmanager + Grafana
  • 日志:filebeat + Elasticsearch / ClickHouse
  • 业务:HikariCP / Druid + ORM 防护参数。

17.5 治理节奏的真实定位

  • 治理不是写完 SQL 改完索引就完了。
  • 治理是长期工作:只要业务在演进,SQL 在写入,慢查询治理就一直在路上。
  • 真正的闭环是:业务需求 → Schema 评审 → SQL Review → 上线 → 监控 → 警报 → 复盘 → 优化 → Schema 评审。

十七点五、慢查询与上层的协同治理

慢查询本身并不是孤立存在的。下面这段我把它当成”上层联动”的补充,分别从 SQL 防护、ORM 防护、缓存协同、变更治理四个角度展开。

17.5.1 SQL 防护(应用入口)

text

# 白名单式 SQL 防护
- 应用连接的 MySQL 账户没有 DDL/DROP 权限
- 应用连接的账户 GRANT OPTION 取消
- SQL Review 自动化校验:
&nbsp; - 不允许 SELECT *
&nbsp; - 不允许 ORDER BY RAND()
&nbsp; - 不允许大表全表查询

更精细一些的工具如 archery、Yearning、Bytebase、SQLFlow 等可以做规则化处理。

17.5.2 ORM 防护

java

// Hibernate / JPA
@QueryHints({ @QueryHint(name = "org.hibernate.timeout", value = "2000") })
List<SomeEntity>&nbsp;findByXxx();

// MyBatis
@CacheNamespace(size = 4096, ...)

业务侧长期要做的:

  • ORM 生成的 SQL 要能 Mapper 中显式指定字段。
  • 控制 join 层级,避免 ORM 让 N 表 join。
  • 大表查询必须带 LIMIT,做”分页”或者”cursor-based”。

17.5.3 缓存协同

java

// Spring Cache + Caffeine + Redis
@Cacheable(key = "#userId", value = "order:list")
public&nbsp;List<Order>&nbsp;list(int&nbsp;userId)&nbsp;{...}

把”读慢查询”变成”读缓存”。但要注意:

  • 业务侧要评估 cache miss 落到 DB 的压力。
  • 缓存击穿:唯一 key 缓存失效时大量请求打到 DB,要加 singleflight / Redisson lock / 本地内存兜底
  • 缓存一致性:写时双删或延时双删。

17.5.4 变更治理

任何涉及”重新写 SQL、改变 SQL 行为、加索引、改字段、改主键”的代码都要走 PR / SQL Review:

text

# PR Template
- SQL 文本
- EXPLAIN 前后对比
- 灰度比例 + 回滚方式
- 影响面 + 执行窗口

不留”光改代码、不改 SQL”的事。

17.5.5 灰度放量的具体做法

下面是一段较详细的”慢查询治理灰度放量”流程,控制清单要写得清楚。

阶段一:筛选 Top SQL

bash

# 单文件最大 200MB,过大用 head / tail 切
pt-query-digest --limit=200 /var/lib/mysql/slow.log > /tmp/digest.txt
grep -E&nbsp;'^# Score'&nbsp;/tmp/digest.txt |&nbsp;head&nbsp;-20

阶段二:改造 SQL / 加索引(dev / staging)

bash

# 在 staging 库
mysql -uroot -p${PASS}&nbsp;< schema_change.sql

# 验证
pt-online-schema-change --print&nbsp;--execute ...

阶段三:测试库压测

bash

# sysbench 多线程
sysbench --db-driver=mysql \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--mysql-host=staging.host \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--mysql-port=3306 \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--mysql-user=readonly&nbsp;--mysql-password=$PASS&nbsp;\
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--mysql-db=sbtest --table-size=2000000 --tables=10 \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--time=60 --threads=64 \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;--report-interval=10 \
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;oltp_read_write run

阶段四:业务代码灰度

text

# 灰度比例
5% (业务内 1 个集群)
25% (4 个集群)
50% (8 个集群)
100%

每个阶段观察 30 分钟以上,确认 SLO 不退化。

阶段五:固化与文档

text

- PR 链接: ...
- 性能对比图: ...
- 事故复盘: ...

17.5.6 治理工作的工程习惯

  1. 不要把慢查询作为单一指标

    :纳入 P99、错误率、调用方业务指标。

  2. 不要把”我想这样改”当成”它已经被验证”

    :每次改造都要数据库 + 应用 + 监控 + 文档四件套。

  3. 不要在生产用 SELECT BENCHMARK(...) 或 SELECT SLEEP(...) 看执行

    ——可以使用 SELECT @@global.long_query_time;要让 SQL 在客户端模拟。

  4. 不要”先关后开”

    :先开新方案验证,再关旧方案。

  5. 每次线上改造都留 PR + 文档

    :第二年的自己不会记得第三周前为什么这样改。

  6. 永远不要把全部实例同时改

    :至少 1 个不动。

  7. 任何时候都不要被一句”能跑就行”劝住

    :记录是证据,是战斗的弹药。

十八、总结

慢查询治理在每个数据库上都会经历一次,或者更准确地说,会经历很多次。把它分成 5 件事,是为了让”治理”这个动作变得可重复、可度量、可自动化:

  1. 打开 + 收集 + 解析慢日志

    是起点;一个不会记录慢查询的 MySQL 没法做治理。

  2. 定位 + EXPLAIN

    是诊断;不要在不读 EXPLAIN 的情况下动索引。

  3. 索引 + SQL + 锁治理

    是工程;要让开发、DBA 形成共同语言。

  4. Buffer Pool + Redo + IO 治理

    是资源调配;不要盲目参数堆叠。

  5. 监控 + 告警 + 回归

    是闭环;治理没有回归就只是临时抱佛脚。

落到具体动作上,这 5 件事每一条都对应可执行的命令 + 可量化的指标 + 可回滚的路径。慢查询治理没有任何银弹,只有”持续观测 + 持续改造 + 持续回归”。希望这篇文章能在你下一次发现 DB CPU 飙到 90% 时,给你至少 5 件事做,而不是束手无策。

慢查询治理的最终形态不是”再也没有慢查询”,而是”慢查询总有,但总能被定位、能被解决、能被验证”。让”5 件事”成为你数据库日常的一部分,让业务侧、研发侧、运维侧用同一套语言交流慢查询,让每一次 P99 抖动都对应一个明确的”是哪件事没做好”。治理一旦形成节奏,慢查询就不再是黑天鹅,而是可被提前解决的白兔。

最后想强调一句:MySQL 慢查询并不是单点问题。它和业务形态、Schema 演进、应用层缓存、复制拓扑都有关。任何一次治理都要”顺带改善 Schema、修改应用层 SQL、增加监控项”,才会有可衡量的结果。愿你在每一次 DB CPU 飙红的时候都能稳住阵脚,把”治理”做成日常。

文末福利

今天给大家分享一份超级牛掰的Linux学习笔记,足足有1456页!是一位Linux运维大佬整理分享的,分享是获得大佬同意的,大家有需要的尽管收藏起来!

笔记介绍

这份笔记非常全面且详细,从Linux基础到shell脚本,再到防火墙、数据库、日志服务管理、Nginx、高可用集群、Redis、虚拟化、Docker等等,与其说Linux学习笔记,不如说是涵盖了运维各个核心知识。

并且图文并茂,代码清晰,每一章下面都有更具体详细的内容,十分适合Linux运维学习参考!

笔记展示

笔记下载

扫描下方二维码,回复暗号“1456页Linux笔记“,即可100%免费领取成功


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:马哥Linux运维 点击关注 👉 点击关注 👉《MySQL 慢查询把系统拖垮后,我做了这 5 件事》

评论:0   参与:  0