分享一次海量数据平滑迁移实战
背景
采购系统(BIP)在经历多年演进后,系统整体复杂度和数据量俨然已经极具规模,本文着重讨论海量数据的治理
存储现状:工程端实时订单库采用 MySQL 5.5 集群,其中主库配置为 32C/48G/6000G,无法归档的订单热数据占磁盘空间 85%(5.1T)
痛点:6T 磁盘已经单容器最大,无法继续扩容,剩余磁盘余量过小,难适应未来发展
目标
降低磁盘容量,优化数据模型,提升系统稳定性
调研
首先,既然是要解决存储容量的问题,就要对详细的容量情况有个更加清楚的了解。总结下当前存储容量问题,最大的表是订单操作日志表 lifecycle 共 1.3T,大于 500G 的表 2 张共 1.5T;100G~500G 的表 10 张共 2.6T。以下是优化前库里大表(大于 100G)的详细空间占用情况:
其次,确认当前最高效的优化思路是将 lifecycle 表迁移到其他库,原因有二:1.lifecycle 表的含义是操作日志,在业务上不算订单域内最核心的模型,风险可控;2.占用空间大,单表 46 亿行数据,空间占用 1.3T,一张表占了磁盘空间的 22%,优化的 ROI 高
最后,想说明下,为什么没有直接将整个库,从传统 MySQL 切换到 JED,原因也有二:1.JED 和 MySQL 的查询语法还是有一定差异,直接切换,成本和风险极高;2.切换存储中间件,获取分布式架构下更大的存储空间并不是银弹,理智告诉我们要结合系统现状,不可盲目下定论
挑战
保障海量数据(存量 46 亿行,增量 600w+行/天,TPS 峰值:500+,QPS 峰值:200+)迁移期间读、写稳定和准确。 需要补充一下:lifecycle 虽然不算订单最核心业务模型,但依旧是辅助业务决策的关键数据,也非常重要
例子:
方案
整体方案
数据同步 -> 双读 -> 双写 -> 离线验证 -> 数据清理
详细设计
•数据同步,通过 DRC 实现,历史全量+增量,其中有以下几点使用心得:
◦同步速度问题,本次是使用传统 MySQL5.5 -> JED 底层 MySQL 8.0 单表同步,效率大概是 4M/S,一共花了 3 天半左右
◦数据同步过程中不要操作暂停,否则任务重启后,会重新同步历史数据,导致数据同步周期变长。详情参考: 关于全量任务暂停重启之后数据同步慢的原因
◦字段兼容问题,老库历史时间字段类型是 datetime,新库需要改为 datetime(3),这种数据同步是可以兼容的(下文会讲为什么要优化时间字段精度)
◦数据验证问题,当时在历史数据全量同步完毕后开启了 DRC 数据验证,但是许久未执行完成,收到 DRC 运维告知出现大量报错,最终结论是暂时不支持这两个版本的数据比对(5.5->8.0),这也是为什么整体架构上采用 BDP 抽数比对数据的主要原因
•数据验证,业务程序完成双写、双读改造
◦双写
▪为什么采用双写?答:控制风险。 1.团队内还没有应用直接写入多分片 JED 的先例,而且新、老库的底层 MySQL 版本也差异比较大(5.5 vs 8.0),当时通过分批次灰度上线完成逐步切量验证;2.方便进行数据验证,lifecycle 是业务操作日志,基本涵盖了所有的写入场景,其中因为历史问题,不乏一部分逻辑和订单更新在同一事务中,现在迁移到新库,本地事务会存在不生效的场景
▪具体改造方案:
▪新增【验证开关】 ,开启后新/老库双写,另外需要要引入 vitess 驱动,目前只支持 JDK8 及以上
▪新增【上线开关】 ,开启后只写新库,此开关是在验证逻辑无问题后,最终切换的开关,代表迁移完成
▪注意,开关改造完成上线后,“全量+增量 DRC 任务”在验证期间是一直启用的,也就是说验证期间,增量数据会写两份到新库
▪一部分是实际的生产数据,一部分是待验证的测试数据。那么就带来另一个问题,如何识别和区分这两部分数据,我们采用的方案是:JED 建表指定趋势自增的最小 id(200 亿)+【验证开关】开启的时间戳进行区分
▪如下图,其中 A 和 A'都是【验证开关】切换后的增量数据,由于老库的 id 已经自增写到了 70 亿,并且 DRC 同步任务也是指定 id 写入,所以建表时指定新增数据 id 是 200 亿(详情参考: 数据库自增ID列设置 ),和老数据之间存在一定 gap 方便识别。BDP 脚本数据比对的也是:老库.A 和新库.A'(这里默认 DRC 增量同步的数据是准确的)
▪清除测试数据,真正完成【上线开关】切换,需要提前清除测试数据,只需指定 id>200 亿的物理删除即可。注意:针对多分片的 JED 物理删除 delete 语句,我们程序上如果为了防止大事务,而采用“for 循环+limit n”的方式执行,实际的每次 SQL 语句执行结果是多个分片的 n 的聚合,而不是 n,如果程序上对结果有判断逻辑,需要额外注意
◦双读
▪整体逻辑基本复用写入期间已有开关,其中针对新库当中 DRC 实时同步的数据(上图:新库.A)会根据开关开启时间进行过滤
▪其中,在验证期间,新、老库都会根据采购单号进行查询并实际返回老库的查询结果,其中还会进行结果比对,出现数据不一致会输出异常日志关键字
▪另外,因为 lifecycle 操作日志数据是有先后顺序的,老库的处理方式是根据自增 id 进行倒排,到了新库以后,由于采用的是 JED 分片(分布式存储的磁盘空间更大),考虑到开发成本,数据 id 采用的是趋势递增的自增主键(详情参考: Vitess全局唯一ID生成的实现方案 ),这时多集群并行写入无法继续使用基于 id 倒排的方式返回结果(后写入的数据可能 id 较小,可以参考 sequece 发号器的 ID 生成),所以将原始的数据写入时间戳从 datetime 提高精度到 datetime(3),通过数据写入时间进行倒排,这里也解释了上文,新库 DRC 数据同步为什么要考虑字段兼容的问题
▪补充 1:这里基于时间倒排在业务上是准确的,因为 lifecycle 数据是根据订单号进行分片的,所以同一订单一定落在单分片上,也就是说不存在不同分片时钟偏移的问题,单订单的操作日志的时间序列一定是按照写入顺序逐渐增加的
▪补充 2:新库字段类型变更(datetime->datetime(3)),32 分片,共 46 亿行数据,执行了大概 1 小时,期间主从延迟最高 30 分钟,容器负载正常
▪补充 3:应用的关键字告警配置,日志文件仅支持以 error.log、err.log、exception.log 结尾,并开启历史日志的路径
▪最后,双读期间共通过业务的实际查询流量发现数据不一致问题 2 个+,在并未影响到业务使用的前提下及时发现了系统异常
◦离线验证
▪lifecycle 归根结底还是写多读少的业务场景,为了防止出现上文数据比对验证的遗漏,我们会采用 BDP 离线任务会分别开启增量数据+历史全量数据验证。通过对新、老库的全量数据字段相互 sql inner join 的方式完成比对,其中会忽略 id 和写入时间,因为新库的 id 不是单调递增、时间精确到了毫秒。期间共发现有效数据问题 3 个+,均是因为本地事务回滚导致的数据不一致的场景
•收尾工作
◦完成【上线开关】切换,只读、写新库,完成整体平滑迁移。在无 QA 参与前提下,验证期间未出现过数据丢失、重复、错误等异常
◦切换完成后,老库老表和 DRC 同步任务依旧保留了一周的时间,防止出现场景遗漏,产生数据丢失
◦46 亿行大表清理,采用 drop+create 的方式实现效率、稳定性更高,在业务低峰期完成脚本执行,大概花费 10 秒的时间,容器负载、内存等指标正常。但是当时碰上了 DBA 的备份任务,导致有一个从库主从延迟升高,这个后续需要注意
版权声明: 本文为 InfoQ 作者【京东科技开发者】的原创文章。
原文链接:【http://xie.infoq.cn/article/16e54a197141f5da0716fcc1b】。文章转载请联系作者。
评论