渲染中台垂直拆分总结

阅读量:

背景:

为了保证数据库更加合理和稳定,配合业务与发展的需要,渲染中台进行了大量的垂直拆分与迁库迁表。

这其中遇到的坑点也不少,一起总结整理下经验,形成一套流程化操作标准,降低日后的工作复杂度

一、前期分析与设计

1、有哪些表需要迁移

表的迁移拆分总共进行了三波,前后持续了大约8个月。由于中间涉及到了业务拆分,部分数据表还进行了二次迁库。

通过业务梳理后发现,总计有50多张表待迁移,另外发现有大量的共用表,即其他业务方也在用此表。迁移之前需要和业务方沟通确认同步迁移方案。

另外统计发现,部分数据表内容达到了上亿条,每天读写达到了上百万次,迫切需要分库分表。

2、影响范围评估

经过梳理发现,渲染核心服务都受影响,且影响范围较广。除了需要回归业务主流程外,其他如相机视角、商品替换、全景图、水电漫游、风格替换、智能漫游等核心逻辑同样受到影响。

对业务方的影响主要是因为共用表。经排查发现,少数几张表会存在业务方读写的情况,而其他表均为只读。那么我们在进行表迁移的时候,需要提前和业务方沟通好,双写期间完成新表的接入。

这其中重点便是需要保持新老表的数据一致性,在新老表同时进行双写阶段后推动业务方接入到新的表中。

3、测试手段

测试手段:影响范围确定后,即可以从测试角度思考如果保障整体迁移过程中的稳定性。可以从以下几个方面分别去实践:

  • 数据层面,迁表过程中的数据一致性。这里既需要验证DTS数据同步后的一致性,还需要验证双写过程中的数据一致。
  • 服务层面,接口测试。受影响的服务较多,每次验证时需要保证当前接口测试通过且结果一致。这里可以考虑使用diff平台进行流量回放。
  • 业务层面,主流程回归。观察核心服务各个功能是否依旧完善。一般建议内网迁移后可以在sit观察一段时间,充分暴露出问题。

二、整体流程

1、数据库迁移流程

数据迁移过程,基本流程见下图:

  1. 双写代码上线。开关支持双写切换、读切换,且已在内网验证过;
  2. 存量数据同步。先将已有的数据同步到新表中;
  3. 执行双写。打开开关同步写入新老表。默认情况下新表自增主键id来自老表。
  4. 写顺序切换。改写入顺序为先写入新表,老表自增主键id来自新表。
  5. 二次数据同步及增量同步。双写期间,通过增量数据同步实现老表对新表的覆盖,避免双写失败而带来的数据不一致问题。
  6. 切换读表。改为读新表。如有业务方依赖,也需要在此期间推动业务方完成切换。
  7. 停写旧表。停写旧表后观察一段时间,若无问题可以下线老表及迁移代码

所有迁表需要在内网验证通过后再去外网实践,实践过程中需要关注数据的一致性及开关操作。

迁表一般考虑不同情况采用的方案也会不一样。常见的迁表方案有单到单,单到多,数据重构订正。

2、迁移过程中的关键点

  1. 第一个需要注意的地方就是支持迁移的代码及其开关上线。数据同步本身的过程相对简单,但是数据源订阅的切换及顺序操作才是风险最大的地方。因此业务逻辑代码实现必须首先得到保障。
  2. 第二个需要自增主键的来源。一般情况下原数据表是默认有效且可靠的,那么迁表初期自增主键取自旧表;当数据同步完成且校验一致后,自增主键id则可切换到新表。当然如果数据表比较简单且业务量不大,那么选择深夜直接切换会相对比较简单。
  3. 第三个问题也是实现过程中最困难的一点,写顺序切换。因为外网和beta共用数据库,两个环境顺序不一致就会有可能导致自增主键冲突写入失败,两个环境时刻都在大量写入操作,风险较大。

三、方案细节

1、数据同步

数据同步一直是拆分最重要的工作。目前我们进行迁移的话会需要采用DTS或者数据库日志binlog同步存量数据。

一般建议至少同步两次,一次是原始数据的初步同步,一次在执行双写后执行同步,保证数据的完整覆盖。

