开发工作中,可能会遇到如"大表拆分"、"跨库数据迁移"等场景,本文介绍互联网常见架构下的数据迁移方案及实现;
以下是需要数据迁移的场景业务场景;
由于历史原因,为了业务快速迭代,设计之初未考虑用户的规模,对于如用户权益、用户订单的数据,仅做了单表存储;
随着业务规模快速增长,单表数据膨胀,读写压力变大,可能出现单条慢SQL拖垮整个表的风险,以至于使整个业务不可用的情况,因此,需要将存储大量数据的单表拆分为多表(分库分表);
业务发展之初,业务模块小,多个业务都放在同一个数据库中;随着业务规模的增长,当初聚合的多个业务模块要做业务交割,交由不同的团队维护和迭代,各个业务都有自己的库表;
为了防止不同业务之间,可能由于其他业务异常拖累整个数据库,要做业务存储的物理隔离,因此需要将旧库表的数据迁移到新的库表;
举个例子,用户的支付订单的数据都保存在支付中台,由于历史原因,当前业务方的部分业务订单未做存储;
现在要做一个支付管控版本(如未成年人防沉迷专项),需要依赖订单中台来协助实现功能,而跨部门的需求对接周期长成本高;因此需要 [拉取并实时同步支付中台的订单数据] ,当前业务得到这部分数据后,就可以自行实现上述的跟订单相关的功能,从而将交易管控能力(实名制/防沉迷、商品限购)收敛在当前业务团队,提升需求版本迭代效率,减少跨部门的开发、测试之间的沟通、合作、资源等待问题,达到更快速响应业务需求目的;
有损方案指的是停机变更方案,因为需要一段时间停止服务,因此对于业务是有损的;可以按照如下步骤执行:
(1)研发同事通过观察历史的用户流量监控,选择流量较小的时间段执行变更来尽可能减小业务影响,如凌晨4点开始运维;运营同事前一天在网站或者 APP挂个公告,说次日4点到早上6点进行系统升级,无法访问;
(2)接着到凌晨4点停机,系统停掉,关闭用户流量入口,此时老数据库表不再有变更产生,属于临时静态数据;然后通过提前得写好的一个数据导出导入工具,将老数据库表的数据按序读出来,写到分库分表里面去;当然,为了提升数据导出速度,可以使用多线程分段读写数据,甚至可以联系DBA直接物理复制数据库表,然后再执行rename;
(3)数据导出之后,通过修改系统的数据库连接配置,重启服务,连到新的分库分表上去;模拟用户请求验证一下对数据的读写,这时发现如预期一样,这时打开用户流量,再观察一段时间,确认无误后,伸个懒腰回家调休;
说到停机迁移这种方案,可能有些同学觉得很low,但是实际上目前存在相当一部分迁移方案都是使用凌晨停机迁移的方式(例如有时候会看到停机维护公告),因为这种方式成本低简单粗暴、无需考虑新老库数据临界情况不一致的问题、整个迁移周期非常短不存在灰度放量与多次修改代码发布的情况,缺点就是短暂业务有损,需要DBA、研发值班;
平滑迁移也叫无损迁移,服务在迁移过程中不需要停机,最多只需要短暂的重启时间,对于业务几乎没有任何影响的;双写方案,就是平滑迁移方案,简单来说,就是修改线上代码,之在前所有写老库(增删改操作)的地方,都加上对新库的增删改,这就是所谓的双写;
然后系统部署之后,由于我们只处理了某个时间节点之后的增量数据,新库数据整体数据差太远,用之前说的数据导出导入工具,跑起来读老库数据写新库,写的时候要根据类似gmt_modified字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写,简单来说,就是不允许用老数据覆盖新数据,新库的数据总是最新的;
导完一轮之后,有可能数据还是存在不一致,保险起见,通过定时任务做多轮新老库的数据校验,比对新老库每个表的每条数据,接着如果有不一样的,按照以最新数据为准的原则,重新从老库读数据再次写新库,直到两个库的数据追平,完全一致为止;
接着当数据完全一致了,切换为仅读写新库的代码,重新部署服务,重新部署后,数据的读写全部都落在新库中;求稳起见,这一阶段往往建议通过灰度策略,把用户的流量(仅读写新库)慢慢的切换到新库里,注意灰度的用户放量只能逐渐加大,不能回滚,因为灰度命中的用户数据都落在了新库,而老库没有;最终放量100%,迁移完成;
相对有损迁移方案,这种平滑迁移方案无需长达几个小时的停机时间,对业务的影响几乎没有,所以现在基本做数据迁移都会偏向这种方案;
这种迁移方案一般应用于特殊的业务数据,这类业务数据的历史数据价值不大具备有效期属性,如用户的优惠券数据;例如有这样的一个业务场景:用户优惠券表由于历史原因未做分库分表,目前数据增长过快,查询、写入性能变差;该业务属于部门核心业务,每天都有大量的用户流量和数据写入;为了保证业务的可用性需要对优惠券大表做拆分;
针对以上业务场景,考虑用户优惠券本身的特性(存在有效期),采取以下方案:灰度期间,用户优惠券新增数据全部保存至新表中,旧表的历史数据不迁移仅更新,共存阶段同时查询新库和旧库数据做数据聚合,运行N个月后(旧库数据已全部业务失效)用户请求的读写全部切到新库中,旧库直接作为归档;
该方案的特点就是历史数据天然的无需迁移,方案逐渐放量、无需考虑回滚,适用于解决大表拆分问题;
双写方案,总的来说就5个步骤:
1. 双写,增量数据同步;
2. 历史数据同步;
3. 定时任务校验数据一致性并修复;
4. 灰度放量,异常时可回滚;
5. 功能全量,完成平滑迁移;
首先,配置新库和老库两个数据源Datasource,来分别执行对老库和新库的读写;对于写操作的实现,有两种方式:
(1)修改业务代码,在写老库的地方,加上一句对新库的写操作;这种方法将写新库与原业务操作耦合到了一起,修改代码的位置可能非常多,业务代码会改的很混乱,并且当老库写成功,新库写失败时,需要考虑如何补偿,至少不能影响老库已经成功的业务逻辑,这种方法不推荐;
(2)因为业务请求只在数据迁移完成后才切换,所以并不要求老库与新库的写操作同步执行;因此可以考虑使用消息中间件解耦,如记录“对旧库上的数据修改”的日志持久化并丢入消息队列,然后按序消费队列里面的所有消息;
(3)借助公司大数据团队提供的binlog解析采集工具,如基于开源的canal binlog parser模块二次开发的binlog接入工具,通过MySQL主从复制协议去业务db捕获增量变更数据(模拟MySQL slave,通过socket连接去拉取和解析数据,对于MySQL的性能损耗很小,按照MySQL官方的说法,损耗为1%),解析成json格式,写入kafka,最终由业务消费数据,按照自己需求写入hive,hbase,es等等异构数据源中;),作为业务方只需要消费数据库增量变更的Kafka消息即可;
对旧库上的数据修改的同时,在新库上进行相同的修改操作,这就是所谓的“双写”,主要修改操作包括:
(1)旧库与新库的同时insert;
(2)旧库与新库的同时delete;
(3)旧库与新库的同时update;
由于新库中此时是没有历史数据的,所以双写旧库与新库中的affect rows可能不一样,不过这完全不影响业务功能,因为此时还未切库,依然是旧库提供业务服务;
新库写操作的数据变更原则为:
1. 对于单条数据的重复变更,以最新的数据为准,不能用旧数据替换了比它更新的数据;
2. 双写时,只有当老库执行写操作成功,才会对新库执行操作;
3. 新库执行写操作失败不能影响旧库的写操作成功的结果;
接着,执行历史数据迁移定时任务,由于迁移数据的过程中,旧库新库双写操作在同时进行,怎么保证数据迁移完成之后数据就基本一致了呢?可以参考下面的图来做一个说明;
上方是旧库中的数据,数据量还在随时间线持续写入;下方是新库中的数据,边通过迁移工具同步历史数据,边处理旧库中新发生的增量数据(这里的增量数据指的是双写时,新库的写操作导致的数据变更,不仅限于insert操作);
下面针对数据迁移过程中,由于双写同时产生的修改操作,讨论下该如何处理:
1. 旧库执行insert操作后
新库执行insert操作,操作一定能成功(因为新库的数据是旧库的子集,旧库既然可以插入成功,那么新库也可以插入成功),此时旧库新库都插入了数据,数据一致性没有被破坏;
2. 旧库执行delete操作后
根据删除的数据所处的区间,分为两种情况:
情况(1):假设这delete的数据属于[start, current]范围,即已经写入了新库,则旧库新库都删除了该条数据,数据一致性没有被破坏;
情况(2):假设这delete的数据属于[current, latest]范围,即还未写入新库,则旧库中删除操作的affect rows(影响的行数)为1,新库中删除操作的affect rows为0;
但是数据迁移工具在后续数据迁移中,因为这条"未来的"数据已经从旧库删除,因此并不会将这条旧库中被删除的数据迁移到新库中,所以数据一致性仍没有被破坏;
3. 旧库执行update操作后
根据更新的数据所处的区间,分为两种情况:
情况(1):假设这update的数据属于[start, current]范围,即已经写入了新库,则旧库新库都更新了该条数据,数据一致性没有被破坏;
情况(2):假设这update的数据属于[current, latest]范围,即还未写入新库,此时新库将这条update操作改为insert操作即可;数据迁移工具在后续数据迁移这条数据时,由于这条数据的更新时间与新库中已插入的这条记录的更新时间相同(同一条数据),因此不会执行迁移替换,所以数据一致性仍没有被破坏;
也可以这么理解,可以认为update操作是一个delete加一个insert操作的复合操作,结合上面对于inser和delete的分析,所以数据仍然是一致的;
特殊情况:
1. 数据迁移工具刚好从旧库中将某一条数据X取出,准备执行迁移插入新库;
2. 在X插入到新库中之前,旧库发生了写操作(delete),旧库删除了这条数据,affect rows为1,新库由于此时还没有这条数据,affect rows为0;
3. 双写完成后,数据迁移工具再将X插入到新库中;导致新库比旧库多出一条"本应被删除的"数据X,导致数据不一致;
因此,为了保证数据的一致性,切库之前,新库和老库的数据校验是必要的!
在数据迁移完成之后,需要写一个数据校验和修复的数据校验的定时任务,将旧库和新库中的数据进行比对,完全一致则符合预期;如果出现某种极端情况下导致的老库新库数据不一致情况,则以旧库中的(最新的)数据为准;
这个定时任务跟数据迁移写法类似,只不过前者是遍历查旧库写新库,后者是遍历查旧库,然后用旧库结果去查新库,做数据对比与修正;整个过程依然是旧库对线上提供服务,因此没有任何操作风险;
数据校验和修复的定时任务可以执行的久一点,一段时间后都没有出现旧库与新库数据存在不一致的告警时,可以开始逐渐灰度了,即将命中灰度策略的用户请求全部走读写新库,放量可以慢点,直到所有用户请求都路由到新库为止,则数据迁移完成;
我在工作期间,设计过1.1大表拆分和1.3数据同步这两种业务场景的技术方案;这里介绍下在做数据同步时我用到的方案,仅供参考;
背景:组内做了一个针对部门业务的订单小中台,负责对接公司的支付系统(下单、回调、重试),目的是减少部门各个业务模块对接公司支付中台的成本,屏蔽如订单重试补偿的逻辑;此外,将部门内业务订单数据收拢,方便做整个部门业务的支付管控;
任务:对于已经自主对接公司支付中台的部门内业务,需要把这些业务存储的业务订单数据迁移到订单小中台;数据跨库,且新库的表结构与旧库不同;
方案:
(1)binlog采集工具:公司大数据团队做了一款基于canal binlog parser模块二次开发的binlog接入工具,它通过MySQL主从复制协议去业务db捕获增量变更数据(模拟MySQL slave,通过socket连接去拉取和解析数据,对于MySQL的性能损耗很小,按照MySQL官方的说法,损耗为1%),解析成json格式,写入kafka,最终由业务消费数据,按照自己需求写入hive,hbase,es等等异构数据源中;
(2)工具接入步骤:
1. 业务方需要为被采集binlog日志的数据库,申请一个离线从库,并开启这个从库的binlog开关、开启row full 模式、开启gtid,获取当前的GTID信息或者点位文件信息;也就是说只采集位点之后的binlog,对于位点之前的biglog,可以新建一张表并用同样方式监听其binlog,然后将位点前的旧库数据按序写入;
2. 配置采集任务,分别采集旧库位点前后的binlog,binlog event会被处理成方便解析的数据丢入kafka broker集群;并且,会对每条binlog event分配一个全集唯一的版本号,并且保证后面产生的binlog的版本号比前面的大,从而为业务方提供了一种数据顺序性;
3. 业务方只需要写一个消费kafka消息的listener即可;需要注意的是消息可能是乱序的(位点前后的binlog一起消费);
数据同步的消息处理逻辑如下:
历史数据迁移完成后,还是需要一个定时任务来处理新库和老库不一样的数据;
参考:
数据迁移方案 - CSDN
100亿数据,非“双倍”扩容,如何不影响服务,数据平滑迁移? - 掘金