做DBA这些年,我最怕听到的一句话就是“这张表就几千万行,你导一下不就行了”。说这话的人通常觉得,大表迁移无非就是跑个导出、再跑个导入,完事。但我们真正干过的人心里都清楚,几千万行大表迁移的难点从来不在“导”这个动作本身,而在于迁移期间业务还在持续写入、停机窗口只给两个小时、目标库的SQL行为和源库还不完全一致。任何一个环节翻车,凌晨爬起来跑回滚脚本的还是你。
大表平滑迁移,本质上就三件事:存量数据怎么快速搬过去,增量数据怎么持续追平,切换时怎么做到业务无感。这篇文章我会把实际项目中沉淀下来的方案选型思路、双写回放的具体实施链路、三层数据校验的对账机制,以及几千万行大表迁移时踩过的那些文档里根本不写的坑,一次性梳理清楚。不是为了讲理论,就是给正在准备大表迁移的同行一份可以直接参考的实操路线图。
1. 先搞清楚“大表”到底是难在哪:三个瓶颈不说透,后面全白搭
1.1 数据量只是表象,时间窗口才是第一瓶颈
几千万行,按单行平均1KB算,就是几十GB,如果表结构字段多、索引建得狠,上百GB也不奇怪。这个量级下,mysqldump导出的速度大概是多少?实测下来,普通云盘环境下全表导出通常能到每秒5万到15万行的水平,听着不慢,但导入就完全是另一码事了。目标端要逐行插入、维护二级索引、处理事务日志,速度经常掉到每秒几千行。简单算一笔账:3000万行数据,导出约5到10分钟,导入可能要40到90分钟,索引一多或者目标库性能一般,两三个小时都下不来。
而真正的麻烦在于:业务不能停。你导出的时候,源库还在持续产生新的写入和更新,导出来的快照本身就比“当前状态”旧。你要么接受丢数据,要么把业务停下来再导。但核心业务表的停机窗口通常就一两个小时,提前准备、切换验证、回滚余量一扣,真正能用来导数据的时间可能只有30分钟。30分钟导完几千万行再完成校验?基本不现实。
所以大表迁移的第一道瓶颈不是数据量,是时间窗口。方案设计的首要目标,就是让“大量数据的搬运”不再占用宝贵的停机时间,把停机时间压缩到切换那几十秒到几分钟。
1.2 存量与增量:迁移必须同时对付的两种数据
大表迁移的数据要拆成两种看。存量数据,是迁移开始那一刻已经存在于表里的历史数据,这部分有明确上限,做一次快照导出就能搞定。增量数据,是迁移期间业务新写入、更新、删除的数据,这部分没有上限,业务不停,它就在持续产生。
很多第一次做迁移的人只盯着存量数据,导出导入跑通了就以为完事了,结果业务一恢复,新产生的数据全丢了,切库等于切了个寂寞。正确的做法是把迁移拆成两个动作:存量一次性搬运,增量持续回放。存量靠快照机制解决——MySQL里就是InnoDB的MVCC,mysqldump加--single-transaction拿到的就是一致性快照;增量则依赖Binlog日志,把源库上每一条变更操作按顺序在目标库上重放一遍。
理解这个拆分的意义在于:存量导入的耗时可以被“容忍”,因为业务还在源库跑着;而增量回放需要持续运行,直到目标库追上源库的位点,两边数据基本一致,才具备切换条件。整个平滑迁移的架构,本质上就是围绕“存量快照 + 增量回放”这两个动作设计的。
1.3 兼容性差异:你以为迁的是数据,其实迁的是行为
再补一个特别容易被低估的难点。异构迁移时,源库和目标库的行为差异会直接导致数据不一致,或者更隐蔽地导致“数据看着一样,业务跑起来不一样”。
典型的有这么几类:自增主键的缓存策略不同,可能导致ID空洞或冲突;隐式类型转换规则不同,同样的SQL在两个库上产生不同的执行结果;字符集排序规则不同,影响ORDER BY和GROUP BY的语义;时间类型精度不同,比如源库是datetime,目标库对超出精度的时间直接报错或静默截断。这些差异不会在导出导入时报错,但会在迁移后的某天突然冒出来。
所以数据迁移从来不只是搬数据,还要做兼容性调研和SQL行为比对。这个工作量甚至比搬运本身还大,但它决定了一次迁移能不能真正落地。
2. 方案选型:物理拷贝、逻辑导出与双写回放,各自的适用边界在哪里
2.1 物理拷贝:最快,但停机窗口决定了生死
物理拷贝的原理是把整个数据目录在文件层面直接复制,不经过SQL层解析。几十GB的数据,文件拷贝通常能在十几分钟内完成,在“速度”这个维度上几乎没有对手。但它的前提条件非常苛刻:源库和目标库的版本、配置、文件布局要高度一致,目标库基本只能以“空库导入”的姿势接入,跨版本、跨架构的场景很难用。
适用场景是同版本升级、同构环境扩容,比如同一套MySQL版本从物理机迁到云主机。但对于几千万行大表要做跨版本或异构迁移的场景,物理拷贝基本帮不上忙。而且物理拷贝期间,严格来说需要业务停止写入,否则拷贝出来的数据文件内部就是不一致的,除非你配合一致性快照和日志回放一起做,那复杂度就上来了。
我的看法是:物理拷贝更像是一个“加速器”而不是“方案”,它解决的是拷贝环节的速度问题,但没办法独立完成一次业务无感的平滑迁移。
2.2 逻辑导出:简单灵活,但大表场景容易翻车
mysqldump这类逻辑导出工具为什么流行?因为一条命令就能跑通,表结构、数据、权限一把梭,而且支持跨版本、跨架构。但大表场景下它的劣势非常明显。
导出端,虽然可以用--single-transaction拿到一致性快照而不锁表,但导出过程本身会对源库产生不小的压力,尤其是全表扫描几千万行的时候,Buffer Pool被大量占用,正常业务的查询性能会被拖下来。导入端更头疼,目标库每插入一行都要维护索引,二级索引越多越慢,而且单线程导入的效率极低,几千万行导入动辄几个小时。
不是不能优化,比如拆分文件、并发导入、先导数据后建索引,都能把时间压下来。但逻辑导出本质上是一个“先停机、后搬迁”的思路,它的时间消耗和表的大小强相关,表越大越不适合做平滑迁移。
所以我的经验是:逻辑导出最适合千万行以下的表,或者对停机时间不敏感的场景。几千万行大表想做到业务无感,它撑不起这个任务。
2.3 双写回放:业务不停、数据不丢的通用解
双写回放是目前“平滑”程度最高的通用方案,也是我这篇文章真正想展开讲的核心。它的流程可以概括为:先用存量导出把历史数据搬过去,再借助Binlog把迁移期间新增的数据持续追平,等目标库追上源库的位点,差距缩小到秒级甚至毫秒级,做一次极短时间的只读保护,完成切换。
这里说的“双写”不是要求业务应用主动写两个库,更通用的做法是“单写 + 日志回放”:业务只写源库,回放程序订阅源库的Binlog,把变更实时应用到目标库。好处是业务层完全零改动,方案侵入性最小,原生支持跨版本、异构数据库迁移。
适用场景非常明确:几千万行以上的大表、业务不能停、停机窗口严格到分钟级。可以说,在目前主流的技术栈下,它就是大表平滑迁移最值得优先考虑的方向。
2.4 一张表看懂三种方案的选型逻辑
三种方案没有绝对的好坏,只有匹配不匹配。我把选型逻辑整理成一个表,方便各位直接对照自己的场景:
| 方案 | 核心原理 | 大表适用性 | 停机时间 | 业务侵入性 | 典型场景 |
|---|---|---|---|---|---|
| 物理拷贝 | 文件层整体复制 | 中(依赖同构环境) | 分钟到小时级 | 高,需停写 | 同版本升级、同构扩容 |
| 逻辑导出 | SQL层解析导出行数据 | 低(指数级耗时) | 小时级起步 | 高,需停写 | 小表迁移、结构变更 |
| 双写回放 | 存量快照 + Binlog增量回放 | 高(核心方案) | 秒级到分钟级 | 低,业务无感 | 几千万行大表、异构迁移、跨版本升级 |
3. 双写回放迁移实操链路:从存量导入到增量追平再到灰度切换
3.1 前期准备:从表结构到Binlog的完备性检查
选定双写回放方案之后,别急着导数据,先把准备工作做扎实。
第一件事,确认源库的Binlog已经开启,并且格式是ROW。回放程序需要从Binlog里解析出每一行的变更前后镜像,STATEMENT格式解析不出来完整的数据变化,MIXED格式在边界情况下也不可靠,所以ROW格式是硬前提。同时还建议把binlog_row_image设置为FULL,确保日志里包含完整的行数据。
第二件事,结构先行。先在目标库建好表结构,然后逐项比对两边的字符集、排序规则、时区、SQL Mode。尤其是time_zone,如果源库和目标库不一致,Binlog里记录的时间戳在应用时就会出现偏移,而且这类错误极难排查。结构确认无误了,再考虑导数据。
第三件事,做表的数据画像。统计主键分布、行数范围、大字段占比,这决定了后续分片怎么切。主键分布均匀的按区间分片就好,主键分布不均匀的,比如有热点ID区间,就得把区间切得更细,避免单分片数据量过大拖慢导入。
3.2 存量数据导入:分片并发而不是一把梭
存量导入的核心思路是“分片并发”。不要一条mysqldump把全表倒出来,而是按主键范围切成多个分片,每个分片独立导出、独立导入。
我的习惯是每500万行切一个分片,比如3000万行的表切成6片,然后控制并发在4到8个线程。并发太低,时间压不下来;并发太高,源库的IO和CPU会先被打满,正常业务跟着遭殃。具体并发数需要现场压一下,从4个线程开始观察源库负载,负载可控再逐步加。
导出命令参考:
mysqldump \ --single-transaction \ --quick \ --set-gtid-purged=OFF \ --where="id BETWEEN 0 AND 5000000" \ db_name big_table > part_1.sql注意--single-transaction的作用是拿到InnoDB的一致性快照,不加这个参数会对表加锁,生产环境绝对不能直接跑。--quick是让客户端边查边写,避免一次性把结果集全载入内存。--where就是我们用来分片的条件。
导入端可以并开多个mysql连接,每个连接导入一个分片文件。如果表上的二级索引很多,建议先只建主键,数据导完后再批量创建二级索引。为什么?因为每插一行,InnoDB都要同步更新所有索引,索引越多,插入越慢。先导数据后建索引,是把“边插边建索引”的随机IO变成了“批量建索引”的顺序IO,速度提升非常明显。代价是导入期间目标库上这张表的查询可能会因缺少索引而变慢——但反正是迁移中的库,业务还没切过来,能接受。
3.3 增量回放:用Binlog把新库追到只差几秒
存量导出的同时,业务还在写入源库,所以需要记录一个“起始位点”:在导出开始前,执行SHOW MASTER STATUS拿到当前Binlog的File和Position。更省事的做法是在mysqldump命令里加--master-data=2,它会在导出文件里自动记录对应的Binlog位点注释。
回放程序从这个位点开始订阅并解析Binlog,把变更apply到目标库。这个环节里,最容易翻车的是幂等性处理。因为存量导出拿到的是一致性快照,和Binlog之间可能有重叠区间——快照里已经包含了部分Binlog里记录的变更,回放时这些变更会被再次执行。如果直接执行,很快就会出现主键冲突或者重复插入。
处理方案是:INSERT操作在回放时改写成INSERT ... ON DUPLICATE KEY UPDATE,或者直接使用REPLACE;UPDATE和DELETE按主键定位执行,不依赖行内其他字段。这样即使同一条变更被重复回放,结果也是幂等的。
回放过程中要持续监控延迟。最直观的方式是定期对比源库当前Binlog位点和回放程序已经消费到的位点,两个位点差距越小,说明追得越紧。正常情况下,回放延迟应该在秒级以内;如果业务在高峰期批量更新了大量数据,回放延迟会短暂上升,这时候不用慌,只要位点持续在往前进,说明追得上。
3.4 灰度切换:流量切过去之后才算开始
切换动作本身很简短,但准备工作要拉一串清单:
- 确认目标库已经追平到秒级以内,最好连续观察几分钟内位点差距不再扩大;
- 确认三层数据校验都跑过,结果符合预期(校验方法见下一节);
- 确认回滚预案已就绪,源库不会在切换瞬间被销毁或降级。
切换时,先短暂停写源库。注意是“停写”,不是“停服”——应用的读流量可以正常走。利用这段极短的时间窗口,让回放程序消费完剩余的所有Binlog,把目标库追到和源库完全一致,然后把应用的写流量切到目标库。这个停写窗口通常只有几十秒到几分钟,是整场迁移里唯一需要业务配合停顿的时间。
切换之后不是终点。要持续观察目标库的写入延迟、慢SQL、锁等待,同时保持回放程序和新位点监控跑一段时间。一般建议观察15到30分钟,没有异常才算切换真正成功。
4. 数据校验:一致性不是靠感觉,而是靠对账
4.1 第一层:行数与关键字段聚合对账
数据校验是迁移里最容易被“敷衍”的一步。很多人切完库,业务能跑就说“行了”,真出问题的时候已经晚了。我的习惯是做三层对账,第一层最简单也最快:行数和聚合字段对比。
在源库和目标库分别执行SELECT COUNT(*),对比行数是否一致。这一步虽然粗糙,但能快速发现导出导入丢数据、分片重复或遗漏等低级问题。如果表上有一些不容易变化的聚合字段,比如累计金额、最大值、最小值,也都顺手比一遍,基本能覆盖大部分初级错误。
这一层对账通常在存量导入完成后立刻做一遍,在切换前再做一遍。两遍的目的不一样:第一遍发现存量导入的问题,第二遍确认增量回放没有引入新的差异。
4.2 第二层:抽样对比与哈希指纹
行数一致不代表内容一致,所以第二层要做内容级的对比。但几千万行数据,逐行比对哈希在时间上根本划不来,所以我的策略是“全表指纹 + 抽样细查”结合。
全表指纹的思路是:按主键范围分页扫描,把每一行的关键字段拼接起来算哈希,源库和目标库生成两组哈希集合,然后做集合对比。这样可以发现“哪一行不一样”,但耗时较长,适合放在双写回放的观察期里跑,切库之前能跑完最好。
抽样细查更简单直接:随机抽几百上千个主键值,在两个库各自查出完整行数据,逐字段比对。抽样不是万能的,它发现不了只影响极少数行的问题,但配合全表指纹一起用,已经能把风险控制到很低。
4.3 第三层:业务级校验与长事务检查
技术校验做完,还有一类校验是技术手段覆盖不到的:业务语义。同样一行数据,技术校验认为一致,但业务跑起来结果不对,这种场景我见过不止一次。
所以第三层校验要用业务视角做。核心思路是抓几个实时变化的业务指标做两库对比,比如“今日新增订单数”“当前账户余额合计”——这些指标在源库和目标库上应该保持同步变化。在切换前的观察期里,每隔几分钟对比一次,如果持续一致,说明增量回放是健康且完整的。
另外别忘了检查目标库的活跃事务和长事务。迁移期间如果有长事务一直没提交,它的修改可能没来得及进入Binlog被回放,切换后这部分数据就丢了。所以切换前要看一眼information_schema.innodb_trx,确认没有异常的长事务挂在上面。
4.4 校验工具与脚本思路
工具层面,业内常用的有pt-table-checksum,是Percona Toolkit里的老牌工具,专门做MySQL主从数据一致性校验,支持按主键分 chunk 扫描对比,实测对几千万行的表也能接受。如果环境不允许装新工具,也可以自己写脚本:按主键分页取行,两边各自计算哈希,对比差异。脚本思路不复杂,但要注意分页的效率,不要用LIMIT/OFFSET深翻页,要用主键游标的方式扫描,否则扫描性能会越来越差。
最后提一个提醒:校验过程本身会对目标库产生不小的压力,尤其是全表指纹那类扫描,IO和CPU消耗都不低。尽量安排在低峰期执行,或者限制并发线程数,别让校验把目标库拖垮了。校验是为了保障切换,不是为了给切换制造新的风险。
5. 几千万行大表迁移的踩坑实录:这些细节文档里真的不会写
5.1 自增主键冲突:预分配与步长陷阱
第一个坑,也是最常见的坑:目标库的自增计数器没有跟着数据一起走。存量导入时把几千万行原样导过去了,ID都是源库的,但目标库的AUTO_INCREMENT还停在建表时那个初始值。切换之后业务一写入,目标库生成的新ID很快就会撞上已有ID,直接主键冲突,业务报错。
解决办法是在存量导入完成后,主动把目标库的AUTO_INCREMENT调整到“当前最大ID + 1”,或者至少留出足够的余量。另一个更隐蔽的坑是双写观察期内,如果源库和目标库都各自生成了新ID,两边ID区间就会重叠,切换后直接冲突。这个问题要在方案设计时就考虑清楚,要么提前给两个库划分不同的ID区间,要么在切换瞬间保证只有单边在产生新ID。
5.2 大事务与大SQL带来的回放延迟
Binlog回放的时候,源库只要跑一个大事务,回放端就特别容易卡住。比如业务对几百万行执行了一次批量UPDATE,Binlog里对应的是一个几百MB甚至几个GB的大事务。回放程序必须把这个事务整体在目标库执行完,要么整体成功,要么整体回滚。等待期间,源库产生的其他变更全部堆积在队列里,回放延迟瞬间拉大。
应对这个问题的思路是拆包,但拆包方案必须非常谨慎。把大事务按行拆成小事务去重放,效率确实高,但如果源库那个大事务中途回滚了一部分,拆包重放出来的结果可能和源库不一致。所以拆包要配合业务场景评估,确认目标库重放这些变更时不会引入数据差异,才能用。如果拿不准,宁可让它慢,也不要为了追进度搭上数据一致性。
5.3 字符集与时区的“隐形差异”
这类问题最气人,因为从数据文件层面看,一切都是“成功”的,没有任何报错。但实际跑起来,排序结果不对、时间差八小时、字符串比较结果不一样,全是字符集和时区埋的雷。
举个例子,源库character_set_server是utf8mb4_general_ci,目标库是utf8mb4_unicode_ci,日常查询可能看不出差别,但某些中文排序和特殊字符比较就会不一样。时区更隐蔽:TIMESTAMP类型在MySQL内部以UTC存储,展示时依赖会话时区;DATETIME则不转换。如果源库和目标库的time_zone参数不同,Binlog回放出来的时间数据会出现“两边看着一样,实际差了好几个小时”的情况。
所以我在前期准备清单里专门有一条:切换前统一两边的time_zone、character_set_server、collation_server,这三项必须对齐到完全一致。这类问题一旦发生了,回滚和修复的成本都非常高。
5.4 回滚预案:留好后路才能叫平滑
很多团队会把回滚预案想成“切回去就完了”,实际上对于几千万行的大表,回滚远没那么简单。数据已经导过去了,回放日志也推进了很多,如果切换后才发现问题,直接切回源库,那切换期间目标库写入的新数据怎么办?回放位点怎么处理?这些都是要在预案里提前写清楚的。
我习惯在每次迁移前固定一个回滚预案清单,主要包括:切换前源库保持完整可写状态,不做任何降级操作;记录精确的Binlog位点,回滚时回放程序能准确停在断点位置;回滚时先把应用连接切回源库,停掉目标库的写入,再做问题排查;确认问题原因后,清理目标库的脏数据或重新跑一遍存量导入,而不是在旧数据上打补丁。
这个清单听着繁琐,但真到凌晨两点的切换现场,有一份逐条可执行的预案,比什么都管用。
最后再说点个人体会。做了这么多次大表迁移,我越来越觉得“平滑”的本质不是某一项技术有多厉害,而是每一步都有可验证的结果,每一个动作都有清晰的退路。方案选型、分片导入、增量回放、三层校验,任何一个环节做扎实了,切换那一分钟的把握就多一分。如果你正在准备一次大表迁移,别急着选工具、调参数,先把数据量、停机窗口、增量来源、兼容性差异这四个问题想透彻,动手的时候你会从容很多。