编程 pt-osc 中断之后:残留触发器、卡在 MDL 上的 DROP TRIGGER 与 3.6.0 的 --resume

2026-10-20 00:03:55

pt-online-schema-change 被中断之后:残留触发器、卡住的 DROP TRIGGER 和 3.6.0 的续跑

一次 pt-online-schema-change 被 kill、收到 SIGHUP 或者被 critical-load 打停之后,表不会自己回到干净状态。看上去是「没改成」,实际上原表上还挂着三个触发器,库里还躺着一张拷了一半的新表。

中断后的现场:三类残留物

中断时工具会明确打印它没有清理什么,输出大致如下:

Not dropping triggers because the tool was interrupted.  To drop the triggers, execute:
DROP TRIGGER IF EXISTS `db`.`pt_osc_table_del`
DROP TRIGGER IF EXISTS `db`.`pt_osc_table_upd`
DROP TRIGGER IF EXISTS `db`.`pt_osc_table_ins`
Not dropping the new table `db`.`_table_new` because the tool was interrupted.  To drop the new table, execute:
DROP TABLE IF EXISTS `db`.`_table_new`;
`db`.`table` was not altered.

现场留下三类东西:

  1. 原表上的三个触发器 pt_osc__del / _upd / _ins;
  2. 新表 .__new,里面是已经按 chunk 拷过去的那部分数据;
  3. 如果带了 --history,还有 history 表里指向最近完成 chunk 边界的进度记录。

原表本身没有被 alter,这一点输出里写得很清楚:「db.table was not altered.」但触发器还在,意味着之后对原表的每一次写入仍然会往新表写一份。

一个实测案例:从库出现延迟后 pt-osc 一直 wait,人工 kill 之后新表和触发器都没删。白天发现机器负载和 IO 异常高,最后定位就是残留触发器在持续工作。所以脚本里每次 pt-osc 结束,都要核对触发器和 _table_new 是否真的消失。

用 nohup 后台跑也会踩坑:进程收到 SIGHUP 就退出,输出 # Exiting on SIGHUP.。长任务放 screen 或 tmux 里跑。

中断通常从哪来

  • --critical-load 默认 Threads_running=50,超过直接 abort。文档的说法是,默认 50 对大多数服务器可能已经不可接受,也就是触发时必须立刻取消。
  • --max-load 默认 Threads_running=25,每拷完一个 chunk 查 SHOW GLOBAL STATUS,超过阈值就暂停;不写阈值时按启动时当前值 +20% 算。暂停并不能阻止排队,只能等服务器恢复;看到排队应该降 chunk time。
  • --max-lag 默认 1s,每个 chunk 后检查 Seconds_Behind_Source,任一从库超过就 sleep --check-interval(默认 1)后重查。文档原文写得很直白:The tool waits forever for replicas to stop lagging. If any replica is stopped, the tool waits forever until the replica is started. 从库停掉,它就永远等。
  • --sleep 默认 0,每 chunk 后 sleep 若干秒,建议 0.1 级别,用在 max-lag / max-load 限不住的时候。
  • --chunk-time 默认 0.5 秒,块大小动态调整,目标是每块耗时接近这个值。同时显式设置 --chunk-size 和 --chunk-time 时,初始块大小按 --chunk-size,之后仍按 chunk-time 自适应;只设 --chunk-size 不设 --chunk-time 会关掉自适应。
  • --chunk-size-limit 默认 4.0:没有唯一索引时块大小估算不准,估算行数超过 desired*4 就停止拷贝并退出。最小 1,0 表示关闭检查。
  • --tries 的重试表:create_triggers 10 次 / 间隔 1 秒,drop_triggers 10/1,copy_rows 10/0.25,swap_tables 10/1,update_foreign_keys 10/1,analyze_table 10/1。触发重试的条件包括锁等待超时、死锁、KILL QUERY、KILL CONNECTION、连接断开;连接被 kill 会自动重连。

DROP TRIGGER 为什么卡

DROP TRIGGER 本身几乎瞬时完成,和表有多大无关。但它必须先拿到该表的 metadata lock。

任何针对这张表的事务,哪怕只是只读,都会阻止 MDL,于是 DROP TRIGGER 开始排队。而 MDL 的超时由 lock_wait_timeout 控制,默认值是 31536000 秒,也就是一年。所以它看起来就是卡死。

处理方式只有两条:kill 掉持锁的长事务线程,或者在维护窗口里停应用。否则只能等。顺带一提,drop 掉触发器之后还要 drop 新表,磁盘空间才真正释放,残留的新表同样占空间。

最坏的一种:触发器没掉,新表被 drop

清理顺序出问题时,失败会放大。Launchpad bug 1188002 的复现输出(节选):

Dropping triggers...
Error dropping trigger: DBD::mysql::db do failed: Lock wait timeout exceeded; try restarting transaction at /usr/bin/pt-online-schema-change line 9655.
To try dropping the triggers again, execute: DROP TRIGGER IF EXISTS `mydb`.`pt_osc_mydb_mytable_upd`;
Dropping new table...
Dropped new table OK.
`mydb`.`mytable` was not altered.
Error copying rows ... Threads_running=51 exceeds its critical threshold 50

DROP TRIGGER 因为 MDL 等待超时失败了,工具没有中止,接着把新表 drop 掉,并报告「mydb.mytable was not altered.」。

后果是:触发器还在,原表的写入仍然会触发它去往新表写,而新表已经不存在,所有写入报错:

ERROR 1146 Table 'mydb.__mytable_new' doesn't exist

