重构这件“小”事儿 | 得物技术
本文以一个 Web 项目的业务代码重构实践作为依据,来探讨项目业务代码重构过程中遇到的开发问题,以及重构过程中的一些注意点,希望可以给项目开发和服务开发维护重构提供一些通用的参考与思路。
这里不探讨大型项目的重构实践,毕竟一个大型项目的重构,更偏重于架构体系完善更新与业务领域拆分,它所涉及的架构体系、人力资源、部门协调等等其他问题都具有很大的挑战。另外大部分开发所负责的仅是其中的一个服务或者模块,这里探讨的内容可能对拆分后的服务重构更具参考意义。
1.项目代码重构的背景
1.1 背景问题
2022 年年初,我们小组接手了一个已经开发五六个月的项目,该项目正处于快速迭代时期。我们本以为迭代时间不长,接手之大概能很快上手轻松切入业务,但往往我们提到这个“但是”的时候,紧接着就是反转,理想和实际还是有差距的。众所周知,一般快速迭代赶进度的项目,都会存在或多或少的问题。我们遇到的这个项目也正好精准的在这个范围之内。我们在接手后第一次版本迭代中,已经提前考虑对项目不熟悉的情况,并做了一定的准备,但依然有不少非预期的情况出现。这一次的情况,让我们不得不再次提前评估以后迭代可能会遇到的问题!
1.2 需要改变现状
项目第一个版本迭代就出现难以预期的问题,这不合理!在解决完当前版本已经发现的问题之后,我们花费一些时间去大概梳理了一下原来的项目代码,看了不少接口的大致实现。本身看看代码这是个小事情,但是这一看不要紧,好多的接口实现的逻辑都存在问题(比如在循环中调用数据库查询,多层次无效缓存实现,缓存淘汰机制复杂性能差等等... ),这些问题涉及性能、稳定性、业务异常等多个方面。如果不去解决,除了影响用户体验,还会给我们的正常项目迭代和维护造成了极大的干扰!这个项目是用 Python 开发的内部项目,该项目本身是作为 toB 类型的内部工具来开发的。而目前因为项目业务场景的扩展,要越来越多的承担 toC 的功能,在得物 App 中使用场景也同样增加,这对项目性能和稳定性又带来了额外的挑战。我们迫切需要解决这些不稳定因素,快速的切入业务,开展正常的业务迭代,以满足需求的变化。
1.3 重构的初步想法
对于那些已经发现的问题,如果可以快速修复的,我们都进行了修复并验证发布,也明显取得了一些效果。但是还有不少有共性的大类问题充斥在代码中,使我们不能轻易的去对现有的代码动刀,这些问题也是亟待解决的。当时,正好结合公司部门技术栈统一(业务项目转为使用 Java/Go 语言)的要求,我们决定在切入业务的过程中,逐渐通过重构迭代,来提升性能,减少问题,并接入公司的技术基建体系,降低代码的维护成本。虽然有了初步的重构想法,但是这显然不是一件容易的事情! 重构之前,我们还有很多前置工作要做。
2.重构的前置工作
2.1 熟悉业务流程并分析问题与痛点
在正式重构之前,我们浏览了项目的交接文档,往期的产品文档,以此做到对项目整体产品流程做到心中有数。而后,我们根据现有的产品流程,评估了从头开发该项目应实施的主要架构和大致技术方案,经过评审完善再作为重构参照,这相当于给项目的整体重构优化提供了一个目标样板。用以上的方案作为参考的话,再分析整个产品流程闭环上的接口,我们很快可以定位到项目中存在的一系列问题,当然这些问题大多数都是表面的和宏观的,细节上的很多还需要我们在项目中进一步挖掘。
2.2 评估重构成本与重构推进方式
我们作出重构决定的时候,已经做完了现有项目的基础分析。现在我们将对比项目方案的差异,根据重构的难易程度和紧急程度来确定最终的处理方式。
难易程度 根据 方案差异、修改难度、稳定要求、影响范围来 综合评估紧急程度 根据 迭代复用、接口性能、异常频次、受益范围来 综合评估
处理方式分析如下表:
由于我们整体项目处于快速迭代当中,并且有限的人力都要去跟版本需求,所以重构迁移的时间是极度受限的,我们虽然定了模块的重构顺序,但是怎么抽调人力去跟进这些事情呢?需求迭代本身就涉及到大量的接口,如果我们本身已经将新的项目构建起来,这里迭代的接口顺势就可以迁移重构到新的项目代码库中去,在迭代的同时推进项目的重构。其他时候再加上一些零散的时间,我们可以逐步的将重构向前推进,但是在这种情况下,我们一定要接受渐进重构的时间跨度会很长这个实际情况。
2.3 完善并确定流量迁移方案
大多数 WEB 项目基本都会有这些通用架构,如下图所示:
在这样的通用架构里,我们既然切换了底层语言,那公司基建支撑的相应架构,就可以用起来了。
我们最终选择的具体流量迁移方案如下:
初始化新的项目仓库并完善发布部署流程
打通新老两个项目的用户认证方式并做到双向兼容(如果网关统一承接用户认证,可以省去这一步)
利用网关层的能力去配置转发规则,使用特定或者通用规则将接口流量导入新项目中(如果没有网关可以考虑简单接入 Nginx 配置转发)
服务端完成一个批次的接口重构迁移后,在测试环境切换流量到新项目并测试整体流程,通过测试再发布
重构中部分接口需要变更的,推进前端将调用切换到新接口
上线后跟踪流量与新接口功能状态,如有问题随时回滚
到目前为止,我们已经做好了所有的前置工作,那么现在我们 ready go !
3.重构中发现的典型问题与优化方案
3.1 基本运维监控体系完善
在完善运维体系方面,先统一整合了 trace、日志、监控与告警。
目前 log、trace、metrics 三者的整合打通也显得尤其重要,例如 OpenTelemetry 这样的工具提供成套的规范,可以让开发者快速集成。在企业基建可以支撑的情况下,可以选择这三者,如果不能支持,推荐按照以下顺序完善 日志、trace、日志注入 trace 信息、metric 整个体系。
看一下下面的实例展示:
trace 信息可以帮助我们追踪每一次的调用链路
日志注入 trace 信息可让我们对独立调用链路日志做快速筛选和时间维度分析, 根据 traceId 追踪同一条链路数据
监控体系监测业务项目运行状态
3.2 重整业务逻辑
业务逻辑设计中,有一些问题对开发迭代有很大的阻碍,大概如下:
业务流程从管理后台的增删改查,到客户端的展示并回收数据,这整个流程中,很容易出现一个问题点就会影响全流程的情况。以上几个问题在变更的过程中,也是会对整体业务流程有贯通影响的,特别需要注意,所以单列了出来!这几个点也是改造起来比较困难的,我们也是根据紧急程度以及整体的梳理进度进行逐步重构。
3.3 代码重构并解决细节问题
在具体的每一个接口或者任务脚本的重构过程中,我们也总结了之前开发遇到的一些典型问题,整理如下:
以上这些典型问题都是经过提炼总结过后的了,看起来只是有限的几个,但项目代码库中,每一个问题点都可能出现数次甚至数十次,所以才不得不将整体的代码都重构。
下面列举一些实际的例子:
列表类接口循环网络 I/O 的优化
大量数据处理的时候分批操作
Redis 主从版大 Key 导致的慢查询优化前后对比
合理利用 Redis 的缓存淘汰机制(杜绝掉 SCAN 扫描的方式来淘汰 KEY)
敏感数据转移到配置中心(例如这种硬编码的配置,需要迁移走)
4.重构经验总结
4.1 重构的成果收益
经过接近一年的渐进重构,我们大概完成了 80%以上的功能模块,解决了以上提到的绝大部分问题,并且取得了很不错的提升。主要表现在以下几个方面:
已经全线接入 trace、log、metric 和告警,并可以根据这几个工具来指导项目维护和优化
业务稳定性明显提升,从刚接手时候的经常报错修 BUG 到现在几乎没有问题
不再有循环数据库查询 I/O ,除了报表类接口,基本杜绝慢查询
部分重点接口的性能提升明显,有一些后台接口极端 RT 值从 10s 以上压缩到 1s 以内,C 端主要接口 RT 的 99 线在 150ms 内(非高并发设计场景,勿喷)
可复用逻辑已尽量整合,完善并统一了之前不一致的业务逻辑,维护难度明显降低
长链路的数据提交整体流程无丢失数据的异常发生
业务方使用满意度提升
另外我们因为资源限制,截止目前,并未完成所有的重构工作,后续如果有资源投入,我们会继续推进这些问题的处理。
4.2 人员技能要求
我们也分析了本次重构对于开发人员的一些技能要求。在所有参与的开发人员都经验比较足的情况下,基本上可以通过自我驱动以及经验,来发现上面列举的这些问题,并主动去解决,而不需要额外的培训、规范以及纠错。对于经验不是很丰富的开发者,就需要适当的总结和规范去指导作业。抽取上面的问题来具体分析的话,不外乎如下这些技能:
熟练使用当前项目所需要的开发语言,避免产生一些基础的语言问题
对数据库有比较深的了解,能根据实际情况设计比较合理的数据结构并做到适当优化
对常用的中间件使用比较熟悉,了解一些原理,避免在使用的时候只知其一不知其二从而踩坑又难以排查
计算机的基础扎实,操作系统知识,算法与数据结构知识熟悉,可以写高效的代码
了解高并发高可用架构,可以根据一些基本思想来指导开发与优化
4.3 规范执行与 Review 除了以上的技能,一些规范的制定与执行也十分重要,不过相反的是,这是自上而下的流程,需要一定的管理层面的推进。规范本身指定了行为边界,完善合理的规范制定之后,只要遵照执行,就可以避免绝大部分问题。另外 Review 制度可以促进规范的落地,避免空有规范而实践偏离的情况。现行的好设计,在经过一定的时间之后,随着业务需求变化以及用户量的提升,或许又需要开启新一轮的优化或者重构,这也是正常的。
5.让重构成为“小”事
5.1 任务阶段化来变“小”当前事项
纷繁复杂的整套重构流程,如果混在一起,可能会让人望而却步。但是通过具体分析,阶段拆解的方式,将任务切割成一件件“小”事情,可以让我们用比较容易的方式去一步步解决问题。另外拆解不是无脑拆分,还是需要有一个整体上的架构设计,否则可能会导致整个重构任务不能达到目标,既延长了时间,又难有成效。重构过程的质量把控,可以通过规范、强制 lint 检查、Review、单元测试、性能测试 等方式去保障,这在每一阶段都是要贯彻执行的。
5.2 开发的几个主要思路
其实还有很多我们项目中没有出现的问题,恕我们不能一一列举。但是在开发过程中本着几个主要的思路,我们就可以设计并开发出完善且高性能的项目。例如:
最小维护难度:系统设计结构完整、逻辑算法简洁高效
单个接口最小 RT:接口性能要高,RT 尽可能的小
最小限度的数据交互:接口请求参数以及返回值尽量精简,以节约网络带宽
异步削峰限流等:使用异步方式剥离额外逻辑,提升接口性能并提升用户体验
最少访问频次:了解计算机各个硬件以及网络 I/O 的性能层级,尽量减少长耗时的 I/O,转换为程序内部处理
最少数据写入:数据写入需要尽量削减,减少流量以及无效数据的产生
缓存与缓存一致性:多级缓存、缓存一致性、缓存淘汰策略等思路
分治与隔离:该思想除了在拆分资源上体现(对象储存 CDN 剥离图片文件等内容),还可以在业务模块上体现出来(界定业务边界,合理拆分具体的模块与流程)
高可用性:注重稳定性,整体项目流程稳定无差错,降级与灾备需要提前考虑
5.3 提升技能与经验积累
当然,并不是掌握上面说的这些思路就高枕无忧了。例如:我们重构的项目之前就有通过异步方式来处理问题,但这个异步是因为本身接口未设计好导致性能受限,所以不得已采取的方案。本身一些长异步流程带来的数据不一致也会产生不少问题,在我们重构接口之后,就着手取消了这些异步任务,将其做成事务流程,显然就比之前的功能要好很多。所以我们还是要加深自己在计算机基础知识层面的认知,以及拓展完善开发知识体系,成体系的掌握高可用高并发系统设计,学习积累和总结开发经验,才能让自己在项目程序设计开发中游刃有余。
本文属得物技术原创,来源于:得物技术官网
得物技术文章可以任意分享和转发,但请务必注明版权和来源:得物技术官网
版权声明: 本文为 InfoQ 作者【得物技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/61bee9248138fd71f5f7072c4】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论