十几亿数据做全量迁移,中间不可能一路跑到结束。
迁移过程中总会遇到各种问题:迁移程序有Bug要修、或者需要重启又或者数据库压力太大要暂停等,都有可能。
如果每次出了问题都从第一条重新开始,这个迁移基本就做不下去了。
所以,断点续传是必须的。
我们的做法并不复杂。
建一张迁移进度表,只记录当前已经扫描到的最大主键ID。每处理完一批数据,就更新一次这个ID。
程序启动时,先读取这张表里的last_id,然后继续往后扫描。
这样无论程序重启多少次,都能从上一次成功的位置继续跑,不需要重新开始,也不用人工指定起始位置。
迁移刚开始那几天,经常是一边跑一边修Bug,这套机制帮我们省了不少时间。修完代码重新部署,程序自己就接着往下迁移了。
这里还有一个好处。
整个迁移程序是无状态的,真正记录迁移进度的是数据库里的那张进度表,而不是程序内存。
程序什么时候挂、部署多少实例,都不会影响迁移进度。
数据扫描采用按主键范围递增的方式。
SQL其实挺简单的,就是查询id > lastId的数据,按主键升序取2000条。
没有使用OFFSET分页。
十几亿数据,如果用 OFFSET,越往后翻越慢,因为MySQL需要先跳过前面所有记录。
按主键范围扫描始终走聚簇索引,无论扫描到第几亿条,查询性能基本都比较稳定。
这种写法还有一个优点,就是不要求ID连续。
生产环境跑了很多年的业务表,中间通常都会有大量ID空洞。有的是物理删除,有的是历史数据清理留下来的。
id>lastId 并不关心中间有没有空洞的,只会继续扫描下一批真实存在的数据。
如果按BETWEEN去切区间,这些空洞会导致不少批次查出来都是空结果,相当于白跑了一次。
迁移的时候,每批处理2000条,批次之间休眠100毫秒。
这些参数没有写死,而是放到Nacos统一管理。
如果发现写入服务压力比较大,就把批次调小一点、休眠时间调长一点;如果机器资源比较充足,就适当提高批大小,加快迁移速度。
整个过程不用改代码,也不用重启服务,配置推送后下一批数据就会生效。
另外,迁移过程中失败记录一定要单独保存。
RPC超时、历史脏数据、字段转换异常,这些问题都很常见,不可能保证一次全部成功。
我们专门建了一张迁移失败日志表,记录来源表、来源主键、错误类型、错误信息,以及原始数据的JSON快照。
这样排查问题就很快。
如果某一种错误突然大量增加,很容易就能定位到底是写入服务出了问题,还是迁移逻辑遗漏了某个场景。
问题修复以后,只需要针对失败记录重新执行一次重试,不需要把整个迁移任务重新跑一遍。
整个迁移系统真正依赖的,其实只有两张表。
一张记录迁移进度,一张记录迁移失败数据。
前者保证程序随时可以恢复,后者保证任何异常都有迹可循。
对于十几亿数据的迁移来说,建议都建立这两张表。