searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

数据源切换二三事

2024-05-24 09:40:10
19
0
  给停在路边的汽车更换轮胎容易,给运行中的飞机更换引擎则异常困难,如果新轮胎的卡口还和旧的不一样,新引擎的悬挂布局也与旧的不尽相同,那事情就很可能从简单变复杂,从复杂变不可能,应用的数据源切换也是如此。

  在业务的发展过程中,随着访问量级和数据量级的增长,升级数据源,或者更换数据源,都是一件稀松平常的事情。比如,为了扛住流量而增加缓存,为了长期稳定而更换数据库引擎,或者为了平滑负载而进行库表拆分,又或者因为业务迭代就重构了存储的数据,等等。
  这些都说明应用数据源变更场景的普遍性,总结下来有几类:
  • 数据缓存。在原有数据源上增加更轻量、性能更好的数据存储。缓存具有比较成熟的使用模式,有全托管的Cache-As-SoR(SoR,System-of-Record,也就是数据源),也有按需分配的Cache-Aside,不同缓存使用模式主要是封装策略的不同,本质上都是穿透、加载、淘汰的管理,角色上更多是对下层数据源的访问增强,一般着墨于数据一致性,而不归结为数据源切换。
  • 同源同构。相同数据源类型,相同数据存储结构,主要在水平拆分场景,如分库分表。
  • 同源异构。相同数据源类型,不同数据存储结构,主要在垂直拆分场景,如大字段剥离和服务拆分。
  • 异源同构。不同数据源类型,相同数据存储结构,主要在存储引擎替换场景,数据结构可兼容,如MySQL切PG。
  • 异源异构。不同数据源类型,不同数据存储结构,同样在存储引擎替换场景,数据结构不可兼容,如MongoDB切MySQL。
  现实总是比理论复杂,很少给我们按图索骥的机会。拿数据量增长的场景来说,可能最终不是单一地用水平拆分的方式来解决,或许会同时加上缓存,再把大字段分离,然后又将冷数据迁移到大数据存储。不过现实又比理论多了时间的维度,允许我们逐步地拼接积木,所以也不用过分担心混合解法的复杂,从整体来看是方案的嵌套组合,从过程来看又是各种方式的有序串联。

  变更一个生产应用的底层数据源,需要做好两件事情:数据迁移和连接切换。这两件事情既有各自独立的事项,又有相互依存的情况。
  数据迁移的目标是将原有数据根据规则重新分配到新数据源,包含数据传输领域相关问题。由于数据产生的持续性、数据结构的多样性和数据一致的安全性等因素,需要考虑的核心功能点包括:
  • 同步模式。是实时同步还是定时增量,虽然同步模式从远期结果上看不影响业务数据的完整性,但由于影响新旧数据源间差集的收敛速度,可能影响业务切换的平滑程度,或者影响实现平滑切换的边际成本。
  • 数据映射。是同构迁移还是异构迁移,传统的数据传输服务一般包括库表列名映射和数据类型映射,而业务数据源切换可能还伴随数据结构的调整,因此还需要实现映射插件,以支持迁移的汇聚(多对一)、发散(一对多)和转换(一对一)功能。
  • 数据校验。校验既是对差集大小的检查,也是对映射结果的核对,主要有全量、增量和结构校验,方法一般是确定的双边稽核,复杂度在于映射规则的正反匹配。另外,有些时候异构数据间是极难做一对一匹配的,再者,静态的数据匹配也不代表业务运行表现必然一致,在这种情况下,通过将业务流量在新数据源副本上重放,来观察业务表现是否符合预期,也是数据一致性校验一种方式,这需要实现业务的流量录制和重放,以及基于业务日志的数据分析方法。
  • 数据订正。一般来说,由同步延迟造成的数据校验差集是不需要特别处理的,最终都会收敛,但它会干扰准确性的判断。可以用二次校验来尝试进行规避,即对当次检测出来的不一致数据,在一定时间后再次核对,以此确保问题不是由增量延迟所导致的。对于确切不一致的数据,重传是数据订正的基本方法,根据异常范围,需要实现位点(回拨时间)和单点(指定数据)重传。


  做好数据迁移只是给应用准备了一个新轮子,如何给应用装上去,落地后能否跑得平稳,在越来越强调业务连续性的当下,才是数据源切换的重中之重。
  数据是流经业务百骸的血,是驱动业务引擎的油,但连接只是短短的一行文本配置,应用在这头,数据源在那头。简单来说,在不考虑任何后果的情况下,应用切换底层数据源就是替换一个访问链接,在这点上和其他远程调用没有本质区别。当然也有所不同,或许是出于技术的本能,面对这样一波扑面而来的海浪,我们很轻易就猛扎下去,仔细在翻腾的水花下寻找诸如协议、连接、保活、路由、事务等等蚌珠鱼目。
  但是,对应用研发来说,代码有时比技术细节重要,在设计数据源切换方案时,数据访问服务的封装是非常容易被忽视,在实施过程中又极其重要的一环。很难用哪项具体指标来完整评估数据源切换的影响,最终我们用一个形容词来表达对这项工作的期望,那就是“平滑”。平滑具有很多方面的含义,或许是系统性能,或许是用户体验,总之是希望事情能跟以前一样,最好毫无感知,起码不要变坏。还有一位苦主也是这么想的,那就是在上层其他模块调用了数据访问服务的研发兄弟。
  没有人希望仅仅是底层存储调整,就跟牛犁地一样把业务代码翻个底朝天,为了使数据源切换对上层调用“平滑”,必要的分层是一种直接而又有效的方案,即使切换需求紧迫,也值得在那之前先做一次代码优化。

  最终还是到了换新轮子的时候。应用停机切换就像客车停在路边换轮胎,虽然不至于撞到防护带上,乘客也难免会在等待和晚点的焦虑中怨声载道,如果新轮子换上跑得不安稳,有些颠簸刮蹭,还得想着怎么修补赔偿,更坏的是只能换回旧轮胎,载着一车的埋怨和划痕,颤颤巍巍地往前走。
  不平滑的切换实施起来相对简单,但一般也会有损用户体验,根据业务的容忍程度,可以选择在流量低峰期进行,如果不是对体验的极端追求,停机操作其实是首选。不过,更重要的是,如果应用使用新数据源出问题该怎么办,或许某些访问场景测试没有覆盖,或许某些数据转换出现问题,切换之前一定要做足风险预案,像这样非此即彼的切换方式,主要会在数据订正、数据回滚和连接回滚上做文章。
  由于只有单个数据源,在数据无法订正的时候,无论是回滚当前业务数据,或者干脆切回旧数据源,都会面临已生产数据丢失的情况。最好这个新轮子做好后不当备胎,而是直接挂到车上,让它用五个轮子跑,新轮子有问题还有旧轮子顶着,总不比原来差,什么时候稳当了再把旧轮子卸掉。
  双数据源并行是保障数据安全,实现运行时纠错和平滑切换的主要方法,一般有几个过程:
  • 双写。应用同时写新旧数据源,可以验证应用对新数据源的写入。由于新数据源同时还被数据同步链路写入,且存在同步延迟,新数据源的数据可能会出现[新]-[旧]-[新]的情况,但与旧数据源是最终一致的。需要注意的是,由于数据同步延迟,两个数据源在某个时点的数据快照可能不一致,这种做法不适用条件更新类的写入操作,这是方案的必要取舍。
  • 双读。应用同时读新旧数据源,可以验证应用对新数据源的读取。此时以旧数据源为主(一般也是确保最新数据),业务表现与前一致。
  • 单写双读。理论上,当数据同步节点追至双写开启的时候,两边数据就保持一致了,如果这时新写验证无误,就可以去掉旧写和同步链路,新数据源可以负担起正常业务写入,保留双读是为了在使用数据时兜底。
  • 单写单读。如果新读验证无误,双读未出现采旧补新的情况,就可以去掉旧读链路,业务彻底转向新数据源。


  所谓平滑是需要时间来逐步验证的,同时也需要更复杂的实现。相比于一刀切的方式,新增的管理逻辑,例如写数据源的选择和读数据结果的比对,如何让上层调用无感知或尽量少感知,就体现出前面代码封装的意义了。
  那么,该如何在运行时进行读写策略的替换呢,将规则写成代码是一种方式,但不如配置的动态下发来得简便。
  最后,双写是不是会有两个数据源事务一致性问题,或许可以不用考虑,双写的时候以旧数据源为主,同步链路可以保障数据最终一致性,双读做最终兜底,业务总是可以使用最新的数据。
 
0条评论
0 / 1000