ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

千万级MySQL大表新增字段不锁表:在线DDL与避坑实操

千万级MySQL大表新增字段不锁表:在线DDL与避坑实操

千万级大表新增字段,这五个字在国内互联网公司的数据库运维圈里,几乎等同于“夜间加班”和“事故预案”。做过一次的人都知道,这条ALTER语句写起来不到二十个字符,但在千万行级别的表上执行,可能直接把业务钉死在锁等待上,轻则接口超时,重则主从切换之后流量打挂。这篇文章不聊理论,直接聊风险、方案和落地细节,我把这几年踩过的坑和抄过的作业整理出来,给后面还要做这件事的你一个可以照着操作的参考。

全文会按照“风险盘点、方案选型、完整实操、故障排查与最佳实践”的顺序展开,主要针对MySQL生态,但里面关于锁与复制延迟的分析,对其他数据库同样有参考意义。无论你是在做活动表、订单表还是日志大表,只要数据量上了千万,新增字段就不要再写一条裸ALTER了。

1. 风险盘点:为什么“一条ALTER”会酿成事故

1.1 锁表与元数据锁的影响

很多人觉得加字段不过是“改个结构”,数据库又不是不支持。错就错在,修改结构的代价和行数强相关,而且这个过程会锁住整个表。

MySQL 5.6之前,ALGORITHM默认使用COPY,也就是把原表拷贝到新表,再切换名字。这意味着你不仅需要一张完整新表的空间,而且在拷贝期间,原表上所有写入都会被阻塞。即使5.7以后支持了INPLACE算法,不重建表,但添加字段在多数实现里依然会申请MDL(元数据锁),这个锁会阻塞所有查询和事务,直到DDL完成。千万级表即便只是改个元数据,实际执行时间也可能远超你的预期,锁表时间从分钟级到小时级都不稀奇。

我在生产环境见过一次事故:某团队给一张约三千万行的订单表加一个“是否优惠”的标记字段,直接在低峰期执行ALTER TABLE,结果该表后续所有读写被MDL堵住,业务侧告警铺天盖地,最后只能kill掉DDL进程回滚。杀掉DDL本身还要等待当前事务结束,中间又拖了几分钟,这短短的几分钟足以让一个高并发系统雪崩。

1.2 主从延迟与同步中断

如果说锁表是主库的噩梦,那么主从延迟就是从库的隐形炸弹。主库执行完DDL,binlog会把SQL同步到从库,从库应用时同样要执行一条重量级ALTER,期间从库要么写阻塞,要么CPU打满。

很多业务采用读写分离架构,读流量大量压到从库上。一旦从库开始执行DDL,延时就蹭蹭往上涨。轻则业务读到旧数据,重则从库与主库差距超过阈值,监控直接告警,紧接着就是人为摘除从库。更惨的是,如果主从本身处于高负载状态,DDL期间可能触发半同步复制超时,甚至有主从切换的风险。这个场景下的“新增字段”已经不是性价比问题,而是可用性问题。

1.3 事务大小与binlog堆积

还有一个容易被忽略的风险是binlog堆积。直接ALTER加字段,如果采用COPY算法,整个拷贝过程都在一个大事务里完成,生成的binlog事件巨大。这些binlog不仅占用磁盘,还会在从库回放时消耗大量IO。

我在实际中看到过一张不到一亿行的表,一次COPY方式的ALTER产生的临时文件加binlog,直接把磁盘剩余空间吃掉了20%。如果你没有提前做磁盘容量评估,半路上磁盘写满,数据库会自动进入只读保护模式,那才是真正的“雪上加霜”。

所以,千万不要觉得“加个字段而已,不就是一条SQL”。对于千万级大表,操作前必须把风险想清楚,下面给的方案就是围绕规避这些风险展开的。

2. 方案选型:从“短痛”到“无痛”的路径

2.1 直接ALTER TABLE的适用边界

不是所有加字段场景都必须上工具。如果你的表只有几十万行,或者当前处于测试环境,直接ALTER TABLE没有问题。但一旦数据量上了千万,或者单表占据磁盘超过几十GB,我就会默认走在线DDL方案。