进行迁移的话会需要采用dts或者数据库日志binlog同步存量数据的过程,这里根据自己公司的技术栈来选择 存量数据写入数据库的时候带上主键

INSERT INTO tablename(field1,field2, field3, ...)
VALUES(value1, value2, value3, ...)
ON DUPLICATE KEY UPDATE
field1=value1,field2=value2, field3=value3, ...;

2、数据一致性验证

对于简单表,数据校验主要有两种。

  • 数据量不是很大的话可以直接在dbv中对比验证。我们只进行了数据量大小的校验,没有进行字段比对,没有严格做到位。
  • 执行数据字段校验。在低峰期通过脚本对两张表的数据字段逐个校验。
  • 对于数据量比较大的,包括分库分表的,建议交给大数据组或者dba去验证,耗费时间较长。
  • 第三种方法比较简单粗暴,直接对存量数据进行多次同步,然后对数据总量进行校验,基本可以保证数据同步的准确性。

如果时间允许的话,也可以进行数据库的增量校验,确认双写逻辑完整。

3、业务验证

  • 开关切换的时候首先要验证业务可用,核心链路不受影响;同时可以根据调用链查看数据的插入先后顺序,确认开关是否生效
  • 核心服务的接口测试一直在持续集成中,可以用在迁表过程总的接口验证,需在每次上线前验证接口测试执行结果通过。从业务回归的角度来说,可以降低不少人力成本。
  • 另外diff平台可以持续使用起来,通过对比验证确认不同环境返回的数据一致。

4、自增主键的插入问题

(1) 老表如果是单表自增的,新表是单表自增的话

找流量低的时候切换读写顺序,如果业务需要高度一致性,加分布式锁,需要额外的开关来决定是否走有锁的逻辑,顺序切换以后,关闭那个控制是否走锁的逻辑的开关

但是加锁会让实现逻辑非常复杂,同时会严重降低写入速率,性能瓶颈较为明显。

(2) 老表如果是单表自增的,新表是分表的话

分库分表的自增主键都会有一个sequence表,一般我们会采用跳区间的做法。在进行切换的时候,预先分配一个较大的主键id,切换后当前环境直接从新的区间开始。在跳过的区间段消耗之前完成外网环境的切换,即可避免主键冲突。不过这里会需要3次跳区间。过程确实会比较麻烦,这也是我们实际迁移过程中用到最多的地方。但是好处是可以在beta进行充分的业务验证测试。

1) 确定旧表的最大id。记T0时刻当前的id为old_id ,设定一个delta 为10,

旧表执行sql第一次跳区间,修改为 old_id + delta * 5 ,调到一个足够大区间

alter table users AUTO_INCREMENT = old_id + delta;

这个时候旧表新写入的id从150开始,同时会将自增主键的id赋给新表。

这个阶段我们记为Stage 1.


2) 此时为避免T0时刻产生的数据值消耗,新表也需要单独跳一个区间。新表的主键值改为 old_id + delta

这一步,分表的时候改的是sequence表的值,此时 update sequence

update sequence set value = old_id + delta

这个时候可以是新表先写入数据,然后数据同步到老表。新表的id从110开始。

这个阶段我们记为Stage 2。一般这个阶段我们就可以在beta进行验证,而且两个环境先后写入的顺序是相反的。


3) 此时我们可以在beta上打开开关,需要重启服务重置sequence ,原因是sequence会在机器启动的时候预分配

 第一步做完的时候,旧表先插入,新表后插入 id 在 (150,++)

 第二步做完的时候,新表先插入,旧表后插入 id 在    (110,  150)  这个区间里

delta取得足够大,就不会有冲突,   而且不影响旧表的主键值。如果出现问题,闭开关即可,无需其他操作

4) beta验证通过,可以执行外网切换顺序。此时新表需要进行第三次跳区间,需要超出线上当前最大id值

update sequence set value = old_id + delta * 200

这个时候,外网会从2000这个id开始写入新表然后同步到老表,由于自增主键的特性,beta也会从2000+开始写入新的id

这个阶段我们记为Stage 3,也是跳区间的最后一步。之后所有的写入操作都是一致的了。

