dballgts02e58-1,这串字符第一次出现在我们部门的排期表上时,不少同事都以为是谁手滑敲了一串乱码。只有我盯着它看了半天,脑子里立马蹦出来几个关键词:数据库、全量、同步、批次号。没错,这是半年前我们做的一个数据库迁移项目的内部代号,拆开来看其实非常有规律——db取自Database,all代表全量,gts是我们内部的Global Table Sync(全局表同步)方案缩写,02e58是工单系统自动生成的批次编号,-1表示第一轮调整版本。这项目解决的核心问题,就是在一个业务系统不动代码、不降可用性的前提下,把线上核心库从旧环境平滑迁移到新环境,顺便把数据一致性校验也一并做了。
如果你也在准备数据库迁移、跨机房同步、或者想给现有库做一次彻底的数据体检,那这篇内容会很对你的胃口。我会把整个项目从代号的由来、方案选型、执行细节到踩坑记录全部拆开讲清楚,尤其是那些文档里不会写的实操心得和问题排查路径。文章内容不需要你有DBA级别的专业背景,只要接触过数据库、写过SQL,基本就能跟上节奏。
1. 项目代号解读与背景定位
1.1 这个代号背后的真实需求
很多人第一次接触内部项目代号会觉得高大上,其实它就是一个方便团队沟通的索引。dballgts02e58-1说白了,是我们当时面临的一个真实业务场景:线上有一套承载交易核心数据的MySQL数据库,运行了快五年,机器老旧、磁盘水位长期在70%以上,而且版本还停留在5.7的早期小版本,连窗口函数都没有。研发那边新功能提了一个又一个,DBA这边光是处理慢查询和备份恢复就快被拖垮了。
换库这件事,老板一句话很轻巧,落地全是坑。我们不是没有想过原地升级,但评估了一圈发现:老机器本身性能已经见顶,磁盘IO打满的时候连mysqldump都能把业务拖出抖动,更别说要在原库上做版本升级了。于是定了方向——新购两台更高配置的机器,搭建新环境,然后通过全量导出导入加校验的方式,把业务平滑切过去。这也就是dballgts方案的核心诉求:数据库全量级的表数据同步,零业务代码改动,切换时最大程度缩短停机窗口。
这里有一个很容易被忽视的点:很多人一想到迁移就直奔工具,其实在动手之前搞清楚业务对可用性的要求才是最关键的。我们和业务方拉了好几轮会议,最终确认了两个硬指标:第一,正式切换当天,允许最多15分钟的只读维护窗口;第二,迁移过程中旧库必须照常承担读写,不能出现长时间锁表或性能劣化。这两条直接决定了后面所有工具和流程的选型走向。
1.2 为什么需要做一个“全量+校验”的同步方案
基于上面的约束,我们其实有两条路可以走。一条是短痛路线:找业务低峰期,直接停写,做全量导出导入,校验完毕就切流量,四个小时内搞定。另一条是长跑路线:先做全量基线同步,再借助增量同步工具把新旧库的数据差距不断缩小,等两边数据基本追平的时候,再选个深夜窗口做秒级切换。
我们最后选了第二条,并且把它拆成了两个阶段:第一阶段是全量基线同步,代号里的all指的就是这部分;第二阶段才是在全量同步完成后,通过binlog解析做增量追平。为什么不直接用第一条路?因为虽然业务方嘴上说着15分钟可以接受,但运营后台、报表任务、定时脚本这些下游系统实在太多了,一旦主库切换,所有连接串和权限配置都要同步变更,谁也不敢保证15分钟内全部搞定。拉长同步周期,把风险前置消化掉,才是最稳妥的。
增量部分其实不是dballgts02e58-1这个版本的重点,但全量阶段做得好不好,直接决定了增量阶段能不能站稳。如果全量基线数据就是脏的、缺的,那后面增量再怎么追也是白费。所以整个全量同步方案里,我花心思最多的反而是校验环节。我要求每一次全量同步完成后,不光要对比行数,还要做关键表的checksum校验和抽样数据比对,确保从源库导出的每一条记录都和源数据完全一致。这套"全量迁移+多重校验"的组合拳,才是这个代号真正的价值所在。
2. 整体架构设计与方案选型
2.1 全量迁移与增量同步怎么选
接手这个任务后,我第一件事不是去翻工具文档,而是先画了一张架构草图,把新旧两台数据库的上下游关系全部理清楚。我们旧库上跑了大概30多个业务库,其中核心库有6个,最大的表有接近2亿行数据,单表容量接近200GB。整个实例的物理体积加起来接近1.8TB,对于MySQL来说已经不算小体量了,如果直接一把梭用mysqldump全量导出,耗时和风险都会非常感人。
这里就涉及到一个核心决策:全量迁移到底选逻辑备份还是物理备份。逻辑备份的代表工具是mysqldump和mydumper,它们导出的是SQL语句或CSV格式的文本数据,跨版本、跨平台兼容性好,但速度相对慢。物理备份的代表是Percona XtraBackup,它直接拷贝数据文件,速度飞快,但它对版本和文件系统有一定要求,而且恢复时通常要求目标实例的版本不能低于源实例。
我当时的判断是:优先用mydumper做并行逻辑备份,不选XtraBackup。原因有三:第一,我们源库和目标库版本有跨大版本的差距(源库是5.7,目标库想直接用8.0),虽然XtraBackup在新版本里也能做到部分跨版本恢复,但限制比较多,一旦踩坑就是整个恢复流程卡死;第二,迁移过程中我们还需要对数据做一定的清洗和格式统一,逻辑备份的文本中间格式更方便处理;第三,mydumper天然支持多线程并行导出,单表200GB用20个线程导出,速度实测下来比mysqldump快了好几倍。
2.2 技术栈与工具选型的心路历程
工具选型这件事,看似简单,实际上每个选择背后都是取舍。我们最终敲定的工具链是这样的:源库数据导出用mydumper,导入用myloader(和mydumper配套),增量同步用的是阿里开源的canal配合自研消费脚本,一致性校验用自研的Python脚本加SQL聚合。
这里我特别想聊一聊为什么没有直接选DataX或者CloudDTS这类商业/开源全家桶。其实我们最开始也考虑过DataX,它的同步能力确实强,尤其擅长异构数据源之间的搬迁,而且有可视化配置界面。但问题是它太重了,部署一套DataX分布式调度环境,对当时我们这个小团队来说有点杀鸡用牛刀。而CloudDTS这种云厂商提供的迁移服务,虽然省心,但要求源库和目标库的网络链路尽量稳定,我们这边源库是自建机房,网络环境比较特殊,走云服务反而多了一层不确定性。
自己组合工具链的好处是每一步都清楚知道在干什么,出了问题也更容易排查。坏处嘛,就是所有坑都得自己踩一遍。比如mydumper导出时如果没指定--trx-tables-only或者加了不合适的参数,导出的数据可能不是一致性的快照。再比如myloader导入时默认会关闭外键检查,这虽然能提升导入速度,但如果你没提前处理好表之间的依赖关系,导入完成后可能藏着不少逻辑隐患。这些细节后面我会在实操章节一个一个展开。
所以我的总结是:工具选型没有绝对的好坏,关键是匹配自己的场景和团队能力。如果你只有单个人力、时间又紧,直接上云服务或者全家桶会稳妥很多;如果你有足够的排查意愿和学习成本预算,自己组合工具链能让你对整套数据链路的理解上一个台阶。
2.3 数据一致性校验方案的设计
全量同步做到一半,我意识到一个问题:光靠行数对比根本不够。举例来说,同一张表在源库和目标库各有100万行,但很可能源库里某一行叫张三,目标库里被写成了李四,行数一点没差,数据却完全不同。所以我们在dballgts方案里设计了一套三层校验:
第一层是库表级元数据校验,对比源库和目标库的表数量、表名、字段名、字段类型、索引数量、分区结构。这一步可以在导入完成后立刻执行,能发现大部分因为导入异常或建表语句差异导致的问题。第二层是行数与聚合值校验,除了count(*)之外,还会对关键数值字段做SUM和MAX的对比,SUM能发现数据增减问题,MAX能快速判断是否有异常值。第三层是抽样内容校验,对于核心业务表,按主键做哈希取模抽样,抽取出大约5%到10%的数据行,逐字段进行比对。
这套三层校验方案在后来的操作中被验证是非常有效的。第三层抽样校验曾经真的抓出来一个大问题:有一张业务配置表的编码字段,在导入时因为默认字符集不一致,几万个中文描述全部变成了问号。行数没差,SUM也没差,只有抽样比对的时候发现字符串内容对不上。这个问题如果没被拦下来,上了生产之后配置管理模块会直接乱套。所以在这里也给所有要做数据迁移的朋友提个醒:校验方案一定要结合内容级比对的维度,不要迷信行数和聚合值。
2.4 网络与权限环境的准备
正式动手之前,还有一块特别容易被忽略的准备工作:网络链路和数据库账号权限。我们的源库在旧机房,目标库在新的云VPC里,如果网络不通或者延迟太高,后面的导出导入根本没法做。我们用了一台跳板机打通了两边网络,然后把跳板机的内网IP加到了源库和目标库的白名单里,同时创建了专用的迁移账号。
迁移账号的权限也给得很克制:源库账号只授予SELECT、RELOAD、LOCK TABLES这三个权限。SELECT用来导数据,RELOAD和LOCK TABLES是为了配合mydumper的--lock-all-tables或FLUSH TABLES WITH READ LOCK操作,保证导出过程的数据一致性。目标库账号授予ALL PRIVILEGES就够了,因为导入阶段需要建库建表导数据。这里有一个实操tips:源库账号尽量不要给SUPER权限,否则一旦发生长事务把主库拖住,排查时连是谁干的都不好分辨。
网络延迟和带宽同样要提前测试。我们当时用scp传一个100MB的测试文件实测,发现带宽只有3MB/s,算了一下1.8TB的数据量光是传输就要好几天,这显然不行。后来协调网络同事开了一条专线,带宽提升到30MB/s,整个数据文件传输时间才压缩到10小时左右。这个经验告诉我们:迁移项目的排期不能只看导出和导入时间,中间文件传输的耗时一定要提前用实测数据去估算。
3. 核心实操步骤与细节
3.1 前期盘点:表结构、数据量与依赖关系
任何一次全量迁移,前期盘点做得越细,后期踩坑越少。我们花了整整三天时间来做盘点,产出三份清单:表清单、大表清单、依赖关系清单。表清单记录了所有需要迁移的库和表,以及每张表的行数、数据大小、字符集、存储引擎。大表清单则把单表超过10GB或行数超过5000万的表单独拎出来,作为后续并行导出时的重点观察对象。依赖关系清单用来梳理表之间的外键关联和业务调用链,这一步主要是为了规划导入顺序,避免先导了子表后导父表造成外键校验失败。
盘点阶段的SQL长这样的,如果你也要做类似的事儿,可以直接参考:
SELECT table_schema, table_name, table_rows, ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb, table_collation FROM information_schema.tables WHERE table_schema = 'your_db' ORDER BY size_mb DESC;这张表跑出来之后,我们对整个迁移的量级有了非常直观的认知。下游的报表库、日志库不算,核心6个业务库加起来一共287张表,其中超过1GB的大表有34张,最大的那张订单流水表有1.8亿行,占了总数据量的接近一半。看到这个数字之后,我开始意识到整个迁移流程里最需要优化的是大表的导出和导入策略,而不是纠结于小表的方案适配。
3.2 迁移执行:导出、传输、导入的完整流程
执行阶段我们按表的大小分了三个梯队来处理。第一梯队是小于1GB的小表,一共200多张,平时业务访问量也不大,直接用mydumper按库导出,再用myloader导入,整个过程非常快。第二梯队是1GB到20GB的中型表,比如用户表、商品表,这类表我们单独处理,导出时开启16个线程,导入时开启16个线程,尽量提高并行度。第三梯队是大于20GB的大表,典型的就是那张1.8亿行的订单流水表,这类表我们不仅单独导出,还会在导出前和业务方确认,选择一个对源库压力最小的时间窗口。
mydumper导出命令大致长这样:
mydumper \ -h source_host -P 3306 -u migrator -p 'password' \ -B your_db \ --trx-tables-only \ --chunk-filesize 256 \ --threads 16 \ --compress \ --outputdir /data/dump/your_db参数说明一下:--trx-tables-only是告诉mydumper通过开启一个一致性的只读事务来获取快照,而不是对全局加锁,这样能最大程度减少对线上业务的影响。--chunk-filesize 256表示每256MB的数据拆分成一个chunk文件,方便并行处理和后续的断点排查。--compress开启压缩,这里是CPU换带宽的典型操作,因为我们的网络带宽是瓶颈,压缩后能省将近一半传输时间。
导入侧我用了myloader,命令大致是这样:
myloader \ -h target_host -P 3306 -u migrator -p 'password' \ -B your_db \ --directory /data/dump/your_db \ --threads 16 \ --overwrite-tables \ --enable-binlog这里有一个我一开始踩过的坑:myloader默认会禁用binlog记录,也就是说导入过程不会被记录到目标库的binlog里。这对于一次性迁移来说问题不大,但如果你迁移完之后马上又要搭建目标库的高可用从库,那就麻烦了,因为主库的binlog里缺少了导入阶段的数据,从库无法正常追平。我当时加了--enable-binlog参数,让导入操作也写binlog,给后面的高可用架构留了一条后路。
3.3 校验与回切:如何确认数据没问题
数据导完之后,我们立刻跑了一轮前面设计好的三层校验。第一层元数据校验用了information_schema对比,确认287张表全部创建成功,字段类型和索引数量和源库一致。第二层行数与聚合值校验,写了一组SQL,在源库和目标库分别计算出每张表的行数、SUM、MAX,再统一交给Python脚本比对。
抽样比对脚本的核心逻辑用的是主键哈希取模,大致伪代码如下:
import pymysql # 连接源库和目标库 src = pymysql.connect(host='source_host', ...) dst = pymysql.connect(host='target_host', ...) # 获取表的主键列表 pk_columns = get_primary_key_columns(table_name) # 采样:按主键的哈希值取模,拿出10%的行 sample_ids = get_sample_ids(src, table_name, sample_rate=0.1) # 逐行比对每个字段 for row_id in sample_ids: src_row = fetch_row(src, table_name, pk_value=row_id) dst_row = fetch_row(dst, table_name, pk_value=row_id) if src_row != dst_row: log_diff(table_name, row_id, src_row, dst_row)这套脚本看起来简单,实际跑起来遇到不少问题,最典型的就是浮点数精度不一致。源库的DECIMAL字段迁移到目标库后,由于版本升级,默认的浮点计算精度发生了变化,导致SUM值的对比经常出现极小差距。后来我们统一在SQL里把浮点数先做ROUND再对比,才把这种误报压了下去。这里也想说明一点:校验脚本本身也需要根据数据特征去迭代,并不是写一次就能一劳永逸。
回切操作是整个流程的高潮。回切之前我们把新库设为只读,让开发同事改了一遍配置中心的数据库连接地址,然后让业务方做一轮快速冒烟测试。确认没问题之后,再正式打开目标库的读写权限,同时把源库改成只读作为备份留存。整个过程我们控制在12分钟,比业务方给的15分钟窗口还快了3分钟,算是非常顺利的一次切换。
3.4 参数调优与性能观察
参数调优这部分,我踩了不少坑,也总结了一些实实在在的经验。在导出阶段,mydumper的线程数并不是越大越好,线程开得太高,源库的磁盘IO和CPU会被瞬间打满,直接影响线上业务。我们测试发现16线程是最优值,超过16之后导出速度几乎没有提升,反而源库的慢查询数量明显变多。导入阶段同样如此,myloader开到16线程已经能跑满目标库的写入能力了,开更高会导致目标库的redo log刷写跟不上,反而拖慢整体速度。
网络传输环节我们用了rsync加断点续传的思路。第一次全量文件传输断了一次,当时已经传了6个小时,差点崩溃。后来改用rsync来做数据文件同步,就算中途断了重新跑一遍rsync,它也只传输没传完的部分,不会从头再来。这个细节在传输大文件时价值巨大,强烈建议所有做迁移的朋友都学会用rsync的--partial和--append-verify参数。
另外,整个迁移过程中我们要实时观察一个核心指标:源库的活跃会话数和慢查询数。我们当时专门写了一个监控脚本,每隔30秒抓一次show processlist和慢查询日志的最新输出,一旦发现异常立刻调整导出线程数或暂停任务。这种运维习惯能让你在迁移过程中保持对生产环境的掌控感,而不是把命运交给运气。
4. 常见问题与排查技巧实录
4.1 字符集乱码问题
字符集问题是我在迁移过程中遇到的第一个大坑。排查过程前面已经提到过,配置表里的中文字段全部变成了问号。这个问题出现的根源是源库连接字符集是utf8mb4,但我用mydumper导出时没有显式指定连接字符集,导致导出的SQL文件里中文字符被转成了Latin1的乱码字节。更麻烦的是myloader导入时又自动识别错了,双重编码转换之后中文内容彻底损坏。
解决办法是mydumper和myloader两端都强制指定字符集:
mydumper --defaults-file=/path/to/my.cnf ... # my.cnf 里配置 [client] default-character-set=utf8mb4同时在导入侧也要配上同样的default-character-set。这个教训让我明白一个道理:凡是涉及数据迁移的工具,字符集一定要在两端显式指定,千万不要依赖默认值。
4.2 大表迁移超时与断点续传
第一张超过100GB的大表,导出用了一个多小时,结果传到一半网络闪断,前面全白干了。为这事我郁闷了一个下午。后面痛定思痛,把传输链路改成了rsync加断点续传,并且用tmux挂起长任务,避免SSH断连导致整个进程被杀掉。
另一个跟超时有关的问题是mydumper导出时自身可能会因为大表的chunk划分不合理而报错。mydumper对于单表默认会先按主键范围划分多个chunk,如果主键分布不均匀,某些chunk会特别大,导出时容易出现等待超时。这个问题的排查方法是看导出的元数据文件里每个chunk的记录数,把明显偏大的chunk单独处理,或者换用--rows参数重新划分chunk大小。
4.3 外键约束与自增主键冲突
导入阶段遇到过一次外键约束失败,原因是我们按表的大小做了排序,大的父表先导,子表后导,但由于外键依赖方向搞反了,导致子表插入时找不到对应的父表记录。解决办法是导入前先临时禁用外键检查,myloader其实默认会加SET FOREIGN_KEY_CHECKS=0,但如果你的导入流程里跳过了某些元数据恢复步骤,这个设置可能不会生效。
自增主键冲突的问题则出现在我们第一次切换后,业务方反馈新插入的数据主键和旧数据撞了。查了半天发现是mysqldump在导出表结构时,创建表的自增起始值没有从源库带过来。mydumper其实是支持导出自增信息的,但如果你在--no-data模式下手动导了表结构,就会漏掉AUTO_INCREMENT这个属性。所以这里建议:表结构导出和导入尽量使用工具自带的元数据恢复功能,不要自己拆开来手动操作。
4.4 校验不一致时的快速定位法
一旦校验脚本报出数据不一致,快速定位问题就成了一件很重要的事情。我们的做法分三步走:第一步,看差异清单里的表和主键ID,先判断差异是集中还是分散的。如果集中,大概率是导入时某段chunk出了问题,找到对应的导入日志即可。如果分散,可能是字符集或浮点精度这类系统性原因。第二步,把差异行的原始数据分别从源库和目标库导出来,做十六进制级别的字节比对。第三步,比对字段类型和表结构定义,看看是不是因为新旧版本MySQL的隐式转换规则变化导致的问题。
这里有一个很实用的排查命令,用来查看某行数据的原始字节:
SELECT HEX(column_name) FROM table_name WHERE id = 123;拿这个输出和源库对比,几秒钟就能确认是不是字符集问题。如果不是字符集问题,再看字段类型定义,十有八九能定位到根因。这套快速定位方法论在我们后来的多次数据迁移中被反复使用,效率非常高。
5. 后续演进与个人心得
5.1 从一次性迁移到持续同步
dballgts02e58-1做完的时候,我以为这件事就翻篇了。结果没过两个星期,业务方跑过来问:能不能把源库的增量变更也同步到新库?因为切换之后有些老的报表任务还在直连旧库,两边数据在短期内必须保持一致。于是我们在全量同步的基础上,又加了一层基于binlog的增量同步管道,这就是这个代号里之前提到的增量部分,也顺带完成了从一次性迁移到持续同步的能力升级。
这个升级其实不难,核心就是消费binlog事件,把insert、update、delete操作在新库上重放一遍。我们用了canal来做这个事,它在社区已经很成熟,配合ZooKeeper做位置记录,基本能做到秒级的同步延迟。但需要提醒的是,增量同步不是银弹,它对目标库的写入性能有额外消耗,而且一旦源库执行了像ALTER TABLE这样的大DDL操作,增量管道很容易因为解析不了新的binlog格式而中断。所以这类管道一定要配置好告警监控,别等着业务方来反馈数据不对了才想起来去查。
5.2 踩坑后的几点实操心得
整个项目做下来,我的一个核心体会是:数据迁移项目真正难的从来不是把数据从一个库搬到另一个库,而是保证搬过去之后两边长一个样,并且业务感知不到变化。这里有几个实操心得,分享出来希望对你有帮助。
第一,迁移前一定要做充分的盘点,最好能形成清单文档。不管是表结构、数据量、字符集还是主键索引,每一项都提前记录清楚。很多问题在盘点阶段就能预判到,提前规避比出问题后再去排查高效得多。第二,校验环节不能省,而且要设计多层的校验方案,不要只对行数。一个比较实用的经验是,把行数、SUM、MAX、抽样比对组合起来用,虽然费一点时间,但能保证数据质量真的过关。第三,给自己留退路,源库的只读备份状态至少保留一周再彻底下线。我们当时就是因为留了一手,才能在切换后发现一个报表漏迁时迅速恢复数据,避免了事故。
如果你正在计划类似的迁移项目,不妨先把你手头的那张最大的表和最核心的业务表列出来,预估一下它们的导出导入时间,再对照这篇文章里的方案设计一种适合自己的迁移路径。数据库迁移这种事,准备得再充分都不为过,每个坑都是真金白银换来的经验。