MySQL大表ALTER TABLE锁表导致业务卡死,线上应急处理与安全变更方案
MySQL大表ALTER TABLE锁表导致业务卡死,线上应急处理与安全变更方案
摘要
线上MySQL业务,一张千万级大表,执行alter table修改字段、增加索引,执行之后业务全部卡死,大量查询堆积,数据库CPU飙升,网站接口全部超时。很多开发人员本地测试ALTER很快,直接在线上大表执行DDL,引发线上故障。本文还原故障场景,分析MySQL DDL锁表机制,介绍pt‑online‑schema‑change、gh‑ost在线无锁变更方案,同时提供故障发生之后应急回滚处理步骤。

问题现象
业务MySQL InnoDB表,数据量1200万行,业务正常读写。开发人员需要给表增加一个索引,直接执行SQL:
ALTER TABLE `order_list` ADD INDEX idx_create_time(create_time);
语句提交之后,命令行长时间卡住。业务网站瞬间大量请求超时,数据库连接数打满,业务完全不可用。数据库监控看到大量select、update语句处于Waiting for table metadata lock等待元数据锁状态。新的业务SQL全部阻塞,旧的业务读写全部卡住。强行kill掉alter线程,大量堆积的业务SQL瞬间涌入数据库,出现数据库雪崩。
复现步骤
- MySQL5.7 / 8.0 InnoDB大表,千万级别数据。
- 业务持续读写,表有活跃事务。
- 直接执行ALTER TABLE增加索引、修改字段。
- DDL操作申请元数据锁MDL,遇到未提交事务,DDL阻塞,后续所有DML语句排队等待MDL锁,业务全部卡死。
补充:MySQL5.6开始支持Online DDL,但是不是所有alter操作都可以真正在线,部分DDL依旧会锁表;同时如果存在未提交事务,就算Online DDL,依旧会被MDL元数据锁卡住。
根因分析
MDL元数据锁(Metadata Lock)
MySQL InnoDB访问表的时候会获取MDL锁。DML(select update insert)获取MDL读锁;ALTER TABLE DDL操作申请MDL写锁。
读锁之间可以并发;写锁和所有锁互斥。
当有活跃事务没有提交,持有MDL读锁,DDL申请写锁就会阻塞。DDL一旦被阻塞,后续所有新的查询、更新语句,都需要申请MDL读锁,全部排队等待DDL,整张表读写全部卡住。很多人以为只有MyISAM才锁表,InnoDB行锁只锁行,但是MDL元数据锁是表级别,会引发整张表阻塞。
Online DDL的局限
MySQL5.6引入Online DDL,部分DDL可以避免锁表,但是有前提条件:存储引擎、操作类型、参数innodb_online_alter_log_max_size,并且不能有长时间未提交事务。一旦有长事务,Online DDL同样会卡住,并且堵塞后续全部业务。直接kill DDL不是万能急救手段
ALTER被卡住之后,很多DBA直接kill这个alter会话。kill之后,堆积大量等待MDL锁的业务SQL会瞬间全部执行,瞬间巨大流量压垮数据库,直接雪崩。大表拷贝数据耗时
部分DDL需要重建整张表数据,千万级表会消耗大量IO,磁盘IO打满,数据库响应变慢。故障发生后的应急处理步骤(线上已经卡死场景)
注意:操作生产数据库务必谨慎,优先在低峰演练。
- 找到持有MDL锁的会话,找到长时间未提交的事务
SELECT FROM information_schema.innodb_trx;
查看trx_started,找到运行时间很长的事务。
- 查询当前线程列表
show processlist;
找到处于Waiting for table metadata lock的线程,找到正在执行alter的线程id。
- 优先杀掉持有长事务的会话,而不是先kill alter。长事务不释放,kill alter没有意义。
KILL 会话ID;
长事务释放之后,MDL锁释放,alter才可以继续执行,或者被kill。
如果业务已经大量堆积,在业务允许短暂停机的前提下,再kill alter语句。
风险:kill alter之后,大量排队SQL瞬间释放,瞬间并发会打满数据库,需要业务侧做好限流。
线上安全修改大表的方案
方案一:pt‑online‑schema‑change(pt‑osc)
Percona工具集pt‑osc,原理:创建一张临时新表,执行DDL变更,通过触发器把原表的数据DML同步到临时表,分批拷贝原表数据,最后原子rename替换原表。全程几乎不会锁原表,业务读写不受影响。
示例命令,给order_list表增加索引:
pt‑online‑schema‑change \
‑‑user=root ‑‑password="xxx" \
‑‑host=127.0.0.1 \
D=business,t=order_list \
‑‑alter="ADD INDEX idx_create_time(create_time)" \
‑‑execute
pt‑osc注意要点:
- 原表必须要有主键或者唯一索引。
- 避免业务高峰期执行,拷贝数据会消耗IO。
- 参数控制拷贝批次大小,防止数据库压力过大。
禁止在有大量触发器的表使用pt‑osc。
方案二:gh‑ost
github开源gh‑ost,不依赖触发器,通过binlog完成增量同步,相比pt‑osc对业务影响更小。
工作流程:- 创建幽灵表,执行DDL。
- 分批迁移原表数据。
- 消费binlog同步增量数据。
原子切换表名。
gh‑ost不需要触发器,对业务影响更低,现在很多互联网公司优先选择gh‑ost。方案三:MySQL原生Online DDL(适合小表,确认无长事务)
如果确认表数据量不大,业务没有长事务,可以直接原生online ddl。
ALTER TABLE order_list ADD INDEX idx_create_time(create_time), ALGORITHM=INPLACE, LOCK=NONE;
ALGORITHM=INPLACE不拷贝原表,LOCK=NONE尽量不加锁。
⚠️重要:就算指定LOCK=NONE,一旦存在长事务持有MDL读锁,DDL依旧会阻塞,并且阻塞全表业务。线上大表不建议直接原生ALTER。
方案四:业务低峰期执行
如果没有pt‑osc、gh‑ost工具,只能选择业务访问量最低的凌晨时段执行,执行前确认没有未提交长事务。
日常运维避坑建议
- 禁止业务高峰期直接对千万级大表执行ALTER TABLE,本地开发环境表小,感受不到锁表问题,线上直接引发故障。
- 数据库监控增加长事务告警,运行超过30秒的事务直接告警。长事务是MDL锁故障最主要来源。
- 上线DDL变更,优先使用pt‑osc或者gh‑ost工具。
- 变更前,先查询
information_schema.innodb_trx确认没有活跃长事务。 - 开发测试环境模拟线上数据量,测试DDL行为,不要只拿空表测试。
MySQL8.0依旧存在MDL锁问题,不要误以为升级8.0就可以随便执行大表DDL。
故障复盘总结
很多开发者只知道InnoDB行锁,忽略MDL元数据锁。只要DDL被长事务卡住,后续所有读写全部阻塞,一张表的DDL故障,直接搞垮整套业务。遇到Waiting for table metadata lock,不要上来kill DDL,优先找到源头长事务会话。大表结构变更优先使用在线DDL工具,不要直接原生alter。
凡尘版权
友情链接:凡尘博客
凡尘博客文章|凡尘博客文摘
凡尘影院
凡尘乡音|凡尘街坊
凡尘博客|雨落凡尘博客|羽落凡尘博客
凡尘博客|雨落凡尘博客|羽落凡尘博客