在触发器被删掉之前,原表的写入全部失败。bug 的诉求就是:DROP TRIGGER 失败时不应该继续 drop 新表。这属于清理顺序的问题,和表大小无关,只在 critical-load 中止叠加 MDL 冲突这种组合下出现。所以中断后第一件事不是重跑,是先确认触发器的状态。

3.6.0 的 --history / --resume

3.6.0 起 pt-osc 可以续跑。前提是先知道断在哪,所以要带 --history,把进度写进 history 表。默认表是 percona.pt_osc_history,可用 --history-table 改:

CREATE TABLE pt_osc_history (
job_id INT NOT NULL AUTO_INCREMENT,
db CHAR(64) NOT NULL,
tbl CHAR(64) NOT NULL,
new_table_name CHAR(64) DEFAULT NULL,
altr TEXT NOT NULL,
args TEXT NOT NULL,
lower_boundary TEXT,
upper_boundary TEXT,
done ENUM('yes','no') NOT NULL DEFAULT 'no',
ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (job_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段含义:job_id 唯一标识一次运行;new_table_name 是拷贝表名(如 _table_new);lower_boundary / upper_boundary 是最近完成 chunk 的边界;done 标记是否完成;ts 是最近处理 chunk 的时间。

续跑用 --resume=JOB_ID。工具会检查之前的拷贝表和触发器是否还在,然后从失败的那个 chunk 继续。

四个前提:

  • 第一次运行就要带 --no-drop-new-table;
  • 带 --no-drop-triggers;
  • 中断时拷贝表还没被 drop 掉,否则没有已拷贝的数据,无从续起;
  • 第一次运行就带 --history。否则续跑再次失败时没有新记录,只能从头来。

局限也要说清楚:这是「从最近 chunk 边界继续」的机制,不是任意点恢复。binlog / 触发器的增量同步在中断期间是断的,续跑前工具会重建并重新校验对象是否还在,中断窗口内的变更依赖触发器一直存在(中断不等于触发器被删,所以理论上仍在同步)。如果手工把触发器或新表删了,就只剩从头重跑。

另外注意 --preserve-triggers 的互斥关系:它用于原表已有触发器的情况(MySQL 5.7.2 起同一张表可以定义多个同事件同时间的触发器),工具在拷贝前把原触发器复制到新表、拷贝完再重建,不能与 --no-drop-triggers、--no-drop-old-table、--no-swap-tables 同用;若同时 --no-swap-tables 和 --no-drop-new-table,触发器会留在原表并在新表重复一份(名字带随机后缀)。--reverse-triggers 需要 --no-drop-old-table,rename 之后表缓存 metadata version 变化,可能出现 Prepared statement needs to be re-prepared,服务端预编译语句要重新 prepare。

手工收尾的核对 SQL

先确认触发器和新表的状态,再决定是清理还是续跑:

SHOW TRIGGERS FROM `db` LIKE 'pt_osc%';
SHOW TABLES FROM `db` LIKE '\_%\_new';

救援案例里的做法值得参考:客户只给很短的 MDL 窗口,必须手工完成 swap,于是用 --no-drop-triggers --no-swap-tables 让触发器继续同步新旧表,之后用 pt-archiver 补拷数据。核对时发现新表比原表多出约 4000 行——pt-archiver 已经在事务里、记录尚未写入新表时,DELETE 触发器已经 firing,两边没对上。用 NOT EXISTS 对比找出多余行:

SELECT COUNT(*) FROM largetable l
WHERE NOT EXISTS (SELECT our_id FROM __largetable_new n WHERE n.our_id = l.our_id);

确认这些是原表已删掉的记录后,用唯一索引 DELETE 清理。收尾用一次原子 rename:

RENAME TABLE largetable TO __largetable_old, __largetable_new TO largetable;

然后 DROP TRIGGER,最后等 buffer pool 里旧表页面冷掉再 drop 旧表(可查 information_schema 判断),不要紧接着就 drop。

别忽略磁盘与 binlog

残留的新表本身占磁盘。同时 pt-osc 会逐块把拷贝写进 binlog(目的是让从库并行重放同样的拷贝),binlog 的增长大致与表的 data_length 成正比;而 ALTER TABLE 本身只写一条 DDL 事件。空间不够时,可以用 PURGE BINARY LOGS 清理到复制或恢复仍然需要的那个日志为止。曾经出现过库大到无法对最大表做任何 schema 变更的局面,磁盘这步要在动手前就看好。

版本相关的坑

PT-19191:3.1.0 版本在「表有外键 + 表有触发器 + --preserve-triggers + --alter-foreign-keys-method=drop_swap」这个组合下,改表成功后原表触发器丢失(show triggers from world 为空)。3.3.1 修复。跑之前用 --version 对一下,别用 3.1.0。

项目信息

  • 官方文档:https://docs.percona.com/percona-toolkit/pt-online-schema-change.html
  • 3.6.0 续跑:https://www.percona.com/blog/resume-your-failed-pt-online-schema-change-job/
  • 崩溃救援案例:https://www.percona.com/blog/rescuing-crashed-pt-online-schema-change-pt-archiver/
  • Launchpad bug 1188002:https://bugs.launchpad.net/bugs/1188002
  • 触发器丢失(PT-19191):https://www.mydbops.com/blog/pt-online-schema-change-resulted-in-missing-mysql-triggers
  • DROP TRIGGER 残留讨论:https://dba.stackexchange.com/questions/328540/failed-pt-online-schema-change-left-behind-triggers-how-to-delete

推荐文章

程序员茄子在线接单