当前位置: 首页 > 科技观察

256转4096:如何通过分库分表扩容实现数据的平滑迁移?

时间:2023-03-11 21:22:06 科技观察

一、背景2020年,笔者负责的一个高德打车系统进行了分库分表扩容和数据库迁移。整体订单系统部署在阿里云上,服务使用阿里云ECS部署,数据库使用阿里云RDS,配置中心基于阿里云ACM自研,数据同步基于阿里云DTS自研-开发和自主开发的分库分表组件和分发TypeID组件等。本次扩库扩表的背景是,原来的4个实例4个数据库中有256张表,每张64张表,部分单表已经超过千万。单表会有上亿条记录,单表数据量过大会导致数据库性能问题。注:【内外兼备】Bomb指的是弹性计算。Bombinside和Bomboutside实际上指的是两个独立的弹性计算网络环境。丹内主要是指部署在阿里巴巴生产网络上的弹性计算环境。最早基于淘宝原有技术构建,主要用于支撑淘宝业务。子弹外面主要是指部署在阿里巴巴公有云上的弹性计算环境,支撑阿里巴巴的云计算业务。二、容量规划1、当前数据库和表划分4个实例(16C/64G/3TSSD),4个数据库(每个实例一个数据库),每个数据库64张表,共256张表。使用RDS后台一键诊断功能计算表空间使用情况(这里以测试环境数据库为例)。2、容量计算实例数数据库的瓶颈主要体现在:磁盘、CPU、内存、网络、连接数,其中连接数主要受CPU和内存影响。CPU和内存可通过动态升级升级,但SSD盘容量最大支持6T(32C以下最大3T,32C以上最大6T)。但是考虑到现阶段的成本,我们可以先将实例容量翻倍,使用8个实例(16C/64G/3TSSD),每个实例建4个数据库(databases),每个数据库有128张表(这里是实际上a在成本选择的过程中,理论上应该采用“多库少表”的原则,单库128表实际上太多了,推荐单库32或64表)。以后如果实例压力增加,可以升级实例配置(16C/128G、32C/128G、32C/256G等);成本更低。表数按照每表最大1000万条数据进行评估,4096张表可以支持3年每天5000w(10.1压力测试标准)和5年每天2000w的架构。(因为业务表较多,这里忽略单个数据大小的计算过程)数据库有32个,每个有128张表。未来可以扩展到最大32个实例,无需rehash,只需要迁移数据即可。阿里云RDS规格及价格一览3、由于分库分表的扩容,数据迁移涉及到rehash过程(表256到表4096),而阿里云DTS只支持同构数据库的数据迁移,所以我们使用DTS的binlog-to-kafka能力自研数据同步中间件。整个数据迁移工作包括:前期准备、数据同步(历史数据全量同步、增量数据实时同步、rehash)、数据验证(全量验证、实时验证、验证规则配置)、数据修复工具,ETC。。1.准备工作唯一业务ID在进行数据同步之前,需要先整理出所有表的唯一业务ID。只有确定了唯一的业务ID,才能实现数据同步操作。需要注意的是:业务中是否使用数据库自??增ID作为业务ID,如果需要,需要先对业务进行改造,还好没有订单业务。每个表都有唯一索引吗?梳理过程中,发现有几个表没有唯一索引。一旦表中没有唯一索引,数据同步时就会存在数据重复的风险,所以我们先根据业务场景给没有唯一索引的表添加唯一索引(可能是联合唯一索引)。顺带一提,阿里云DTS进行同构数据迁移,使用数据库自??增ID作为唯一ID。如果在这种情况下使用双向同步,会造成数据覆盖问题。也有解决办法。我们之前的做法是新旧实体使用自增ID奇偶来解决,保证新旧实例的自增ID不冲突。由于我们这次使用的是自研的双向同步组件,这个问题这里就不详细讨论了。分表规则sorting分表规则决定了rehash和数据校验的区别。需要逐表梳理表是分用户ID纬度还是非用户ID纬度,是否只分库不分表,是否不分库不分表,等等。2.数据同步数据同步的整体方案如下图所示。数据同步基于binlog,独立的中间服务进行同步,不侵入业务代码。接下来介绍各个环节。将所有历史数据与单个服务同步,使用游标从旧数据库批量选择数据,rehash后批量插入(batchinsert)到新数据库,这里需要配置jdbc连接字符串参数rewriteBatchedStatements=true来使批量操作生效。另外需要特别注意的是,历史数据也会不断更新。如果先开启历史数据全量同步,刚刚同步的数据可能不是最新的。所以这里的方法是先开启增量数据的单向同步(从旧库同步到新库)。此时只有积压的Kafka消息不会被实际消费;然后开始全量历史数据同步。当历史全量数据同步完成后,最后开始消费Kafka消息进行增量数据同步(提高全量同步效率,减少积压也是关键一环),保证数据迁移时的数据一致性。增量数据实时同步增量数据同步考虑了灰度流的稳定性、容灾、回滚能力,采用实时双向同步方案。一旦新库出现稳定性问题,或者在推流过程中新库出现数据一致性问题,可以快速回滚切换回旧库,保证数据库的稳定性和数据的可靠性。增量数据的实时同步是通过数据订阅自研的基于阿里云DTS的数据同步组件data-sync实现的。主要解决方案是DTS数据订阅能力会自动将订阅的数据库binlog转成kafka,data-sync组件订阅kafka消息,Filter,merge,group,rehash,分表,批量insert/update,最后提交offset等一系列操作完成数据同步。过滤循环消息:需要过滤掉循环同步的binlog消息。这个问题比较重要,后面会单独介绍。数据合并:对同一条记录的多次操作只保留最后一条。为了提高性能,data-sync组件在收到kafka消息后不会立即传输数据,而是先存储到本地阻塞队列中,然后再对本地队列中的N条数据进行数据传输操作每X秒由本地定时任务。此时N条数据可能是对同一张表的同一条记录的操作,所以这里只需要保留最后一条(类似redis的aof重写)。updatetoinsert:合并数据时,如果数据中有insert+update,只保留最后一次update,会执行失败,所以这里需要将update转为insert语句。按新表合并:将最终要提交的N条数据按照新表拆分合并,这样就可以直接按照新表的纬度进行数据库批量操作,提高插入效率。整个过程中有几个问题需要注意:问题一:如何防止异步消息乱序导致的数据一致性问题?首先,kafka异步消息存在顺序问题,但是需要知道的是binlog是顺序的,所以在详细传递kafka消息的时候dts也是顺序的。这里需要做的是保证一个库只能保证一个消费者的数据顺序和数据状态不会被覆盖,从而解决数据一致性的问题。问题2:会不会出现消息丢失的问题,比如重启消费者服务时?此处,不会自动提交偏移量。取而代之的是,每次消费数据最终录入数据库后,offset被异步存储在一个mysql表中。如果消费服务重启宕机等,重启后从mysql中获取最新的offset开始消费。这种方式唯一的问题可能是瞬间重复消费一些消息,但是由于上面介绍的binlog是顺序的,所以可以保证数据的最终一致性。问题三:update转insert时字段会不会丢失?binlog是全字段发送的,不会有字段丢失。问题四:循环消息问题。后面会单独介绍。上面说到rehash,因为256表改成了4096表,所以每条数据都需要经过一次rehash,重新分库分表计算。说到rehash,就得先介绍下当前订单数据的分库分表策略。用户ID后四位在订单ID中冗余,库号和表号由用户ID后四位哈希计算确定。在数据同步的过程中,从旧数据库到新数据库,需要获取订单ID中用户ID的后四位,4096,来确定数据库表中数据在新数据库中的位置;从新库到旧库,需要用用户ID的后四位四位模256来确定数据在旧库中的库表位置。双向同步时的binlog周期消耗问题想象一下,业务向老实例的一张表中写入了一条数据,从而产生了一条binlog;data-sync中间件收到binlog后,将记录写入到新实例中,所以在新实例中也生成了一个binlog;这时候data-sync中间件又收到了binlog……不断循环,消息越来越多,数据的顺序也被打乱了。如何解决这个问题呢?我们采用数据着色方案,只要能识别写入数据库的数据,让data-sync中间件写入而不是业务写入,下次收到binlog数据时,不需要再进行消息循环.因此,数据同步中间件要求每个数据库实例创建一个事务表。事务表tb_transaction只有id、tablename、status、create_time、update_time字段,status默认为0。回到上面的问题,业务往老实例的一张表写了一条数据,所以一个binlog生成;data-sync中间件收到binlog后,执行如下操作:#启动事务,使用事务保证SQL的原子性和一致性starttransaction;setautocommit=0;#更新事务表status=1,事务logo后面的数据开始上色updatetb_transactionsetstatus=1wheretablename=${tableName};#下面是业务生成binloginsertxxx;updatexxx;updatexxx;#更新事务表status=0,logo后面的业务数据失去颜色updatetb_transactionsetstatus=0wheretablename=${tableName};提交;此时data-sync中间件将上述语句打包提交给新实例,新实例更新databinlog后也会产生对应的上述语句;当data-sync中间件再次收到binlog时,只要判断遇到了tb_transaction表中status=1的数据,后面的数据就会直接丢弃,直到遇到status才会继续接收数据=0。确保数据同步中间件只会传输业务产生的消息。3.数据校验数据校验模块由数据校验服务data-check模块实现,主要基于数据库层面的数据比对,逐一校验各个数据字段是否一致。如果不一致,则按照配置的校验规则进行校验。重试或报警。全量校验基于旧数据库,查询每条数据在新数据库中是否存在,字段是否一致。基于新数据库,查询每条数据在旧数据库中是否存在,字段是否一致。实时验证定时任务每5分钟验证一次,查询旧库和新库最近5+1分钟的更新数据,做diff。差异数据校验两次和三次(由于并发和数据延迟的存在),如果三次校验不同就会报告警。4.数据修复数据校验后,一旦发现数据不一致,需要对数据进行修复。数据修复有两种解决方案。一种适用于大规模的数据不一致。重置kafka偏移量的方法用于重新消费数据消息并覆盖有问题的数据。二是适用于小规模的数据不一致。数据修复模块自动拉取data-check模块记录的sls日志进行数据校验,分析日志,生成同步语句,更新到目标数据库。四、灰度切换数据源1、整体灰度流量切断方案整体灰度方案:通过SP+用户纬度实现,SP纬度:依赖灰度环境切断,用户纬度:依赖用户ID后四位截流。灰度切割的过程必须配合停止写入(第二级)。为什么不写了,因为数据同步有一定的延迟(正常毫秒级),必须保证所有业务操作都在一个实例上,否则老数据库中业务刚刚修改了一条数据。这时候如果切换到新数据库时数据不同步,旧数据就会出现数据一致性问题。所以步骤应该是:先停写观察所有数据都同步了,切换数据源最后关闭停写,开始正常的业务写,但是测试环境毕竟和生产环境不一样。一旦生产环境数据库出现问题,可能就是一场灾难。上面虽然介绍了数据校验和数据修复的过程,但是服务稳定就是要在问题发生之前将其拦截。最重要的工作。因此,我们提出了ABC验证的概念,灰度环境ABC验证准备:购买两套数据库实例,当前订单库为A,新购买的两套分别为B、C。A到B配置DTS(dts支持不需要rehash的同构数据同步),B作为老库的验证库,C库作为新库,B和C作为生产演练确认。B、C演练完成后,配置A、C为Formal双向同步3、灰度切换步骤具体灰度方案及数据源切换过程:代码预先配置两套数据库分库分表规则.通过ACM配置灰度。代码拦截mybatis请求,对用户id后四位取模,与ACM设置中设置的灰度比进行比较,将新的库ID通过ThreadLocal传递给分库分表组件.判断当前是否存在灰度白名单。如果命中,新的libraryID会通过ThreadLocal传递给分库分表组件。分库分表组件根据ACM配置获取新分库的分表规则,进行数据库读写操作。切片时会配合ACM配置灰度比命中的用户停止写入。5、总结整个数据迁移过程还是比较复杂的,时间也不是很充裕(过程中还穿插了11次全链路压测改造)。在有限的时间里,我们会凝聚大家的力量,反复讨论,挖掘可能存在的问题。然后演示解决方案,任何一个可能有问题的环节都不要放过。再次强调,防患于未然是服务稳定最重要的工作。过程中还有很多细节,从数据迁移的准备到数据同步测试,从灰度流程确定到正式生产切换,尤其是结合业务和数据的特点,有很多细节需要完善予以考虑,本文不一一列举。出去。最终,经过近两个月的紧张工作,分库分表扩容和数据迁移工作顺利完成,没有业务代码入侵,零事故。