5) 这里需要注意的是,每次跳区间一定要预留充足的空间,首先是因为分库分表意味着线上每天写入的流量很大,最多每天可以上百万的写入;另外切换的过程会持续比较长,并且有条件的话可以在beta运行观察一周后再去外网执行切换。一般建议预留空间为预计使用量的2-3倍较为稳妥。

(3)新老表都是分表

许多分库分表都会有sequence表,那么自增主键来源就唯一确认了。对于写入先后顺序无所谓了,全部从sequence表同步过来即可。

那么对于数据量比较大的单表,也可以改造成依赖sequece表,然后迁移。

当然如果主键不是自增的,那么就无所谓先后顺序了。

(4)直接做线上切换是否更简单

之前确实会遇到这种疑问,如果不在beta做验证,内网验证可行性后,直接beta prod一次性切换写入顺序是否可以?

我们认为对于一些流量较小的表或者配置表,这么操作时可行的,如果有异常,直接切换回去。

但是内外网数据始终是两个不同的数据源,不同的环境,且对于流量非常大的数据表,本身数据方案的可行性、数据的一致性,都需要做一个充分的验证;在beta做验证操作甚至观察一段时间是非常有必要的。

直接切换的方案确实比较简单,但是风险相对较高。

而跳区间的做法多次更改线上数据库,确实也存在一定的风险;另外还存在两个环境写入顺序不同,sequenceid主键冲突的风险。

这其中的方案与风险需要由组内充分评估后选择,没有绝对的方法。渲染中台也是在多次迁表的过程中不断优化自己的方案。

四、开关控制

开关控制在整个迁表过程中也是非常重要的事情。

开关验证需要在sit、beta进行两次验证后,再到外网进行操作。故持续时间会比较久。

开关操作一般是在所有代码完全灰发上线后进行操作,一般有四次。

  • 第一次,进行双写。当然这一步可以在灰发时直接进行,但不是很建议,因为新表数据并不是连续的。稳妥起见,一般建议晚上上线后再打开开关进行双写。
  • 第二次,读写顺序切换。写入双表时会有先后顺序,这里涉及到的一个问题就是自增主键的来源。一般有两种方式。
  • 第三次,即为读表切换,由读旧表切换到读新表。在这之前,检验完成数据集后,可以直接切换,风险较小。
  • 第四次,则为停止旧表写入。这样整体旧表可以停止下线了。一般建议切换后继续观察一段时间,线上稳定运行后,可以通过改表名的方式逐步drop掉。

五、代码设计细节

最后就是代码双写逻辑的实现了。由于每个服务会有多处读写数据表,影响范围很广。执行完双写和读写切换后,还需要再次去掉冗余代码,这又会额外进行一次代码变更。

渲染中台的方案是自己实现了一个代理,绕过MapperProxy,直接通过 SqlSession 去执行,同时我实现一个 mapper类的代理对象去替换掉业务代码里面用到的mapper对象,从而实现基本无侵入性, 迁移完了以后代码还是需要改一下包名

这里以一个UserMapper为例

DbHandler 整体架构如下,包括写入先后开关、读开关等

很多细节这里暂时就先略去了,大家可以自己思考下怎么写,毕竟这个比较偏向业务,渲染就是直接获取 sqlSession, 然后通过 MapperSignature这个内部类来完成mybatis的接下来的工作。下面这里给出一个 invoke和insert方法的简要实现 , 细节大家看下注释,这里是实现插入数据库最关键的地方

mybatis xml插入的时候判断下userId是否为null,是null就自增否则直接插入

这样一来的话 原有的业务代码里面基本不需要我们自行修改代码,降低了我们改动代码的风险和工作量。

关注我们

酷家乐质量效能团队热衷于技术的成长和分享,几乎每个月都会举办技术分享活动(海星日),每半年举办一次技术专题竞赛分享(火星日),并将优秀内容写成技术文章。

我们尽可能保障分享到社区的内容,是我们用心编写、精心挑选的优质文章。如果您想更全面地阅读我们的文章,请您关注我们的微信公众号"酷家乐技术质量"。


comments powered by Disqus