在我们的工作中,经常会遇到系统或模块重构工作,今天就来聊一聊我曾经经历过的一次系统重构经历。
01 背景
重构发生的背景是,原有的系统架构采用all-in-one的方式,随着业务的快速发展,用户访问量急剧上升,系统请求流量成倍增长,陆续出现了各种问题。当时的系统架构的示意图如下
02 痛点
当时遇到的典型问题有
1、系统模块耦合严重,访问量上涨无法快速扩容
2、数据库表混杂,定位不清。比如支付订单和商品订单在一张表,一个状态字段代表两种不同订单的状态流转含义,经常会出现各种状态异常单据。
3、复杂SQL和跨表join横行,SQL慢查多,数据库频频告警
4、无服务和领域划分,系统和接口耦合严重,经常是单点出问题,全系统宕机
5、接口响应慢,系统稳定性差,数据丢失、错乱情况经常出现
6、产品需求版本庞杂,业务需求场景多,业务逻辑分散,需求迭代速度慢
7、客诉问题高发,排查问题困难,研发疲于奔命在查问题的道路上
面对着这些问题,当时摆在眼前的方案有两个
1、继续按照原有系统迭代,但可能要付出更多的人力、精力来维持系统的稳定性和需求迭代速度
2、完全重构系统,但需要投入一定的人力,并且可能会在短期影响业务的需求迭代进展
考虑到产品会长期迭代,而眼前系统已经成为巨大的瓶颈,因此决定对系统做完全的重构。
当时我被领导安排作为这个重构项目的负责人。但领导也提出了要求:
1、公司业务在快速发展中,系统重构期间,需继续保持业务需求的迭代速度,可以适当增加人员
2、新系统设计和规划,需考虑到3年后可能的用户访问量的上涨和数据量的上涨
3、新老系统切换期间,需要保证不影响用户和业务方的正常使用,不出现数据的丢失和错乱
任务既然已经确定了,接下来就是考虑如何做的问题了。
03 方案
系统重构是一个复杂的工程,而在一个业务高速发展的背景下做系统重构,无疑于给飞行中的飞机换引擎,需要考虑周全,计划缜密,才能保证万无一失。
针对面临的问题和目标要求,在技术层面制定了以下几点大的原则:
1、采用分布式架构设计,将各个模块系统完全拆分出来,独立部署迭代演进
2、数据库模型完全重构,原有的数据库模型已经无法支撑新的业务需求扩张,同时配合分布式架构的改造落地
3、业务逻辑收归,对涉及到的相关领域按照业务逻辑收口,统一服务接口
4、新老数据库双写,保证系统稳定性和数据不丢失
5、新老系统并行提供服务,通过灰度控制流量切换,直至老系统下线
04 实施
需求和接口的梳理
在大目标和技术方向确定的情况下,接下来就进入到实施阶段。
考虑到系统中的核心场景和瓶颈都出现在订单模块,因此制定了分布分阶段实施的方案,第一步核心解决订单相关功能的重构拆分,本文也将按照订单系统的重构拆分来展开说明。
既然是系统级重构,首先需要对业务需求和产品功能进行梳理。
好在有产品的历史文档,加上通过线上产品的实时模拟验证,能够将订单相关的大致功能脉络理清楚。
功能层面的需求梳理还无法满足系统级重构的要求,需要更精确的梳理到接口级别,包括对订单相关接口调用的上游模块和订单对其它下游模块的调用,这样才基本做到把订单模块的边边角角功能完全覆盖。
功能需求和接口层面的整理,为数据库表模型设计提供了大致的参考。
数据模型层面考量
通过对已有产品功能和接口的分析,分析清楚订单模块提供的核心能力应该有哪些,和其它模块的边界是怎样的,外部对订单模块的复杂调用需求有哪些,基于这几点设计新的数据库模型。这里面有几个关键的考虑:
大数据量的解决方案:分表。考虑到订单数据量过大,原有的单一订单主表已经无法满足需求,因此将订单主表按照用户ID取模的方式分64张表,按照单表5000w数据的测算,基本可以支撑未来3年内数据量的增长。按照用户维度的分表方案,在单个用户的订单查询场景下,通过数据库单表就可以完成。但除了按照用户维度的查询,还有按照时间、地域等维度的订单查询需求,考虑到继续按照其它维度建立相应的分表方案太过冗余,因此决定对其它的查询能力通过ES构建搜索索引提供。
主键生成策略:分布式ID自增。订单表的主键,原来采用的是数据库自增策略,分表后已不再适合,借鉴twitter的snowflake方案,设计了分布式的ID自增方案。
跨表查询的解决方案:服务层聚合。原有的代码中,有大量的跨表查询,容易导致复杂SQL出现,严重影响数据库性能。在新的数据库表结构下,将表的职责划分清楚后,不再允许新的跨表查询,涉及到跨表查询的需求,通过在代码层面拆分成单表查询再聚合的方式,解除跨表查询带来的问题。
新老模型双写:为了保障系统的稳定性和不停机灰度流量验证,设计开关来实现对新老模型进行双写,因此还需要将新老模型的相关表整理好对应关系,如表字段枚举值不同带来的映射等等。新老模型双写采用的方案也是通过程序处理,而非binlog等方式,主要考虑是为了处理的灵活性和设置开关用于切换的可控性。
数据库模型设计完成,接下来需要考虑到订单模块的架构设计方案。
架构方案设计
根据对于需求的整理和理解、接口的梳理以及表模型的整理,大致可以确定的系统架构示意图如下
这里面有几点需要说明:
首先,考虑到历史版本App无法强制要求所有用户升级,因此需要在Nginx中将老版本的接口做重定向,转发到新设计的接口服务层对应的接口上。
其次,对接口服务层做了拆分,因产品有不同的展现形态,包括App、Web管理后台等,因不同用户角色也有多个不同的App,因此设计接口服务层,将相关的用户鉴权、数据加解密等统一收归到这一层处理。
第三,设计业务逻辑层,将订单相关的业务逻辑抽象到业务逻辑层,对外提供聚合封装的订单服务能力,如订单详情服务,订单列表服务等。业务逻辑层需要调用订单领域层的服务,还可能会调用到其它模块的领域层服务做聚合,例如订单详情页除了展现订单的信息,还有商品相关信息、支付相关信息、配送相关信息,这些信息基本都在业务逻辑层做聚合处理。
第四,领域服务层,核心是本领域内数据库表的操作封装,这一层基本只做单个表的增删改查。
最后,将订单相关的库从原有的单一库中拆分出来,建立订单库。实际上订单系统又分了多个领域,也可根据实际情况将订单相关的单一库再做拆分细化。
以上的设计只是一个改造完后的方案。但真正在实施重构的时候,为了保障线上系统可以不停机切换,又分别作了相关的开关设计用于过渡阶段的验证。
阶段一的过渡方案架构示意图如下:
在阶段一,有以下两点设计
在接口服务层all-in-one-app应用中,设计开关,可以控制all-in-one-app应用调用新的接口服务层,或继续走原有的直接访问数据库的逻辑。一旦出现新服务、新的库表模型有问题,通过开关直接切换回原有的调用链路中。
在领域服务层如oder-domain1-service、order-domain2-service、other-domain-service等应用中,设计开关,实现对原all-in-one库和订单库的读、写开关。
第一阶段上线后,正常的流程实现是
1、通过nginx将老的订单相关接口,转发到新的订单接口服务层应用的相关接口,实现流量切换。
2、将all-in-one-app应用中的调用开关打开,切换到调用新的拆分过的相关业务逻辑层。
3、在领域服务层,将对all-in-one库实现读、写,而对订单库实现只写不读。
这个阶段主要验证了整个服务和接口调用链路正常。当相关链路或环节出现问题,也可以通过关闭对应开关快速切换回原有方案。
阶段二的架构示意图如下
经过阶段一的验证,基本可以保证整个接口链路层面的逻辑正确,外部的调用方不再感知接下来的改动变化。
阶段二的核心是在内部的数据层面做验证,保证落在新模型中的数据是正确无误的。
这一阶段没有特别多的开发工作,主要操作是
1、在订单领域层相关应用中,将对all-in-one库的写开关保留,读开关关闭
2、在订单领域层相关应用中,将对订单库的读写开关同时打开
这时整个调用链路和数据链路已经完全实现了走新的接口服务和新的数据库表。再通过产品功能层面验证数据展现和产品流程是否正确,辅助老库相关数据做对照,基本能够验证整个系统的重构的正确与否。
这个阶段如果相关链路或环节出现问题,可以继续通过开关的控制切回到原有的调用链路和数据链路。
阶段二验证通过后,后续还需要做一些收尾工作,包括去除双写代码、去除代码中的开关及历史代码逻辑等等。
项目重构实施
整个架构方案确定后,接下来的重点是制定重构项目的计划,锁定相关资源,确定重构项目的各个里程碑节点。
项目计划的制定,不仅仅是关注订单模块本身的改造开发,还包括识别相关资源方和调用方,推动项目排期和落地。
在大部分的程序员认知中,只要自己系统没有大问题,都不愿意做相关的改动,毕竟任何一点改造都会额外的工作量,也会对系统的稳定性有着未知的影响。另外业务方也可能会对重构有排斥,这时就需要搞定关键人物,将改造的利弊陈述清楚,有时甚至需要上升到更高的层级去推动。最终能够和相关方达成一致,确定改造的时间计划,提前锁定对应的开发、测试资源,保障整个重构的顺利进行。
开发阶段的任务既包括重构相关的接口改造开发,还需要考虑新老库表模型切换所做的兼容,包括新老库表数据迁移兼容、消息队列兼容、缓存兼容等。
在开发完成后,需要做新老库表数据迁移的模拟演练,以验证老的表数据导入到新库表后,流程和展现不会出现问题。
系统级的重构改动,不可或缺的是全流程的测试验证。
为了保证测试的充分性,当时我们采取了以下几点关键措施:
1、通过已经沉淀和新增加的接口自动化用例,对大部分接口的响应和返回值做多次的跑批验证
2、通过测试人员不断的交叉测试,对可能遗漏的业务场景验证
3、通过将线上的流量复制重放,对新的接口进行逻辑验证
4、通过预发环境的流量灰度,对全流程的业务做模拟验证
系统重构在开发测试完成后,面临的另外一个重要问题是上线。
首先,制定详细的上线计划,将上线步骤事项按照先后顺序全部罗列出来
其次,每个上线步骤事项需要预估出操作的时间并明确责任人
第三,对任一环节可能出现的问题,提出假设并给出解决预案,防止上线中途出现问题因慌乱导致可能出现的异常。
最后,统一指挥,有序切换流量做灰度验证,保障整体流程正常。
上线过程中,借助已有的监控系统,用于观察系统、服务、接口等各项数据指标的变化情况,判断上线中的每个环节是否有异常。
上线完成后,通过逐步的控制灰度流量占比,验证流程和数据是否正常,进而验证整个重构是否成功完成。
在订单模块重构的过程中,其它模块也在改造和推动中,经过接近半年左右的时间,基本完成了对原有all-in-one服务的完全拆分重构。
整个架构也在后续的迭代中不断进行着新的重构和演进,包括在接口层前置设计网关接入层、订单服务的细化拆分、ES对查询场景的替换改造、业务逻辑层的中台化演进等。
05 总结
总结在整个重构过程中的几个关键步骤
1、分析目前系统的问题点,找到最重要最优先要突破的点
2、确定重构所要达成的目标、方向及限制条件
3、确定重构涉及到的核心技术方案及可行性
4、梳理重构所涉及到的需求、场景及相关上下游依赖方
5、设计明确和完善的技术方案
6、制定详细的项目计划,锁定资源和里程碑节点并推进
7、全流程的测试验证
8、详细完备的上线计划
9、不可或缺的灰度验证
系统重构是一件耗时耗力的工作,但同时也是对自身综合能力的一个巨大挑战和锻炼,期间会遇到各种各样新的问题。但正是通过这些真实的实战,在不断的重构中发现自身的能力瓶颈,去学习和成长。
如果你也有相关经历和想法,也欢迎与我交流。