一次慢改表引发的线上死锁事故复盘
一、事故背景
在一次常规的数据库表结构变更过程中,对某核心业务表执行了慢改表操作(使用 pt-online-schema-change)。操作开始后,短时间内触发报警:
- 部分接口响应时间显著上升
- 出现请求超时
- 影响约千级请求
整个故障持续时间较短(约几十秒),通过手动干预终止异常事务后恢复。
二、事故经过(简要)
- 11:22:38:执行慢改表
- 11:22:40:发现接口延迟升高
- 11:22:45:尝试中断操作,但响应延迟
- 11:22:50:排查数据库,发现长事务并手动终止
- 11:23:09:系统恢复正常
三、直接原因
在执行慢改表前,未检查数据库中是否存在长事务,导致:
慢改表过程中无法获取必要的锁资源,从而进入锁等待,最终引发类似死锁的阻塞现象。
四、什么是"慢改表"
在 MySQL 中,对大表执行 DDL(Data Definition Language 数据定义语言,如 ALTER TABLE)通常会带来严重问题:
- 表锁(metadata lock)
- 数据重建
- 业务写入阻塞
因此在生产环境中,通常采用在线变更方案,例如: pt-online-schema-change
其核心思路是:
- 创建新表(目标结构)
- 在旧表上创建 trigger,同步增量数据
- 分批拷贝历史数据
- 最终通过 rename 完成切换
整个过程避免了长时间锁表,实现"在线变更"。
五、关键知识点:Metadata Lock(MDL)
这是本次事故的核心。
MySQL 在访问表时,会自动加 metadata lock(元数据锁):
- 普通查询(SELECT) → 持有 MDL读锁
- DDL操作(如 ALTER / CREATE TRIGGER) → 需要 MDL写锁
锁冲突规则:
text
读锁(共享)可以并存
写锁(排他)必须等待所有读锁释放
六、事故根因分析(关键)
1. 长事务的存在
系统中存在未提交的事务,例如:
sql
BEGIN;
SELECT * FROM table;
-- 未提交
该事务会长期持有:MDL读锁
2. 慢改表的行为
pt-online-schema-change 在执行过程中需要:创建 trigger(DDL操作) → 需要 MDL写锁
3. 冲突产生
此时出现:
| 操作 | 锁 |
|---|---|
| 长事务 | MDL读锁 |
| pt-osc | MDL写锁(等待中) |
pt-osc 被阻塞
4. 为什么会影响业务请求
问题并不会停在"阻塞"这一层,而是继续扩大:
- pt-osc 已经占用部分资源(连接、锁等)
- 新的业务请求进入数据库
- 请求开始排队等待锁
最终表现为:
- 请求RT(Response Time 响应时间)升高
- 接口超时
- 系统报警
七、为什么"Ctrl + C"无法立即中断
这是一个常见误区。
MySQL 中:
- DDL操作(如创建 trigger)
- 正在等待锁的线程
不会立即响应中断信号
因此:
- Ctrl + C 可能延迟甚至无效
- 必须通过
KILL或处理阻塞源(长事务)
八、正确的操作流程(慢改表标准 Playbook)
为了避免类似问题再次发生,所有线上慢改表操作必须遵循标准化流程。该流程不仅包含"是否能执行"的判断,还包括"如何安全执行"。
1. 执行前检查(强制步骤)
(1)检查长事务
sql
SELECT
trx_id,
trx_started,
trx_mysql_thread_id,
trx_query
FROM information_schema.innodb_trx;
重点关注:
-
事务运行时间(trx_started)
-
30秒:需要评估风险
-
60秒:必须处理
-
-
SQL内容(trx_query)
- 全表扫描
- 大查询
- 未提交事务
若存在长事务:
- 优先联系业务方确认
- 紧急情况下可执行:
sql
KILL <thread_id>;
(2)检查锁等待情况
sql
SHOW PROCESSLIST;
或:
sql
SHOW ENGINE INNODB STATUS;
确保当前数据库没有异常锁竞争
(3)检查表结构
sql
SHOW CREATE TABLE your_table;
确认:
- 表是否存在主键(必须)
- 是否已有 trigger(pt-osc 不支持)
2. 执行慢改表(标准命令)
推荐使用 pt-online-schema-change 工具。
示例命令:
bash
pt-online-schema-change \
--host=127.0.0.1 \
--port=3306 \
--user=xxx \
--password=xxx \
--database=your_db \
--table=your_table \
--alter="ADD COLUMN new_col INT DEFAULT 0" \
--chunk-size=1000 \
--max-load Threads_running=25 \
--critical-load Threads_running=50 \
--sleep=0.1 \
--set-vars innodb_lock_wait_timeout=50 \
--print \
--execute
关键参数说明:
--alter:要执行的DDL语句--chunk-size:分批拷贝的数据量(影响性能与风险)--max-load:数据库负载阈值(超过自动暂停)--critical-load:极限负载(超过直接终止)--sleep:每批之间的休眠时间(用于降压)--print:输出执行过程(建议开启)
3. 执行过程监控
在慢改表执行期间,需要持续观察系统状态:
(1)观察执行进度
pt-osc 会输出当前进度(如 10%、20%)
(2)观察数据库负载
sql
SHOW PROCESSLIST;
关注:
- Threads_running
- 是否出现大量 Waiting 状态
(3)异常处理
若出现卡顿或异常:
- 检查是否有新产生的长事务
- 必要时终止异常连接
- 无法恢复时中断操作(Ctrl + C 或 KILL)
4. 执行完成后的确认
(1)确认数据一致性
sql
SELECT COUNT(*) FROM your_table;
(2)确认业务状态
- 接口响应时间(RT)
- 错误率
(3)旧表处理
慢改表会保留旧表(如 _old 表):
sql
DROP TABLE _old_table;
建议保留一段时间后再删除,用于回滚兜底
5. 总结
慢改表并不是"无风险操作",其安全性依赖于:
- 执行前的环境检查(尤其是长事务)
- 执行中的负载控制
- 执行后的完整验证
任何一步缺失,都可能导致线上故障。
九、总结
这次事故的本质可以归结为一句话:
在线DDL虽然避免了长时间锁表,但仍依赖 metadata lock。一旦存在未提交的长事务,会阻塞DDL获取锁,进而引发锁等待甚至死锁,最终影响业务请求。
十、可沉淀的工程经验
- 所有线上DDL必须有前置检查步骤
- 长事务是线上数据库的隐形杀手
- 不要相信"慢改表一定安全"
- 必须建立标准化 playbook 并严格执行
十一、后续优化方向
- 建立慢改表执行 checklist(强制流程)
- 增加长事务监控与报警
- 自动化检测脚本(执行前校验)
- 低峰期执行变更操作