改表不报错也不动:元数据锁排到了一年之后|洹拓科技

一、不报错不是运气,是默认值决定的结果
结构变更撞上一个未提交的读事务时,现象是“卡住不动”而不是报错。这不是运气好,而是两个默认值叠加之后的必然。
一层是锁的性质。在我们讨论的这套现场版本上,绝大多数结构变更默认走全表重建,全程持有排他型的元数据锁;而任意一个还没提交的会话,哪怕只是对这张表跑过一次全表查询,手里都握着共享读型的元数据锁,两者互斥。关键在于查询不需要“正在跑”才持锁:只要事务没提交,锁就一直握着,报表任务、交互式客户端里忘了提交的事务、被挂起的批量作业,都算。
另一层是等待时间。元数据锁的等待超时默认给到一年。于是改表语句拿不到锁时既不失败也不返回,就在队列里等;一年之内不会有任何一条错误替它说话。运维看到的“没有报错”,其实是“还在等”,两者在这个默认值下长得一模一样。
二、两个超时参数各管一层
现场最常踩的坑,是把 lock_wait_timeout 与 innodb_lock_wait_timeout 当成同一件事。
后者默认五十秒,只管存储引擎层的行锁等待,对元数据锁完全无效;前者才管元数据锁这一类对象,默认值是一年。所以会出现这样一幕:有人盯着“五十秒”这个口径等它自己失败,等了很久什么都没有发生,业务侧的连接却越积越多。行锁的超时管不住元数据锁的排队,这是两套不同层次的等待,任何一个都不能替另一个兜底。
排队顺序还会把影响放大。元数据锁的请求按到达顺序处理,一条排在长事务之后的改表,会把排在这条改表之后的普通读写一起挡住。真正被卡的不是那条改表语句本身,而是它后面整条队——这也是“改表一跑,整张表都不动了”的由来,被挡的读写语句既没和谁争行,也根本没执行到存储引擎那一层。
三、变更前查的是排队,不是表大小
动作顺序要固定下来,而不是先猜表太大。按下面这个次序做:
- 先查有没有未提交事务覆盖这张表,包括那些只做过查询的会话;
- 再按事务开始时间算持续时间找长事务,超过六十秒即可疑,把它们列成名单;
- 然后查元数据锁视图,按库名与表名过滤,分别列出持有者与等待者;
- 最后才看表规模与预估的重建耗时,决定要不要拆窗口。
这里有个必须写进手册的细节:普通的连接列表看不到元数据锁。这类锁在连接视图里没有对应行,要查性能元数据库里的元数据锁表,按库名与表名过滤;找不到持锁者时,退回存储引擎的事务视图,按事务开始时间找长事务。用连接列表判断“没人占着这张表”,得出的结论是假的。
名单拿到手之后要先处置再变更:能由业务方自己提交的,请他们在窗口前提交;联系不上的,按下面第六条里定好的授权在窗口开始的那一刻终止。这份名单本身要作为变更单的附件留档,而不是留在某个人的屏幕上——事后追问“当时到底是谁占着锁”,只有附件能回答。
四、三种“以为能在线做”的误判
第一种,以为显式写了 INPLACE 就会在线。真实行为是:条件不满足时直接报错,不会静默降级成拷贝重建。加在表末尾、允许为空、不带默认值这三条同时满足,加列才可能走在线路径;一旦写成非空还带默认值,整表都要回填,就退化为拷贝重建。显式声明的价值在于把降级变成一次可预见的失败,而不是在执行中途悄悄换了代价更高的路径。
第二种,以为前置校验里 lock 为 none 就等于全程无锁。校验要用 JSON 形式的执行计划去看,而且两个条件必须同时为真:算法规格里给出 inplace,并且支持在线这一项为真,缺一不可。即便 lock 那一格也是 none,也不代表零锁——末尾的元数据切换仍然会短暂加锁,切换期间排进队列的读写会被挡住。在线与无锁是两个概念,把前者当成后者是这一类事故最常见的起点。
第三种,以为改字段长度不会重建。把变长字段的上限从二百五十五扩到一千零二十四,属于百分之百强制重建的那一类,原因是长度前缀要从一字节升成二字节,每一行的存储格式都得改。这类变化看数据本身像是“没动”,但格式变了就要重建。判断重建与否则,要看是否触及行格式与长度前缀这类底层约束,而不是看业务同学能不能察觉。
五、在线工具的两道硬门槛
真要在线做,可接受的路径只有两类:一类是拷贝式的在线改表工具,建影子表、用触发器同步变更期间的新写入、最后改名切换;另一类是升级到支持瞬时加列的版本。工具只作为一类方案谈,但它的两道门槛必须写进变更单的前置校验,而不是事后解释。
触发器这一道:这类工具靠触发器把在线期间的新写入同步进影子表,表上本来就有触发器时,机制无法再叠一层,因此带触发器的表不能自动改。要么先做结构整理把触发器问题解决掉,要么回到停机重建,这条要提前查而不是撞上了再说。
从库这一道:不能在从库上单独跑。工具的每一步,建影子表、加触发器、批量回填、改名切换,本身都会产生要复制的内容;在从库上直接改,会和主线结构与复制状态互相打架,最后可能既没改成也不敢切回去。正确顺序是在主线上变更,让复制自然把结果带下去。表还要求有主键或唯一且非空的索引,这一条同样要放进前置校验。
六、窗口里的权限分配与变更后要留的证据
责任分配必须在窗口开始前定死:这个窗口里,谁有权终止那个长事务。
这条权限决定变更能不能做。如果改表方无权结束别人的会话,那么窗口一开,阻塞范围就顺着队列扩大到整张表的读写,而没有任何人能立即处置;没有这条授权时,变更窗口应当放弃,而不是“先跑跑看”。同时要把被终止方是谁的会话写进窗口公告:业务方需要事先知道他们的报表任务会被打断,而不是从故障通报里得知。终止动作由谁执行、用什么依据、事后向谁同步,这三条一并写清。
变更后要留的证据有三项,直接做成变更记录字段:
| 字段 | 记什么 | 为什么必须记 |
|---|---|---|
| 实际算法 | 在线、拷贝重建还是瞬时 | 以执行后查到的结果为准,不以提交语句里写的为准 |
| 末尾切换时长 | 元数据切换那一段真实持续了多久 | 这才是真正会挡业务的那一段 |
| 期间排队情况 | 有几个会话在等、等了多久 | 判断下一个同类窗口能否复用的唯一依据 |
三项齐了,下次同类变更的时长预估才有依据;缺任何一项,窗口讨论就退回凭感觉。洹拓把这三项固定在结构变更模板里作为收口条件,而不是复盘时临时收集——毕竟这次没报错,只说明它还在等。