另外要关注MySQL版本。8.0.12以后支持INSTANT算法,对于“新增字段”这个操作,可以在瞬间完成,不拷贝数据,不锁全表。听起来很美好,但它并不是万能的。INSTANT只支持在表末尾追加字段,不能指定AFTER某列,不支持某些字段类型,而且如果表上已经用过INSTANT多次,后续操作可能需要重建表。生产上如果你能确定目标字段可以加在末尾,且不需要在线工具的复杂逻辑,直接用ALTER TABLE ADD COLUMN, ALGORITHM=INSTANT也是可行的。但如果你用的是5.7或者更老的版本,就别想了。

最稳妥的判断方式:先看线上版本,再看表大小,再评估业务容忍度。只要能容忍秒级锁等待,直接改也不是不行,只是风险自担。我个人的红线是单表行数超过千万、表大小超过100GB,一律不直接ALTER。

2.2 主流在线工具:基于触发器的通用方案

业界最常见的在线DDL工具是pt-osc,全称很拗口,大意是“用触发器方式在线修改表结构”。它的核心思路是:创建一个新表结构,把原表数据分批拷贝到新表,同时在原表上建立INSERT、UPDATE、DELETE触发器,把增量数据实时同步到新表,最后在低峰期RENAME两张表完成切换。

这个方案的优点是兼容性好,支持MySQL 5.6、5.7、8.0,对业务侵入相对可控。拷贝过程中,原表上的读写不会被长时间阻塞,只在最后切换瞬间需要短暂锁表。缺点是会额外消耗主库性能,因为触发器和拷贝同时运行,而且变更期间要对原表加三个触发器,也有一点风险。

如果业务上无法接受触发器,或者担心触发器对性能的损耗,可以考虑另一类无触发器方案,比如gh-ost。gh-ost的核心是模拟一个从库,从主库拉取binlog事件,应用到已经拷贝好的新表上,这样就不需要触发器了。它的优点是更轻量,但整体操作链路更复杂,对权限和参数要求更高。

2.3 手工分批加列:极端场景的兜底方案

还有一种场景,工具都不能用,比如MySQL版本奇高、权限限制严格,或者安全要求不允许使用在线工具。此时只能手工“搬表”。思路是:建一张包含新字段的同结构新表,然后通过INSERT ... SELECT分批读取原表数据,按主键范围分批插入新表。全部拷贝完成后,同样通过RENAME交换表名。

这个方法最容易出问题,因为你需要自己维护增量数据的同步,操作繁琐,而且中途一旦数据不一致,很难追查。如果没有团队DBA的底气和完整回滚方案,不建议在千万级大表上手工操作。它更适合当作临时兜底,而不是首选。

综合来看,如果让我给一份选型建议,直接列成表:

场景推荐方式理由
小表(百万行以下)直接ALTER TABLE执行时间短,风险可控
大表且MySQL 8.0.12+考虑INSTANT算法秒级完成,但注意位置限制
大表且MySQL 5.7/5.6pt-osc / gh-ost在线变更,避免长锁
工具不可用手工建新表+分批导数据兜底,但操作复杂

3. 核心实操:以某开源在线表结构变更工具(pt-osc)为例的完整流程

3.1 前置检查与环境准备

动手之前,先把环境摸一遍。这一步能避免80%的坑。

你需要确认几件事:

  • 当前MySQL版本,以及表引擎,InnoDB是必须的,MyISAM大概率不适用在线变更。
  • 表是否有触发器。pt-osc在使用触发器方案时,会要求原表不能有现有同名触发器,实际执行中也会检查并报错。
  • 是否有主键或唯一索引。这是分片拷贝的关键,没有主键的表需要提示,并且工具建议你补一个。
  • 磁盘剩余空间。新表结构和数据会额外占用存储,至少要有原表大小的一半以上。
  • 主从拓扑结构。工具要能连上从库,用于监控复制延迟并自适应调节拷贝速度。

我通常会在操作前执行几条SQL,记录原表行数、表大小、当前复制延迟。比如:

SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='你的库名' AND table_name='你的表名'; SELECT ROUND(SUM(DATA_LENGTH + INDEX_LENGTH)/1024/1024,2) AS size_mb FROM information_schema.tables WHERE table_schema='你的库名' AND table_name='你的表名';

同时看一眼SHOW MASTER STATUS,记录当前binlog位置。这个位置可以用来事后核对数据是否完全一致。

3.2 执行参数详解与计算过程

在线变更工具的核心命令长这样:

pt-online-schema-change \ --alter "ADD COLUMN user_level INT NOT NULL DEFAULT 0 COMMENT '用户等级'" \ --chunk-size=2000 \ --max-lag=2 \ --critical-load=Threads_running=50 \ --recursion-method=processlist \ --execute \ D=yourdb,t=your_table

每个参数都不是摆设。chunk-size表示每个批次拷贝多少行。这个值需要根据表大小和主键范围来估算,不要追求大,我建议从1000到3000之间试。如果主库CPU和IO负载偏高,调到500也行,宁可多跑几分钟,别把主库打挂。

max-lag控制从库最大允许延迟,单位秒。当时延超过设定值,工具会暂停拷贝。这是保护从库的关键参数。critical-load是对主库负载的阈值,这里我设为Threads_running=50,表示当活跃线程数超过50时,工具会暂停,等待负载下降再继续。这个值要根据当前峰值活跃线程数来定,建议取正常峰值的1.5倍到2倍。

还有一个重要参数是--check-interval,默认是1秒,即每秒检查一次从库延迟。如果不想太频繁,可以设置为2秒,降低主库额外开销。

执行前一定要先跑dry-run模式,也就是把--execute替换成--dry-run,让它预演一遍,检查是否有权限问题、触发器冲突、主键缺失等问题。等预演完全无报错,再正式执行。

3.3 执行中的监控与校验

正式执行后,不要傻等着。我习惯开两个终端,一个跑SHOW PROCESSLIST看工具进程,另一个每隔30秒查一次主从延迟。

SHOW PROCESSLIST; SHOW SLAVE STATUS\G -- 主要看 Seconds_Behind_Master

如果主库活跃线程数出现过线,工具会自动暂停,日志里会有对应记录。此时不要手动去调chunk-size,耐心等它恢复就行。如果延迟一直降不下来,可以手动中断工具,然后重新用更小的chunk-size执行。中断不丢进度,下次重跑会重新开始,但至少不会让问题恶化。

拷贝完成后,工具会执行RENAME TABLE,把旧表改名为带下划线和时间戳的备份表,新表改名为正式表名。这一步发生得非常快,但在那一瞬间表会被短暂锁定,业务侧可能会出现极少量的等待超时。如果业务无法容忍任何秒级抖动,需要在切换前再接一个低峰期窗口。

整个操作完成后,还需要校验新表的行数、字段定义、默认值。我常用的方法是:

SELECT COUNT(*) FROM your_table; SHOW CREATE TABLE your_table;

然后把新表和你预期的字段对比一遍,尤其要注意自增列、索引、外键是否都被正确创建。

3.4 验证与回滚策略

很多人在执行完命令后就觉得万事大吉,其实漏了最关键的验证环节。在线变更工具会保留一张旧备份表,名称类似your_table_数字时间戳,不要急着删。先在新表上做几轮真实业务读写,确认没有数据错乱、主键冲突,再考虑回收。

回滚的真实含义是什么?如果你发现新表结构不符合预期,但数据已同步,你可以直接删除新表,把备份表RENAME回来。这个操作需要谨慎,因为备份表里没有工具执行期间新增的数据,如果业务已经继续写入,回滚就会产生丢数据。所以生产上我更推荐“向前修复”而不是“向后回滚”。在新表上继续执行一次ALTER来修正字段,比回滚要安全得多。只有当变更完成且业务尚未写入时,才考虑回滚。

另外,操作前务必做一个物理或逻辑备份。虽然在线工具本身是安全的,但万一碰到断电、误操作等意外,备份就是最后一根救命稻草。对于千万级大表,可以选择在从库上做一次备份,或者用mysqldump加--single-transaction --quick参数拉一份逻辑数据,放其他宿主上存着。

