一次慢改表引发的线上死锁事故复盘

一次慢改表引发的线上死锁事故复盘

一、事故背景

在一次常规的数据库表结构变更过程中,对某核心业务表执行了慢改表操作(使用 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

其核心思路是:

  1. 创建新表(目标结构)
  2. 在旧表上创建 trigger,同步增量数据
  3. 分批拷贝历史数据
  4. 最终通过 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)异常处理

若出现卡顿或异常:

  1. 检查是否有新产生的长事务
  2. 必要时终止异常连接
  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获取锁,进而引发锁等待甚至死锁,最终影响业务请求。


十、可沉淀的工程经验

  1. 所有线上DDL必须有前置检查步骤
  2. 长事务是线上数据库的隐形杀手
  3. 不要相信"慢改表一定安全"
  4. 必须建立标准化 playbook 并严格执行

十一、后续优化方向

  • 建立慢改表执行 checklist(强制流程)
  • 增加长事务监控与报警
  • 自动化检测脚本(执行前校验)
  • 低峰期执行变更操作
相关推荐
heze092 小时前
sqli-labs-Less-61
数据库·mysql·网络安全
猿小喵2 小时前
MySQL数据库参数解读-第二篇
数据库·mysql
宁小法2 小时前
MySQL - information_schema.COLUMNS中字段的排序原则 以及如何修改
mysql·列排序
PD我是你的真爱粉2 小时前
MySQL 索引深度解析:从底层结构到实战优化
数据库·mysql
神舟之光2 小时前
Springboot+MyBatis-Plus连接MySQL初体验
spring boot·mysql·mybatis
WangJunXiang63 小时前
MySQL高可用详细解析
android·数据库·mysql
蜡台3 小时前
macOS 无法启动 MySQL服务解决
数据库·mysql·macos
执笔论英雄3 小时前
vLLM V1 Scheduler的调度逻辑&优先级分析
数据库·mysql
桌面运维家3 小时前
MySQL安全实战:慢查询诊断与SQL注入防御
sql·mysql·安全