大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!
前阵子遇到一次极其典型的生产事故:凌晨2点,我在主库给一张8000万行的核心交易表加了个字段。
使用的是 MySQL 8.0 的 Online DDL,ALGORITHM=INSTANT,主库毫秒级完成。当时我看了一眼监控,主从延迟是0,就安心睡觉了。
结果早上9点,业务群炸了:"从库读出来的数据全是旧状态,报表严重失真!"
登录从库一看,SHOW SLAVE STATUS 里的 Seconds_Behind_Master 赫然显示:7200秒 。而 SHOW PROCESSLIST 显示,从库的 SQL 线程一直卡在 Waiting for table metadata lock。
主库秒级完成的 DDL,为什么到了从库就变成了一场灾难?今天我们把这件事的底层链路彻底拆开。
一、深层剖析:从库的 MDL 锁雪崩
在 ROW 格式的 Binlog 下,主库的 DDL 会作为一个 Query_event 写入 Binlog。从库的 SQL 线程在读取到这个事件并尝试重放时,必须经历以下链路:
-
申请 MDL 排他锁 :从库执行
ALTER TABLE前,必须获取该表的 MDL X锁。 -
阻塞后续所有事务 :由于从库的 SQL 线程(如果是单线程,或者遇到跨线程的表级依赖)正在持有这张表的 MDL X锁,所有针对这张表的后续 DML 操作(INSERT/UPDATE/DELETE)在回放时,统统只能进入 MDL 的 PENDING 队列排队。
-
雪崩效应:主库还在持续不断地产生新的 Binlog(包含大量针对该表的 DML),从库 SQL 线程卡在 DDL 上,后面的日志堆积如山。
为什么 Seconds_Behind_Master 没提前报警?
如果在 DDL 执行前,从库已经处于空闲状态(延迟为0),DDL 事件到达后,从库开始卡住。此时 Seconds_Behind_Master 可能会显示为0或很小的数字,直到主库有新的事务过来,时间戳开始产生差值,这个数字才会开始飙升。这就导致了一个监控盲区。
二、隐秘的角落:INSTANT 算法为什么会"悄悄降级"?
我明明写了 ALGORITHM=INSTANT,主库也确实是秒完的,为什么从库会卡这么久?
MySQL 8.0.12 引入的 INSTANT 算法极其严苛。在以下场景中,即使你指定了 INSTANT,MySQL 也会静默降级为 INPLACE 甚至 COPY,且不报错:
-
表包含全文索引(FULLTEXT)或空间索引(SPATIAL)。
-
在表的中间位置添加列(8.0.29之前)。
-
修改列的数据类型、字符集。
如果是 COPY 算法,意味着主库和从库都需要新建一张临时表,把原表数据逐行拷贝过去。8000万行的表,拷贝一次可能耗时十几分钟,这期间从库的 MDL 锁死死不放,主从延迟瞬间爆炸。
实操验证:如何防止算法降级?
不要盲目相信自己的 SQL,在执行 DDL 时,强制带上 ALGORITHM 和 LOCK 子句:
bash
ALTER TABLE orders ADD COLUMN remark VARCHAR(100), ALGORITHM=INSTANT, LOCK=NONE;
如果当前表结构不支持 INSTANT,这条语句会直接报错退出。宁可报错,也不要让它悄悄降级导致主从瘫痪。
三、实操解法:大表 DDL 的正确打开方式
对于千万级以上的大表,哪怕支持 INSTANT,也建议走以下方案:
方案一:使用 gh-ost 规避锁等待(大厂标配)
gh-ost 的核心原理是:创建幽灵表 -> 模拟从库拉取 Binlog -> 应用增量 -> 原子 Rename。
深层机制 :gh-ost 在主库上执行的是大量的 INSERT INTO ghost_table SELECT ... 的按行拷贝,产生的 Binlog 都是正常的 DML 操作。从库在回放这些 DML 时,不会触发表级 MDL 排他锁,因此业务读取正常,延迟极小。
但要注意 gh-ost 的限流机制 。它默认会检查主库的 Threads_running,如果超过阈值就会暂停拷贝。你可以通过 --max-load 参数调整,避免拷贝打满主库 CPU。
方案二:利用主备倒换(Smart DBA 的杀手锏)
如果是在云环境或有成熟的高可用架构(如MHA/Orchestrator),可以采用"切主"方案:
-
在业务低峰期,主动触发一次主备切换。原备库提升为主库(承接读写业务)。
-
原主库(现已被降级为备库),它上面的数据与主库是完全同步的。
-
在这个"新备库"上执行你的 DDL 操作。因为它不承载业务读取,你可以随便锁表、随便跑 COPY 算法,不会影响业务。
-
等 DDL 执行完毕,主从延迟追平后,在下一个低峰期,再把它切回主库。
方案三:精准延迟监控(替换掉不可靠的 Seconds_Behind_Master)
既然 Seconds_Behind_Master 有盲区,做大表 DDL 时,必须开启 pt-heartbeat 来监控真实的延迟:
bash
# 在主库启动心跳写入
pt-heartbeat --user=root --password=xxx --create-table --database=test --update --daemonize
# 在从库监控真实延迟
pt-heartbeat --user=root --password=xxx --database=test --monitor --print-master-server-id
它会每秒在主库写入一个带时间戳的探针,从库读取后对比当前时间,哪怕从库没写入,你也能精准看到当前回放落后了多少秒。一旦超过阈值,立即通过 kill 命令终止从库的 DDL 回放,防止雪崩。
四、小结
不要被主库上 Online DDL 的"秒级"现象迷惑。在主从架构下,DDL 回放涉及到 MDL 锁的排他性、算法的降级以及并行复制的失效。对于大表,尽量避免在主库直接执行 DDL;优先使用 gh-ost 或"主备倒换";并在 DDL 期间用 pt-heartbeat 替换掉原生的延迟监控。宁可提前排查,不要等到业务大面积超时再来救火。
小耶在手,SQL 不愁
还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~