
1. 为什么千万级大表加字段是高风险操作先说个我踩过的坑。前几年在一家互联网公司做数据库运维某天凌晨两点被值班电话叫醒说订单表加字段把业务写挂了。那张表大概三千多万行同事直接在备库上跑了一条ALTER TABLE orders ADD COLUMN ...结果锁表锁了四十多分钟主从延迟直接飙到接近一小时核心接口全部超时。那晚之后我们团队立了一个规矩凡是超过五百万行的大表加字段必须走评审流程。其实大表加字段的风险核心不在加字段这个动作本身而在执行过程中对数据库产生的连锁影响。以最常见的MySQL InnoDB为例老版本的ALTER TABLE会用 copy table 算法也就是新建一张临时表把原表数据一行一行拷贝过去期间对原表加排他锁读写全部阻塞。虽然从MySQL 5.6开始支持了INPLACE算法很多加字段操作不再需要全量拷贝数据但仍然要在准备阶段拿元数据锁在提交阶段做表重建或瞬间锁表对千万级大表来说这些阶段的耗时和风险都会被放大无数倍。除了锁的影响还有几个容易被忽略的点IO和CPU压力即使inplace算法的DDL在修改表结构时也需要读取大量数据页、写入新的数据页把磁盘IO打满很正常。复制延迟主库执行DDL会通过binlog传到从库从库再执行一遍同样的操作主从之间延迟会突然暴涨。空间占用部分方案如copy table需要额外一份全表空间磁盘不足直接导致DDL失败甚至实例只读。回滚困难ALTER TABLE如果在执行中途失败InnoDB会尝试回滚但回滚本身也需要时间期间表可能仍处于不可用状态。所以在动手之前必须先搞清楚三件事你的数据库版本是什么表的行数确切是多少以及哪个方案最适合你的场景。这不是跑一条SQL的事而是一个需要规划、评估、监控、回退的完整操作流程。这篇文章就是把我在生产环境里处理千万级大表加字段的完整思路、工具选型和踩坑记录整理出来给同样被这个事儿折磨过的同学一个参考。2. 方案选型直接执行、gh-ost、pt-osc还是重建表2.1 先看清你手里的牌数据库版本和表特性选方案之前第一步永远是收集信息。我会先登录实例把下面这些查出来SHOW VARIABLES LIKE version%; SELECT table_name, table_rows, engine, create_time, update_time FROM information_schema.tables WHERE table_schemayour_db AND table_nameyour_table; SHOW TABLE STATUS LIKE your_table;table_rows 是估算值InnoDB统计信息不一定准但对百万级以上还是能反映量级。更精确可以用SELECT COUNT(*)但千万级大表跑这个也会很慢一般用information_schema里的值就够了。接着要确认几个关键特性是否有自增主键这决定了很多工具的效率gh-ost和pt-osc都强烈建议有主键否则会退化成逐行处理。是否存在外键约束外键会让部分在线DDL方案失效需要额外处理。是否有二级索引索引会拖慢copy阶段的速度也需要评估。表是否在复制链路中如果是一主多从DDL产生的复制延迟你必须有预案。把这些信息放在一张表里方案评估才有依据。我见过不少人在这一步失手直接拿生产表跑工具结果跑到一半才发现没有主键整个流程卡死进退两难。2.2 不同方案的原理与适用边界先聊最直接的方案ALTER TABLE ... ADD COLUMN。MySQL 5.6以上如果加的字段不涉及索引重建、不使用ALGORITHMCOPYInnoDB会走inplace算法元数据操作很快前提是innodb_online_alter_log_max_size要足够大并且当前没有长事务持有元数据锁。但即使如此这一步仍然可能遇到排他锁窗口而且如果表上有未结束的查询DDL会一直等待MDL锁。所以直接执行适合行数相对较少比如几百万或者业务允许短暂不可用的场景。对千万级我不是很推荐直接用除非是低峰期加上业务能接受分钟级不可用。接下来说两个主流工具gh-ost和pt-osc。pt-oscPercona Toolkit的pt-online-schema-change用的是触发器方案。它在原表上创建触发器把增量变更写入一个新表同时把旧数据批量拷贝过去完成后用原子性的表重命名交换。这个过程对业务基本无感知但触发器本身会带来一定性能开销而且在高并发写入下触发器在执行DML时叠加了额外的行级操作主库负载会有可见上升。gh-ost是GitHub开源的工具思路是基于binlog伪装成一个从库去订阅原表的增量变更然后自己在后台把数据迁移到影子表。这个方案对主库的侵入更小不创建触发器而是把增量变更通过binlog回放到影子表上。它的一个好处是你可以把它放在一个独立服务器上运行业务连接主库走binlog主库几乎不感知额外逻辑。缺点是设置稍微复杂对binlog格式有要求必须用ROW格式而且需要有连接binlog的权限。重建表是另一种思路不是原地修改表结构而是新建一张空表定义好新的schema然后通过数据迁移如pt-archiver、mysqldump分段导入、或者业务双写把数据搬过去最后切换表名。这个方案最灵活可以顺带做分表、归档、清理碎片等但迁移周期长、业务改动大如果不是顺带做重构一般不为了加一个字段就去重建整张千万级的表。我把不同方案的适用场景整理了个表方便你对照方案原表锁影响主库额外负载要求适合场景直接ALTERinplace有短暂MDL锁可能有排他锁窗口较低但有IO波动MySQL 5.6无长事务阻塞百万级以下或业务可短暂中断pt-osc建触发器时短锁全程无表锁中高触发器增加DML开销必须存在主键或唯一键千万级大表可接受工具依赖触发器gh-ost几乎无锁源库无触发器逻辑较低但需要额外进程和binlog订阅binlogROW有主键权限够千万级大表不想用触发器有独立机器重建表最终切换时短锁迁移过程无锁取决于迁移工具可控需要业务配合迁移周期长需要顺带重构、分表、归档的场景2.3 一个真实的选型决策过程说一个我经手的例子。某个核心业务表约两千四百万行每秒钟写入在两百到四百行之间应用侧要求加字段期间不能有超过五秒的不可用窗口。当时的版本是MySQL 5.7binlog格式已经是ROW表上有自增主键。直接ALTER肯定不行因为即使inplace很快MDL锁也可能被长查询挡住不可控。pt-osc要看写入量触发器的开销在高峰段可能有风险gh-ost虽然配置复杂点但负载可控支持暂停和恢复还能做预检。最后我们选了gh-ost在凌晨低峰期执行整个过程对业务几乎没有影响。这个案例说明了选型的核心逻辑不是选最先进的方案而是选最能满足业务约束的方案。如果业务允许五分钟不可用直接ALTER也能做到如果表上没有主键gh-ost和pt-osc都会劝退你如果binlog格式不是ROWgh-ost直接没戏。所以选型表一定是在完成基础信息收集之后才能确定的千万别凭感觉拍脑袋。3. 核心操作步骤以gh-ost为例的完整实操指南3.1 执行前的环境检查与参数准备gh-ost用起来不算复杂但有几个前置条件必须满足。我会按下面这个顺序来检查缺一个都不开工第一确认binlog格式是ROWSHOW VARIABLES LIKE binlog_format;如果是MIXED或STATEMENTgh-ost会直接报错。如果公司有binlog同步链路比如解析binlog到数据仓库还要确认新增字段不会影响下游解析必要时先通知相关团队。第二确认该表存在主键或唯一键而且没有复合外键问题。可以在工具运行前用gh-ost --test-on-replicate做一次演练它会模拟整个执行过程但不做最终切换直接看输出。第三预留磁盘空间。gh-ost的临时表和binlog记录都需要空间估算一下原表在物理磁盘占多少空间最好留出至少等量的空余。用下面的语句查看SELECT table_name AS 表名, ROUND((data_length index_length) / 1024 / 1024 / 1024, 2) AS 大小GB FROM information_schema.tables WHERE table_schemayour_db AND table_nameyour_table;第四检查主从延迟基线。用SHOW SLAVE STATUS查看Seconds_Behind_Master确保在执行前主从延迟本来就在一个健康的范围内。如果平时就延迟几秒那执行时更需要盯紧。第五确定执行时段的业务流量。虽然gh-ost不锁表但大量的行拷贝本身会产生IO和复制负载务必避开业务高峰。我一般选凌晨两点到五点或者至少是低峰窗口。执行之前还要跟业务方对齐如果有定时任务、跑批任务在这个时段跑必须要错开。准备好之后我通常还会先做一次explain和表结构备份把建表语句导出来存留万一要回滚有据可依mysqldump -h hostname -P 3306 -u user -p --no-data --single-transaction --skip-lock-tables your_db your_table your_table_schema_backup.sql3.2 gh-ost 执行命令与关键参数解读下面是一条我在两千四百万行大表上实际用过的命令你可以直接改改参数拿去参考/usr/local/bin/gh-ost \ --host127.0.0.1 \ --port3306 \ --usergh_ost_user \ --passwordyour_password \ --databaseyour_db \ --tableyour_table \ --alterADD COLUMN new_column varchar(64) NOT NULL DEFAULT COMMENT 业务备注 \ --chunk-size1000 \ --max-lag-millis1500 \ --max-loadThreads_running50 \ --critical-loadThreads_running100 \ --heartbeat-interval-millis100 \ --allowed-master-master \ --approve-renamed-column \ --initially-drop-old-table \ --initially-drop-ghost-table \ --execute \ --panic-flag-file/tmp/ghost.panic.flag \ --postpone-cut-over-flag-file/tmp/ghost.postpone.flag逐项说下关键参数--chunk-size每个拷贝线程一次处理的行数。对千万级我建议500到1000太大会导致单次查询耗时过长太小又会让频繁的请求打满主库IO。--max-lag-millis主从复制延迟阈值超过这个值工具会自动限速或暂停。生产环境我一般设1000到2000毫秒宁可慢一点也要保证复制不崩。--max-load/--critical-load监控主库的线程数超过阈值自动限速超过critical值直接停止操作。这个参数对保护主库非常关键。--heartbeat-interval-millis心跳频率用于测量复制延迟一般默认100即可。--panic-flag-file指向一个文件如果执行过程中发现异常创建这个文件可以让gh-ost快速紧急停止。--postpone-cut-over-flag-file先跑完数据拷贝但不做最终切换这个参数特别适合做完演练后等到业务确认再手动切表。--approve-renamed-column这个参数很实用它允许gh-ost在最终交换时支持列名变更我用过几次避免了一些额外麻烦。--initially-drop-*会先drop掉残留的临时表因为上次中断可能留下脏东西需要保洁。注意如果是在从库所在机器上运行可以加--assume-master-host主库host:port明确主库地址避免工具连错。3.3 执行过程怎么盯、怎么判断健康跑起来之后不要甩手走人得一直盯着输出。gh-ost会打印很详细的状态重点关注几个指标Copy: X/Y已经拷贝的行数 / 总行数%拷贝进度Lag当前复制延迟State当前阶段比如copy、cut-over在执行过程中我一般再做三件事第一另开一个终端盯着主库的负载和慢查询SHOW GLOBAL STATUS LIKE Threads_running; SHOW FULL PROCESSLIST;第二看磁盘空间使用率特别是gh-ost临时表所在的目录防止空间耗尽df -h第三盯主从延迟SHOW SLAVE STATUS\G如果延迟超过阈值工具会自动限速如果超过了critical会停止。此时不要直接kill进程先确认是不是负载异常或慢查询拖垮了复制如果是看看能不能通过降低chunk size、加长心跳间隔来继续。还需要强调一个点gh-ost在cut-over阶段会短暂地获取锁但那个锁的时间窗口非常短通常毫秒级只做表重命名。不要因为这个窗口就过度紧张关键是要确保执行期间没有长事务卡在那边否则很小的锁也可能被放大。3.4 执行完成后的收尾验证当看到 gh-ost 输出类似Done或者Migrated字样时先别急着下结论。我会按下面这个清单做一次事后验收确认新旧表是否已切换SHOW TABLES LIKE your_table%正常的话应该有your_table和_your_table_del这样的旧表残留。核对新增字段是否正确SHOW CREATE TABLE your_table;确认数据行数是否一致可以比对 gh-ost 结束时统计的行数与源表行数但最好在业务低峰抽样几条记录确认。检查主从延迟是否恢复到执行前水平。检查复制有没有报错特别是binlog后续传递到下游时下游解析是否正常。确认没问题后手动清理旧表残表DROP TABLE IF EXISTS _your_table_del;drop千万级旧表这个操作本身也会有IO压力建议放在低峰期或者用pt-archiver分段清理不要一把梭。我在生产环境见过直接drop三千万行的旧表把从库IO卡了十几分钟的。4. 常用方案的风险对比与参数取舍4.1 选pt-osc时触发器带来的性能损耗怎么量化如果你因为某些原因选了pt-osc要能预估它对主库的影响。触发器方案的本质是原表每次INSERT、UPDATE、DELETE都会在这个事务里额外同步写入另一张影子表。这等于把原有DML的成本每一项都多了一次复制开销。举个例子一张表原来是每秒三百次UPDATE每次UPDATE涉及一行。开启pt-osc后这三百次UPDATE不仅更新原表还要触发触发器向影子表写入对应的变更可能还会加上触发器中成的DELETE/INSERT逻辑整体DML成本能上升30%到50%压力大的时候甚至翻倍。所以在执行前最好统计一下业务侧的DML分布如果写入并发非常高我建议用gh-ost而不是pt-osc。如果非要用pt-osc有几个参数值得重点调--chunk-size推荐500-1000道理同gh-ost。--max-lag控制主从延迟的阈值同上。--critical-load控制主库的线程数上限。--recursion-method检查从库时要用正确的方法否则会漏掉某个从库。--pause-file可以通过创建文件来暂停操作和gh-ost的panic file类似。另外pt-osc对表上已有的触发器非常敏感。如果原表已经有其他触发器pt-osc默认会拒绝执行必须用--preserve-triggers参数去处理但这个参数需要额外配置否则触发器会丢失或者产生冲突。千万级大表加完字段才发现原来业务触发器丢了那可是大事故。4.2 直接ALTER的适用边界与风险控制直接ALTER不是完全不能用尤其在MySQL 8.0里新的Instant算法对于加列操作已经非常快。MySQL 8.0.12之后某些ADD COLUMN甚至可以做到O(1)时间完成因为它只修改元数据不重建表。所以如果你的库是MySQL 8.0.x较新版本并且加列的业务语义符合只加列、不改列顺序、不加索引那么直接执行可能比任何工具都高效。判断是否走Instant算法可以这样验证ALTER TABLE your_table ADD COLUMN new_col VARCHAR(64) NOT NULL DEFAULT COMMENT 测试, ALGORITHMINSTANT;如果执行加快且不锁表说明支持instant。但这里有几个雷如果表上已经有Instant算法重建过行格式会变成某种特殊状态后续的很多DDL可能不再支持instant。如果加的列放在某个位置不一定是末尾可能无法走instant。如果历史DDL用了INSTANT多次之后ROW_FORMAT会受限最终还是要重建。所以即使8.0可以instant也要先测试不能盲目在生产上直接执行。在5.7以下还是踏实用在线工具。4.3 参数计算示例以2千万行、磁盘空间与复制延迟的预算为例假设你的表是2千万行每行平均1KB那么表的大小大概是20GB左右加上索引可能到25GB。如果是PT-OSC或gh-ost预估需要额外至少20GB的磁盘空间来存放影子表和相关的binlog变更记录。如果执行过程中有大量DMLbinlog增长可能更加夸张。我在一次高并发操作时binlog两个小时涨了30GB差点把磁盘撑满。所以在执行前我建议先算一个保守的数值必要空间 原表大小 * 1.5 binlog增长预算 临时文件空间比如原表25GBbinlog预算20GB那就是25*1.52057.5GB左右。如果磁盘剩余空间小于这个值就要推迟操作或者想办法压缩比如清理其他表的临时文件。至于复制延迟可以做一个粗略的线性预估在 gh-ost 的--max-lag-millis设为1500时你会发现工具会自动根据实际情况调整拷贝速度。所以这个值不是越大越好设计得恰当既能控制延迟又能保证整体速度不至于慢到不可接受。我常用的值如果平时主从延迟小于500ms就设置1000如果在线DDL期间允许短暂延迟到3秒就设置2000。记住一个原则宁可操作慢一点也不要把复制延迟逼到极限。5. 常见问题排查与现场应急实录5.1 执行卡住不动进度停在同一行这是最常遇到的问题。gh-ost或者pt-osc跑着跑着进度不再变化。原因大概率有以下几种当前表上有长时间运行的查询持有MDL锁阻碍了工具的元数据操作。可以用SHOW PROCESSLIST查一下看有没有Waiting for table metadata lock的会话。找到后评估是否能kill掉或者等它执行完。主库负载已经触达critical阈值工具主动暂停。这时候看主库的Threads_running和负载平均值如果确实很高就耐心等不要强行kill工具进程。复制延迟一直降不下来工具反复休眠。这时要分析是不是某个大的UPDATE语句在从库执行慢或者从库上有什么资源争抢。我遇到过一次卡住查了半天发现是某条定时查询在凌晨整点扫了整表持有MDL锁将近十分钟。当时我当机立断联系业务方确认后把那个查询kill了gh-ost马上就恢复。千万别在没确认SQL来源的情况下乱kill容易误伤。5.2 磁盘空间告急大表DDL最容易引发磁盘告警。gh-ost在拷贝过程中会持续生成binlog还有对应的影子表临时文件加上可能在执行的归档任务空间很容易被榨干。如果发现磁盘占比超过80%按照我的经验要立刻判断是否可以暂停归档任务释放部分空间。是否可以通过改binlog过期时间让binlog尽快清理。是否加大临时表所在目录的容量在云环境可以扩容数据盘但要注意扩完需要挂载和重启实例这个风险更大。如果空间剩余确实撑不住宁可停掉DDL也不要在磁盘满的边缘继续跑。一旦磁盘满InnoDB会进入只读保护模式整个业务都会挂那才是真正的灾难。5.3 主从延迟持续上涨下游消费跟不上还有一次gh-ost跑得很正常主库负载不高但从库延迟一直涨到十几秒原因是执行期间正好赶上别的大查询在从库上跑。解决方案很直接临时调大--max-lag-millis让工具限速同时把从库上的大查询错开等延迟回落后再恢复。如果是分析型从库或者备库做报表查询DDL期间一定要提前通知业务方让他们错峰执行。如果延迟始终降不下来可以通过降低--chunk-size到300甚至100逼着工具用更小的步子迁移数据。5.4 工具意外中断了怎么安全恢复gh-ost或者pt-osc跑了30分钟突然因为网络抖动、进程被误杀中断了。之后不要傻傻地再跑一条同样命令因为它可能不认识之前残留的影子表或者在check阶段就报错。我常用的恢复策略是确认残留表SHOW TABLES LIKE %_gho%或%_old%。手动drop掉残留的影子表和新表确保表区干净。重新启动gh-ost并加上--initially-drop-ghost-table参数让它自己清理。但要注意如果中断时已经执行了cut-over也就是新表已经替换了原表那么再启动工具前必须确认数据一致性。这时要先备份新表结构再对比根数据确保没丢数据。记住工具是在线的不代表它是免维护的每次中断都必须像个事故一样认真复盘而不是草率重启。6. 从一千行到一千万行不同量级的操作策略差异6.1 小表可以直接跑大表必须评审虽然这篇讲的是千万级大表但实际工作中你的表可能从一千行到一千万行都有。不要用同一套策略。一万行以下的表直接ALTER都行锁表也就毫秒级。十万行到一百万行可以用在线工具也可以用直接ALTER但最好避开高峰。五百万到一千万行直接ALTER已经要非常谨慎。一千万行以上就必须评估方案、压测、演练、评审一样都不能少。我在团队里定了一个规则超过三百万行的表加字段必须走工具方案超过一千万行必须提前一天发变更评审拉上DBA、运维、业务方一起确认。这套流程看着繁琐但救过很多次命。6.2 大表中的大不是看行数而是看实际影响有些表行数不大但字段很多单行长度很大或者有大量小字段和冗余索引那么即使只有几百万行其物理体积也可能跟几千万行的小表差不多操作风险同样不容忽视。反过来如果一张表一千万行但每行只有几个tinyint字段体积很小直接ALTER也未必不行。所以做策略判断时我更推荐用物理大小、每秒DML量、复制延迟基线、是否存在长事务这四个指标来做评判而不是单纯看行数。一句话行数是需要知道的但不是决定性的真正的决定性指标是操作对系统的综合压力。6.3 遇到极端场景怎么办超过五千万行且无法离线如果表已经超过五千万行业务又不能停 GH-OST几乎是你唯一的选择。这种情况下除了常规参数我还会考虑分阶段执行也就是先拷贝到某个进度暂停观察一段时间的负载再继续切表。这个可以用--postpone-cut-over-flag-file来实现先跑完80%的拷贝暂停一段时间确认系统稳定再删掉postpone标记文件让工具完成最后的切换。这样做的原因是五千万行的表最后10%的数据往往包含最热的写入区域一下子打完可能导致IO峰值。分阶段可以把风险拆分到可控的区间里。另外这种大表在执行完成后旧表的drop也建议用分段删除避免一次drop把IO打满pt-archiver --source h127.0.0.1,P3306,Dyour_db,t_your_table_del \ --purge --limit 2000 --txn-size 1000 --sleep 0.1 --progress 50000直到旧表删完再单独drop掉空壳。7. 实际操作中我总结的几条铁律最后分享几条这几年来处理大表加字段攒下来的经验不算什么高级理论但每一条都是用故障换来的。第一任何方案都要先在测试环境跑一遍。不要直接拿生产表试验至少要在测试库建一个同等量级的表把操作全过程走一遍确认工具可用确认业务无感然后再在生产上排期执行。这个流程不能省尤其第一次用某个工具的时候。第二能加后缀字段就不加中间字段。把新加的字段按在表末尾的设计不仅方便走instant算法的优化也让很多在线工具逻辑更简单。业务上完全可以通过视图、ORM映射、查询别名来兼容非末尾列没有必要为了看着顺眼把字段插在中间。第三永远保留一键回退的预案。加字段本身一般不用回退但万一业务上线后发现问题你要能快速把旧表切换回来。这就要求执行前把旧表结构和数据备份到一个可恢复的位置而不是等出了事再找。第四关注binlog下游的兼容性。表结构变更会改变binlog里的schema如果下游有Canal、DataX、Flink CDC等解析工具必须先通知对应团队确认他们能兼容新增字段否则DDL执行完毕后下游任务可能直接报错中断。第五别迷信大表加字段很快的说法。速度快的前提条件是版本、算法、schema都恰到好处。真实环境里不可控因素太多了还是老办法最稳妥评估、演练、执行、监控、验证。每一步都做到位才会不出事。我做数据库这么久最深的感触是表结构变更看起来是最普通的运维操作但恰恰是最容易出大事的环节。希望这篇基于真实踩坑经历的操作手册能让你在下次给千万级大表加字段时少走点弯路。