4. 进阶技巧与故障排查实录

4.1 什么时候操作最安全

很多团队喜欢把DDL放在凌晨两点,但“凌晨”不等于“低峰期”。我见过一个面向海外用户的业务,北京时间凌晨反而是欧洲和美国的高峰。所以在定维护窗口之前,至少要拉一周的读写曲线,分别统计CPU、连接数和慢查询,找到真正的低谷区间。

同时,操作前要把链接池、超时配置都调一下。比如Java连接池的maxActive可以适当调低,防止短时间阻塞导致连接被打满;如果有自动重试机制,建议在维护窗口关闭或者增加退避。之前就见过因为应用层对数据库连接无限重试,导致DDL还没执行完,业务侧自己把连接池挤爆的情况。

4.2 常见错误速查表

我把实际操作中最容易撞上的几个错误整理成一张表,方便你直接对照。

错误信息原因处理方式
The table must have exactly one primary key表没有主键或存在多个主键先补主键,或指定唯一索引替代
The old and new table have the same structure修改语句没有实际差异检查ALTER字符串是否正确,确保字段名、类型真实变化
Cannot proceed because there is a MySQL server running as root工具检测到执行环境存在不安全设置按提示修改,或用非root账号执行
Could not get slave status无法获取从库状态检查主从复制是否正常,或者调整recursion-method
Trigger already exists原表已有同名前缀触发器先删除冲突触发器,或者改用gh-ost
Chunk size must be greater than zerochunk-size设置错误检查参数是否被误设
Table is being used by an alias有会话对表有活跃连接等会话结束,或临时调整连接数上限

这些报错不一定都发生在同一版本上,但排查思路是通用的。最重要的原则是:看到报错不要慌,先看工具给出的完整日志,而不是只盯着最后一行。

4.3 关于字段位置:AFTER还是FIRST

业务提需求时,经常要求新字段“放在某个字段后面”,理由是看表结构更直观。从数据库性能角度讲,字段位置对存储引擎完全无感,除非你是用的MySQL 8.0的INSTANT算法,它只允许加在末尾。所以如果决定用在线工具,千万不要指定AFTER子句,否则会触发整表重建,在线工具的优势荡然无存。

我一般建议业务方放弃对字段位置的执念,因为SELECT *里的列顺序已经不影响代码逻辑,ORM(对象关系映射)更不会关心物理位置。如果确实要追求视觉效果,可以在代码层通过SELECT列名显式控制输出,不需要在库里折腾。

4.4 如何进一步减小主从延迟对业务的影响

在线工具的max-lag参数是保护从库的第一道防线,但它只能做到“暂停拷贝”,不能真正消除延迟。想要做到极致,还需要在从库侧配合。

如果从库是只读的且没有报表或者数据分析的重查询,可以临时禁掉那些长时间慢查询,给从库释放CPU和IO。也可以提前把从库上的查询超时调短,避免因为一个慢查询拖住复制线程。

如果你使用的是多个从库,可以临时剔除少部分从库,让这些从库专注复制变更数据,另一些继续服务读流量。变更完成后再重新纳入集群。对千万级大表来说,这种容灾式操作比任何奇技淫巧都管用。

4.5 我对在线变更工具的使用体会

这几年下来,我在线上执行过几十次大表字段变更。从最初的直接ALTER到后来的工具化,我最大的体会是:方案选型永远比执行命令重要,而执行前的环境检查又比执行本身重要。多数事故都不是工具不行,而是操作者对表结构、主从状态、业务流量一无所知就盲目执行。

个人建议,任何一个大表变更,先花半小时梳理环境和业务,再花十分钟执行,最后花半小时验证。把备份和回滚预案写进文档,而不是记在脑子里。数据库是业务的底盘,任何一次变更,都应该像一次外科手术一样,先有影象资料,再动刀。

如果你手头也正在面对千万级大表新增字段的需求,别嫌流程繁琐。一次锁表事故带来的业务损失,够你认真做十次演练了。希望这份手册让你少走一点弯路。

返回列表