大表结构变更导致主从延迟问题复盘
时间: 2026-04-23 夜间
影响: 从库主从复制延迟,读请求堆积
一、背景
业务需要对大表 table_xxx 进行结构变更,采用 DMS 无损变更方案。
二、DMS 无损变更原理
sql
┌──────────────────────────────────────────────────────────┐
│ 1. 创建影子表 (ghost table) │
│ CREATE TABLE _table_xxx_new LIKE table_xxx; │
│ ALTER TABLE _table_xxx_new ADD COLUMN ...; │
├──────────────────────────────────────────────────────────┤
│ 2. 增量数据同步 │
│ 通过 binlog 或触发器将变更同步到影子表 │
├──────────────────────────────────────────────────────────┤
│ 3. 原子切换 (cut-over) │
│ RENAME TABLE table_xxx TO _table_xxx_old, │
│ _table_xxx_new TO table_xxx; │
└──────────────────────────────────────────────────────────┘
三、问题现象
变更执行后,监控发现:
- 从库主从复制延迟持续增长
- 从库出现大量
Waiting for table metadata lock状态的查询 - 应用层读请求超时/报错
示例阻塞 SQL:
sql
-- State: Waiting for table metadata lock
SELECT id, field1, field2, ...
FROM table_xxx
WHERE primary_key = 'xxx' AND status = 20;
四、根因分析
4.1 MDL 锁机制
MySQL 5.5+ 引入 Metadata Lock (MDL) 保护表结构:
| 操作类型 | MDL 锁类型 | 持有时长 |
|---|---|---|
| SELECT | SHARED_READ | 事务结束 |
| INSERT/UPDATE/DELETE | SHARED_WRITE | 事务结束 |
| DDL (RENAME/ALTER/DROP) | EXCLUSIVE | 操作完成 |
关键特性:
- 即使是普通 SELECT,MDL 也持有到事务结束(非语句结束)
- EXCLUSIVE 锁必须等所有 SHARED 锁释放
- MDL 采用公平排队机制
4.2 问题链条
sql
┌─────────────────────────────────────────────────────────────┐
│ 从库存在长事务 │
│ (慢查询 / 未提交事务 / autocommit=0) │
│ 持有 table_xxx 的 MDL SHARED_READ │
└─────────────────────────────────────────────────────────────┘
↓ 阻塞
┌─────────────────────────────────────────────────────────────┐
│ 主库执行 RENAME TABLE,binlog 同步到从库 │
│ 从库 SQL 线程尝试获取 EXCLUSIVE MDL │
│ → 被长事务阻塞 → SQL 线程挂起 │
└─────────────────────────────────────────────────────────────┘
↓ 阻塞
┌─────────────────────────────────────────────────────────────┐
│ 后续 binlog 无法回放 │
│ → 主从延迟持续增长 │
└─────────────────────────────────────────────────────────────┘
↓ 同时
┌─────────────────────────────────────────────────────────────┐
│ 新的读请求进入从库 │
│ 想获取 SHARED_READ,但 EXCLUSIVE 在排队 │
│ → 公平锁机制导致新请求也必须排队 │
│ → 读请求堆积 → 应用超时 │
└─────────────────────────────────────────────────────────────┘
4.3 MDL 公平排队示意
vbnet
时间轴:
─────────────────────────────────────────────────────────────►
长事务 (SHARED_READ): ████████████████████████████████
RENAME (EXCLUSIVE): ↓等待
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
新 SELECT (SHARED): ↓被 EXCLUSIVE 阻塞
░░░░░░░░░░░░░░░░░░
五、诊断方法
5.1 查看 MDL 锁等待
sql
-- MySQL 5.7+
SELECT
t.PROCESSLIST_ID,
t.PROCESSLIST_INFO,
m.LOCK_TYPE,
m.LOCK_STATUS,
t.PROCESSLIST_TIME
FROM performance_schema.metadata_locks m
JOIN performance_schema.threads t
ON m.OWNER_THREAD_ID = t.THREAD_ID
WHERE m.OBJECT_NAME = 'table_xxx';
5.2 找出长事务
sql
SELECT
trx_mysql_thread_id,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY trx_started ASC;
5.3 查看阻塞链
sql
SELECT
p.ID,
p.USER,
p.HOST,
p.TIME,
p.STATE,
LEFT(p.INFO, 100) AS sql_text,
t.trx_started
FROM information_schema.PROCESSLIST p
LEFT JOIN information_schema.INNODB_TRX t
ON p.ID = t.trx_mysql_thread_id
WHERE p.COMMAND != 'Binlog Dump'
ORDER BY t.trx_started ASC, p.TIME DESC
LIMIT 20;
六、改进措施
6.1 变更前准备
| 措施 | 说明 |
|---|---|
| 检查从库长事务 | 确保无超过 N 秒的活跃事务 |
| 选择低峰期 | 避开读流量高峰 |
| 通知相关方 | BI/报表等暂停大查询 |
6.2 从库配置优化
sql
-- 设置锁等待超时,避免无限等待
SET GLOBAL lock_wait_timeout = 30;
SET GLOBAL innodb_lock_wait_timeout = 10;
-- 检查 autocommit 配置
SHOW VARIABLES LIKE 'autocommit';
6.3 自动化防护
bash
# pt-kill 自动杀死从库长事务
pt-kill \
--host slave-host \
--match-info "SELECT" \
--busy-time 60 \
--kill \
--print \
--daemonize \
--pid /var/run/pt-kill.pid
6.4 变更方案优化
| 方案 | 说明 |
|---|---|
| gh-ost | 有更优雅的 cut-over 重试机制 |
| pt-osc | 支持 --max-lag 自动限速 |
| 主动 kill | 变更前在从库 kill 长事务 |
6.5 监控告警
- 从库延迟 > 10s 告警
- MDL 等待 > 30s 告警
- 事务时长 > 60s 告警
七、经验总结
- MDL 锁是隐形杀手 --- 即使是普通 SELECT,只要在事务中就会持有 MDL 直到事务结束
- 从库长事务危害大 --- 会阻塞 DDL 回放,导致主从延迟
- 公平锁机制会放大影响 --- 一旦 EXCLUSIVE 排队,新读请求也会堆积
- 大表 DDL 需要全链路评估 --- 不仅关注主库,更要关注从库状态
文档版本: v1.0
编写日期: 2026-04-24