十几亿数据迁移的断点续传

47 阅读3分钟

十几亿数据做全量迁移,中间不可能一路跑到结束。

迁移过程中总会遇到各种问题:迁移程序有Bug要修、或者需要重启又或者数据库压力太大要暂停等,都有可能。

如果每次出了问题都从第一条重新开始,这个迁移基本就做不下去了。

所以,断点续传是必须的。

我们的做法并不复杂。

建一张迁移进度表,只记录当前已经扫描到的最大主键ID。每处理完一批数据,就更新一次这个ID。

程序启动时,先读取这张表里的last_id,然后继续往后扫描。

这样无论程序重启多少次,都能从上一次成功的位置继续跑,不需要重新开始,也不用人工指定起始位置。

迁移刚开始那几天,经常是一边跑一边修Bug,这套机制帮我们省了不少时间。修完代码重新部署,程序自己就接着往下迁移了。

这里还有一个好处。

整个迁移程序是无状态的,真正记录迁移进度的是数据库里的那张进度表,而不是程序内存。

程序什么时候挂、部署多少实例,都不会影响迁移进度。

数据扫描采用按主键范围递增的方式。

SQL其实挺简单的,就是查询id > lastId的数据,按主键升序取2000条。

没有使用OFFSET分页。

十几亿数据,如果用 OFFSET,越往后翻越慢,因为MySQL需要先跳过前面所有记录。

按主键范围扫描始终走聚簇索引,无论扫描到第几亿条,查询性能基本都比较稳定。

这种写法还有一个优点,就是不要求ID连续。

生产环境跑了很多年的业务表,中间通常都会有大量ID空洞。有的是物理删除,有的是历史数据清理留下来的。

id>lastId 并不关心中间有没有空洞的,只会继续扫描下一批真实存在的数据。

如果按BETWEEN去切区间,这些空洞会导致不少批次查出来都是空结果,相当于白跑了一次。

迁移的时候,每批处理2000条,批次之间休眠100毫秒。

这些参数没有写死,而是放到Nacos统一管理。

如果发现写入服务压力比较大,就把批次调小一点、休眠时间调长一点;如果机器资源比较充足,就适当提高批大小,加快迁移速度。

整个过程不用改代码,也不用重启服务,配置推送后下一批数据就会生效。

另外,迁移过程中失败记录一定要单独保存。

RPC超时、历史脏数据、字段转换异常,这些问题都很常见,不可能保证一次全部成功。

我们专门建了一张迁移失败日志表,记录来源表、来源主键、错误类型、错误信息,以及原始数据的JSON快照。

这样排查问题就很快。

如果某一种错误突然大量增加,很容易就能定位到底是写入服务出了问题,还是迁移逻辑遗漏了某个场景。

问题修复以后,只需要针对失败记录重新执行一次重试,不需要把整个迁移任务重新跑一遍。

整个迁移系统真正依赖的,其实只有两张表。

一张记录迁移进度,一张记录迁移失败数据。

前者保证程序随时可以恢复,后者保证任何异常都有迹可循。

对于十几亿数据的迁移来说,建议都建立这两张表。