多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

大表平滑迁移:双写回放与三层校验实战指南

大表平滑迁移:双写回放与三层校验实战指南 做DBA这些年我最怕听到的一句话就是“这张表就几千万行你导一下不就行了”。说这话的人通常觉得大表迁移无非就是跑个导出、再跑个导入完事。但我们真正干过的人心里都清楚几千万行大表迁移的难点从来不在“导”这个动作本身而在于迁移期间业务还在持续写入、停机窗口只给两个小时、目标库的SQL行为和源库还不完全一致。任何一个环节翻车凌晨爬起来跑回滚脚本的还是你。大表平滑迁移本质上就三件事存量数据怎么快速搬过去增量数据怎么持续追平切换时怎么做到业务无感。这篇文章我会把实际项目中沉淀下来的方案选型思路、双写回放的具体实施链路、三层数据校验的对账机制以及几千万行大表迁移时踩过的那些文档里根本不写的坑一次性梳理清楚。不是为了讲理论就是给正在准备大表迁移的同行一份可以直接参考的实操路线图。1. 先搞清楚“大表”到底是难在哪三个瓶颈不说透后面全白搭1.1 数据量只是表象时间窗口才是第一瓶颈几千万行按单行平均1KB算就是几十GB如果表结构字段多、索引建得狠上百GB也不奇怪。这个量级下mysqldump导出的速度大概是多少实测下来普通云盘环境下全表导出通常能到每秒5万到15万行的水平听着不慢但导入就完全是另一码事了。目标端要逐行插入、维护二级索引、处理事务日志速度经常掉到每秒几千行。简单算一笔账3000万行数据导出约5到10分钟导入可能要40到90分钟索引一多或者目标库性能一般两三个小时都下不来。而真正的麻烦在于业务不能停。你导出的时候源库还在持续产生新的写入和更新导出来的快照本身就比“当前状态”旧。你要么接受丢数据要么把业务停下来再导。但核心业务表的停机窗口通常就一两个小时提前准备、切换验证、回滚余量一扣真正能用来导数据的时间可能只有30分钟。30分钟导完几千万行再完成校验基本不现实。所以大表迁移的第一道瓶颈不是数据量是时间窗口。方案设计的首要目标就是让“大量数据的搬运”不再占用宝贵的停机时间把停机时间压缩到切换那几十秒到几分钟。1.2 存量与增量迁移必须同时对付的两种数据大表迁移的数据要拆成两种看。存量数据是迁移开始那一刻已经存在于表里的历史数据这部分有明确上限做一次快照导出就能搞定。增量数据是迁移期间业务新写入、更新、删除的数据这部分没有上限业务不停它就在持续产生。很多第一次做迁移的人只盯着存量数据导出导入跑通了就以为完事了结果业务一恢复新产生的数据全丢了切库等于切了个寂寞。正确的做法是把迁移拆成两个动作存量一次性搬运增量持续回放。存量靠快照机制解决——MySQL里就是InnoDB的MVCCmysqldump加--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-purgedOFF \ --whereid 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-data2它会在导出文件里自动记录对应的Binlog位点注释。回放程序从这个位点开始订阅并解析Binlog把变更apply到目标库。这个环节里最容易翻车的是幂等性处理。因为存量导出拿到的是一致性快照和Binlog之间可能有重叠区间——快照里已经包含了部分Binlog里记录的变更回放时这些变更会被再次执行。如果直接执行很快就会出现主键冲突或者重复插入。处理方案是INSERT操作在回放时改写成INSERT ... ON DUPLICATE KEY UPDATE或者直接使用REPLACEUPDATE和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回放的时候源库只要跑一个大事务回放端就特别容易卡住。比如业务对几百万行执行了一次批量UPDATEBinlog里对应的是一个几百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位点回滚时回放程序能准确停在断点位置回滚时先把应用连接切回源库停掉目标库的写入再做问题排查确认问题原因后清理目标库的脏数据或重新跑一遍存量导入而不是在旧数据上打补丁。这个清单听着繁琐但真到凌晨两点的切换现场有一份逐条可执行的预案比什么都管用。最后再说点个人体会。做了这么多次大表迁移我越来越觉得“平滑”的本质不是某一项技术有多厉害而是每一步都有可验证的结果每一个动作都有清晰的退路。方案选型、分片导入、增量回放、三层校验任何一个环节做扎实了切换那一分钟的把握就多一分。如果你正在准备一次大表迁移别急着选工具、调参数先把数据量、停机窗口、增量来源、兼容性差异这四个问题想透彻动手的时候你会从容很多。
返回列表