一次物理表迁移实战

引言
本是集团新零售战略重要一环,也是线上线上奔跑最快的业务部分之一,的仓储也是业界领先的系统,创造了很多新的零售玩法,例如加工仓,暂养仓与冷链仓都进行了新的探索。在业务不断扩张的同时,仓储系统也面临诸多考验,例如各种大促,到2018年上半年可以说是举步维艰,很多历史包袱让业务稳定性堪忧,业务需求的挑战让整个团队举步维艰。仓储团队痛定思痛开始了慢慢的架构升级的道路。

关于仓储出库作业与出库单服务化
在整个物流系统中,每次大促对于仓内最直观的表现就是吞吐量,例如天猫双11,对于外界最直观的就是交易额,订单量,峰值等等指标。那类比仓储就是其出库的吞吐量有多大,的确每次大促之后的复盘都会把系统的订单量放在一个很醒目的位置。由此可以看到出库作业效率对于整个系统的要求是很高的。
最开始是做o2o的,对于仓内就是满足履约而出库,所以当初的设计就只有一个履约主子单两张表来表示仓内订单,上游履约下发,仓内接受并进行波次作业出库。后来开始接入b2b系统,主要增加了配货业务,订单的来源方不一样,由于与履约区分比较大,因此又增加了配货单表。的业务发展很快,后面又增加了调拨业务,于是又增加了调拨单表…,问题出现了,这么多的业务形态,难道每一种出库单都要加一张表么?对业务类型是不可预期的,系统会出现大量的出库单的表,每种单据都是不同的存储模型,开发与维护都非常艰难。 图1
一次物理表迁移实战
架构升级的重要组成部分就是要将出库单物理模型统一,用DDD的思想在存储之上构建一层逻辑模型,中间通过一层convert进行逻辑模型与物理模型的转换,用服务化思想封装各个出库单模型。 优势:保留了业务层对于不同出库单的模型上抽象,又在物理模型上统一更方便维护。 劣势:中间层的convert会增加开发量,物理模型数据量会比较大,这可能对于存储性能会有更高要求。 图2
一次物理表迁移实战
对于表迁移的方案设计
主要做的事情 简而言之就是要把存储表从之前的各种不同单据的表迁移到一张新的出库单表。换表。对于替换表,调研了很多主流的做法就是进行双写,然后把读流量切换到新表即可。 图3
一次物理表迁移实战
方案落地的挑战
这是很容易想到的方案,双写到新表后,验证新表数据的可靠性之后进行读流量的切换,做的完美即可对表进行平滑切换,但是这里有两个点至关重要。第一就是如何验证新表数据的可靠性,第二就是平滑切换的要求就新老表实时数据的一致性,可以来回切换。这两点会在后面的风险切换方案中进行详细描述。
关于读切换详细思考
关于读新表大的方案有两个:
第一是根本上的迁移工作,在DAO的上层调用处全部修改,包括返回的DO模型,把原先调用处使用的字段统统进行改正。优势:彻底。 劣势:开发工作量巨大,需要梳理所有出库链路,风险控制困难,与新需求很难协调与业务赛跑,很难控制业务开发人员不去使用老表,业务优先的原则会导致出库单永远处于被动。我需要一种方式去做控制权的反转。于是第二种方式。
第二是只是做物理模型的切换,主要参考了在新老系统切换常使用的一种设计模式:适配器模式。而不做逻辑模型的切换。优势:开发工作量聚焦在适配上,风险容易控制。Spring的注入方式会避免业务开发人员误读老表。劣势:老表模型依旧存在。

适配器的方式依托于spring的接口松耦合,能够动态注入实现类。

上层业务开发人员会习惯性的按照接口类型进行注入,容器会找到多个接口实现出错,即需要强制指定DAO实现。

    1. 正在方案一直无法敲定的情况下,团队启动了应用层面的架构升级,即做应用服务化拆分,届时新应用将会完全使用新物理模型,这给模型的迁移提供了一个机会,老应用需要逐渐废弃,所以只需要做物理层面的迁移即可,在新应用开始引流之前完成新表数据的可靠性验证(即物理模型完成迁移即可)。因此具体迁移方案采用第二种方式。

实际开发中的几个问题
由于在进行迁移工作前,业务层在出库单迁移项目启动之前在某些时候考虑到表迁移擅自进行了数据的双写,这给底层适配带来了复杂性。这里遇到的麻烦比较多,我举个比较有代表性的栗子。履约单的下发是。
先写未批次单,再批次下发写老表.
新表的逻辑是,先写新表,批次下发再更新新表的批次单号。
图4:
一次物理表迁移实战

