文章目录
- 一、前言
- 二、系统重构的范式
- 1.明确目标和背景
- 2.兼容屏蔽对上层的影响
- 3.设计灰度迁移方案
- 3.1 灰度策略
- 3.2 灰度过程设计
- 3.2.1 case1 业务逻辑变更
- 3.2.2 case2 底层数据变更(数据平滑迁移)
- 3.2.3 case3 在途新旧流程兼容
- 3.2.4 case4 接口变更
- 3.2.5 case5 状态机兼容
- 三、系统重构过程中的风险控制
- 3.1 设计阶段
- 3.1.1 流量梳理
- 3.1.2 异常脏数据梳理
- 3.2 开发阶段
- 3.2.1 单元测试
- 3.2.2 代码CR
- 3.3 测试&预发线上回归阶段
- 3.3.1 线上线下流量回放&流量验证
- 3.3.2 核心主链路回归&异常case回归
- 3.3.3 BCP对账,反向验证数据结果
- 3.4 开城中
- 3.4.1 小批量验证
- 3.4.2 数据迁移过程中的反向校验sql
- 3.4.3 及时回滚
- 3.5 开城后
- 3.5.1 及时响应机制
- 3.5.2 BCP对账
- 3.6.开城后收尾
- 四、回顾与反思
一、前言
前段时间一个老的系统重构项目上线,在这个过程中出现了很多问题,遂想针对在这个过程中遇到的一些问题和实践,记录总结一套系统重构的范式,并回顾反思在这个过程是否有哪些值得改进的地方。
注:这里系统重构的定义,不仅是技术上的重构,更是产品逻辑上的重构。
二、系统重构的范式
系统的重构都需要考虑对原有系统的影响,不会另起一个项目,从头写到尾,还把上下游做相应的修改,这种做法虽然"彻底干净",但是成本太大,所以本文暂不讨论这种方案。
一般来说,老系统的重构都需要做到至少接口层面的兼容,这样可以保证系统的重构不会逸散到系统之外,对于上下游而言重构是无感的。
1.明确目标和背景
对于系统重构,需要最小化影响面,而这个前提就是需要我们明确我们的目标是什么,是字段迁移,还是业务模型的变动,亦或者是业务逻辑的变动。
- 底层存储变动?
- 数据模型变动?
- 业务逻辑变动?
- 迁移过程中是否不可用?
在这个阶段,需要梳理出你的改动对于整个项目的影响,比如影响到哪块业务逻辑,哪些接口。
如果是对整个项目大的重构,需要梳理现有系统的流量入口。
2.兼容屏蔽对上层的影响
核心思路就是尽量在最底层去屏蔽这些影响。
比如底层存储修改,那么是否能在dao层就去兼容,让上层的调用和之前一样。
不同语言有不同的技巧,核心就是方法增强,在原先逻辑上增加附加兼容逻辑。
对于Java而言,可以用切面、框架内注册处理器(如filter),亦或者重写get/set方法等等,
对于Go而言,也是类似,虽然没有切面,但也能利用结构体嵌入、接口和组合的方式实现类似的效果。
这样的好处就是
- 风险可控
- 修改成本低
坏处也有:
- 逻辑里夹杂着特殊逻辑,代码复杂度会提升
3.设计灰度迁移方案
系统重构必然存在新老系统切换的过程,尤其是如果需要灰度验证的话,那么新老系统切换的过程会很长,我们就需要考虑新老系统同时存在的影响。
3.1 灰度策略
系统重构往往需要灰度验证,出发点往往有以下几种:
- 控制影响面
小范围验证,避免新系统的问题对业务的影响过大
- 业务诉求
这点在B端业务里尤其常见,例如每开一些城市前,业务同学都需要和商户侧有对应的宣贯,周知到商户
灰度的策略也有很多,常见的有
by城市、by商户、by流量等,
这个决于业务倾向,这里面着重需要考虑的点在于
- 选用这种灰度是否会影响数据不一致
- 是否对业务有影响
常见的B端业务主要都是by城市维度去进行开城。
3.2 灰度过程设计
3.2.1 case1 业务逻辑变更
对于业务逻辑变更的场景,比较简单,我们往往是封装对应的开关逻辑,命中则走新逻辑,否则走老逻辑。
需要注意的就是在代码中的组织尽量封装成方法/函数,方便后续灰度全量时进行无用代码删除,降低代码复杂度。
3.2.2 case2 底层数据变更(数据平滑迁移)
这里的变更可能是同一张数据表字段使用迁移(典型场景就是敏感字段加密),也可能是不同表的数据。
双写方案
1.开启双写(老->新)
目的:让增量的数据也能同步更新到的新数据,
2.数据迁移脚本
目的:让存量的数据刷到新的数据中
经历第二步骤后,旧数据已经全量同步到新数据中。
3.开城(切读&写)
开启开城开关,流量入口开始从旧逻辑切换到新逻辑,新逻辑会读&写新数据,同时会双写老数据
双写老数据目的:切换开关后出现的数据也能同步老数据,如果出现问题,回滚开关时可以实现无损回滚
4.关闭双写
目的:双写毕竟还是冗余操作,当全量开城时,我们需要将这部分逻辑给去了,避免影响系统性能
5.下线老逻辑代码
目的:减少无用代码,降低代码复杂度
以上是较为完整的迁移过程,实际上可以根据自己的实际场景对步骤做一定程度上的简化。
比如我是典型的B端业务,实际迁移过程无需那么平滑,甚至允许一定时间内停止对外提供服务(比如停服维护),那么我们可以无需第一步的双写(老->新),只需在开城后再增量刷一遍数据即可。
3.2.3 case3 在途新旧流程兼容
旧系统中有一些流程结束并不是简单的通过开关就能进行新旧逻辑的切换,
比如B端的审批流,我们就需要考虑在切换新系统后,兼容老的审批回调。
这里主要有两种思路:
- 老回调逻辑生效,双写新逻辑
- 老回调逻辑失效,新逻辑重新推审
具体方案得根据具体业务而定。
3.2.4 case4 接口变更
原有接口如果无法承载新业务逻辑的交互,如果不能确定影响范围,那么只能在原有请求字段中新增字段,不允许修改和删减,保证向后兼容。
有一种case比较特殊,就是专门给前端交互用的接口,如果新的业务形态下,页面的交互已经变化,那么可以废弃原来的接口,另起接口去支持新的功能交互。
3.2.5 case5 状态机兼容
状态机变化需要明确新老状态之间的映射。其本质就是case2底层数据变更的思路。
三、系统重构过程中的风险控制
3.1 设计阶段
3.1.1 流量梳理
在系统重构,尤其是老系统重构的过程中,流量梳理是设计阶段必须要做的一件事情。
因为老系统往往存在大量无用逻辑,以及各种各样的复杂业务接口。在设计之前我们必须要梳理出当前系统对外部开放在使用的能力。
这里列举一些常见的流量入口:
- http(VIP、域名)
- rpc(如thrift&grpc、dubbo、kepler等)
有了范围,我们接下来做的就是考虑我们本次修改可能会对这些接口造成哪些影响。
3.1.2 异常脏数据梳理
线上脏数据梳理是非常容易被我们忽视的,而这造成的结果往往就是数据迁移/开城后会出现一些特殊case,严重的会引发事故。
在我们不熟悉旧系统迭代的情况下,我们很难考虑到线上数据有多少是在”我们意料的规则之外“的。
”我以为这个数据是这样的,可是线上有脏数据导致…“
”啊,还有数据是这样的?“
这往往是我们事后才发现的问题。
我们应该把这块前置,在设计阶段就要梳理出这个表/数据的用法,以及是否存在一些出乎我们意料的数据,尤其要多对比新旧逻辑下,这些数据可能造成的影响。
也许这个脏数据在老系统规则下能正常运行,但是在新系统规则下它就是会出问题!
我们需要多写sql,去统计当前线上库的数据分布,并去分析新旧逻辑下的影响。
3.2 开发阶段
3.2.1 单元测试
好的单测是可复用的,拥有完整的mock和断言能力。
系统重构设计修改的方法最好是要有对应的单测case。
3.2.2 代码CR
代码CR可以规避部分因代码bug引发的错误。
但是实际项目开发中,因为排期紧、代码改动量过大等原因,大型系统重构的实践效果往往不是很理想。
3.3 测试&预发线上回归阶段
3.3.1 线上线下流量回放&流量验证
流量回放是用于验证老接口是否发生变化的最好工具。
其核心是用线上的流量到线下测试环境进行流量验证,对比前后差异这里依赖基础设施的建设。
如果没有完善的流量回放工具,则可以写个脚本,用来对比流量的接口。
不过对于缺失的mock能力,可以通过copy线上的数据到线下来进行模拟。
3.3.2 核心主链路回归&异常case回归
核心主链路必须要回归,避免新系统对线上核心流程造成影响
3.3.3 BCP对账,反向验证数据结果
针对核心业务流程,以及数据迁移脚本中的数据对比,都可以用BCP对账来进行反向校验。
这里主要针对一些核心的流程去写对账,对账逻辑可以依据一些特定的数据规则来写,从结果层面去验证数据的正确性。
常见实现的方式有:
- binlog+sql
- binlog+延迟mq+sql
- 定时任务+hive
通过这样写反向的sql校验,可以低成本的去对结果进行兜底,及时感知到异常case。
需要注意的是,这种BCP对账的sql要注意sql耗时时间,虽然是离线库,但是性能太差也是不可接受的,运行时间过长的建议采用定时任务+hive表的方式。
3.4 开城中
3.4.1 小批量验证
正式开城时,我们必须要有小规模验证的过程。
比如数据迁移时,我针对单个商户进行迁移,然后回归具体的case,如果没有问题再逐步放量。
3.4.2 数据迁移过程中的反向校验sql
数据迁移脚本运行后,我们如何确定我们的执行结果是正确的,除了打印必要的日志和结果通知,还有准备对应的反向校验的sql,来验证我本次刷数是符合预期的。
BCP的报警其实也是同理(不过在大批量数据变更的情况下,BCP报警可能会出现延迟)。
3.4.3 及时回滚
如果出现问题及时回归,按照预期设定的回滚预案进行回滚。
具体的回滚策略如下:
- 代码回滚
- 灰度开关
- 数据库刷数回退(有的公司可能会支持这种能力)
3.5 开城后
3.5.1 及时响应机制
开城后,可以建立业产研开城群,方便开城后问题的沟通,及时跟进和止损。
3.5.2 BCP对账
同前面,略
3.6.开城后收尾
多次的重构,会留下很多无用代码,徒增代码复杂度,良好的习惯就是在新系统全部开城后,删除无用代码。
四、回顾与反思
真实项目中的挑战远不止这些,还有诸如对老系统不熟悉,排期压力大等挑战,这些buff叠加进而导致老系统重构中的风险不可控。
在最近的重构需求中,我大致统计了下本次重构记录的19个线上问题,其原因分布如下
- 未考虑到的线上脏数据 4
- 代码bug漏出 13
- 产品逻辑&技术方案问题 2
主要原因:
- 旧系统逻辑产研了解都较少,这个虽然不是直接原因,但确实很大程度影响了项目风险控制
- 无一个有效的流量回放&流量对比的工具来控制影响面
- 测试阶段回归不全面,导致bug漏出
- 设计阶段缺少统计线上数据分布,忽略了”脏数据“在新系统逻辑下的影响
大家说的脏数据,其实不单单是在原先系统中的不合规/不该存在的数据,也有一些是因为历史迭代而产生的”正常数据“,这些数据可能在老系统运行正常,但是在新逻辑下就不一定适用
对于及时感知线上问题方面,其中BCP报警起了不可或缺的作用,上述问题中有7个是通过BCP报警发现,由此可见BCP对账对于系统重构的重要性。