这里发现一个问题,逻辑上的双写的时间不一致。写老表是批次下发后,写新表是批次下发前。为了做到平滑迁移,需要保证新老表主子单是id一致的,因为整个链路主子单的id是相关其他单据的外键,必须要强一致。由于写表时间不一致,因此需要在批次下发写老表的时候增加这样一段逻辑:查询新表,如果有则拿对应ID,如果没有则用自己生成。
这还没结束,新表的ID必须要大于老表的ID,否则可能关联出历史数据来,因此在写新表前要确认这个点,必要时需要做ID提升.

这是履约单的场景,配货单上层业务双写是先写老表再写新表。这导致了DAO写新表的适配要走一个相反的逻辑,先查老表,如果有则拿老表ID,否则没有则自己生成。

因为这就结束了吗?No.这里会发现一个问题,老表之前是用的自增,新表使用的sequence,当时发现老表的id<配货单,履约单的双写把该物理表的id提升至sequence。配货单去拿了老表的自增id的时候又新表的sequenceId冲突了。

这就相当于新表的id在用sequence的同时又在用老表的自增id,这样会导致主键冲突。

这个问题困扰了几天,但是后来却使用一种很简单的方式解决了:把老表的id从自增修改成新表一样的sequenceId源.

部分单据其实在DAO上层进行了双写,例如数据的双写,原以为上层的双写与底层的双写覆盖并不冲突,无非多做了一次冗余操作,但是没想到,数据的双写有些字段是增量写入,这导致一个问题就是重复双写会把数量进行累加,这种情况也很无奈,只有链路梳理,全DAO扫描这样的SQL进行一个一个解决,在确认逻辑无误的情况下移除业务层或则适配层双写。
开发工作量的翻倍(吐槽)
主子单两张表的适配方法其实已经够多了,但是由于之前的重构历史包袱,导致底层两张表的DAO数量却各有一个副本,实际适配的DAO是四个。维护已经够困难还要去写一大堆重复逻辑,需要非常胆大心细。
不同场景对应不同的convert
字段映射并不是老表映射新表哪个字段这么简单,这里需要考虑到几个问题。
该字段是否需要SQL索引,最新TDDL貌似支持json格式索引查询。
状态,类型等字段并不是使用的一个枚举,需要进行逻辑语义的convert。
代码的公共逻辑与不同逻辑抽离,灰度切流等逻辑为公共逻辑放抽象类,不同单据类型的convert进行动态选择注入。
迁移切流方案
图7:
一次物理表迁移实战
由于考虑到出库单并非接单这么简单,其存在几个主要特征。
链路长: 从上游到作业链路的方方面面。再到回传链路

场景多: 各种业务均是基于出库单进行

风险高,并且风险后置, 如果出问题,需要后置节点进行反馈,常常会导致block风险。

我们需要一套完整可靠的解决方案。
第一步是验证的是双写的数据一致性与完整性,我们开发一套工具,可以手动调用指定单据的字段校验。但是这存在一个问题就是无法跟踪到单据实时一致性,BCP是一种方案,后来由于一些客观原因采用了另一套方案,那就是读流量切换的时候进行动态校验,如果校验通过才返回,否则依旧读老表。

最开始根据时间切换,后来发现单据的时间很多时候是一个瞬时值,一旦出问题影响巨大,因此补充灰度方案跟踪单据的整个作业链路,进行单据级别的切换. 因此从读的切流四个维度控制:单据 | 时间 | 仓 | 全系统
xflush进行关键节点的埋点,进行核心链路的日志采集,包括前面的字段校验不一致,钉钉主动推送告警,能解决的问题就是尽可能主动发现问题。但是这里存在一个风险就是这样的日志采集可能会给磁盘造成压力。这里采用统一封装日志输出并进行开关控制。
慢SQL风险,这个是临近开发结束发现的一个比较严重的风险,老表的索引字段放到新表在新表并无索引,当时DB拆分并没上线,任何对DB的负载增加都是巨大风险,因此增加了SQL拦截器的识别,对于DB耗时也进行监控告警,这也在测试阶段对QA提出要求进行全链路压测试,因为DB预发和线上是同一套环境,预发的压测也要格外小心。
关于发布节奏与风险识别的思考
由于整个的改造是在业务不断迭代,并且单量水位一直较高的情况下进行的,如果一次性发布,风险很难控制,后面采用了增加发布次数来规避风险,每次发布可能只发布一个灰度策略,或则修复一个字段映射的错误,或则修复一个慢SQL ,然后再切少量流量进行验证试错,通过小步快跑的方式达到目标。这里有两个个前提:1. 该需求可切割。2. 高频率发布带来的测试问题,是否可迅速验证。 3. 回切方案

总结
出库单迁移线阶段已经完成最高危链路履约单的切换,现在正在推动外域下线流量,如果顺利可在不久完成老表的下线工作。这次技改我认为最大的挑战并不是切换方案本身,而是背负着历史包袱和稳定性高压下的风险控制,这些都让表的迁移工作遇到相当多业务场景下才会出现的问题, 需要足够胆大心细和风险控制,才保证了整个迁移过程平稳进行。