本文以一个Web项目的业务代码重构实践作为依据,来探讨项目业务代码重构过程中遇到的开发问题,以及重构过程中的一些注意点,希望可以给项目开发和服务开发维护重构提供一些通用的参考与思路。
这里不探讨大型项目的重构实践,毕竟一个大型项目的重构,更偏重于架构体系完善更新与业务领域拆分,它所涉及的架构体系、人力资源、部门协调等等其他问题都具有很大的挑战。另外大部分开发所负责的仅是其中的一个服务或者模块,这里探讨的内容可能对拆分后的服务重构更具参考意义。
难易程度 根据 方案差异、修改难度、稳定要求、影响范围来 综合评估 紧急程度 根据 迭代复用、接口性能、异常频次、受益范围来 综合评估
处理方式分析如下表:
难度 \ 紧急程度 | |||
在这样的通用架构里,我们既然切换了底层语言,那公司基建支撑的相应架构,就可以用起来了。
我们最终选择的具体流量迁移方案如下:
初始化新的项目仓库并完善发布部署流程
打通新老两个项目的用户认证方式并做到双向兼容(如果网关统一承接用户认证,可以省去这一步)
利用网关层的能力去配置转发规则,使用特定或者通用规则将接口流量导入新项目中(如果没有网关可以考虑简单接入Nginx配置转发)
服务端完成一个批次的接口重构迁移后,在测试环境切换流量到新项目并测试整体流程,通过测试再发布
重构中部分接口需要变更的,推进前端将调用切换到新接口
到目前为止,我们已经做好了所有的前置工作,那么现在我们 ready go !
优化点 | |||
看一下下面的实例展示:
trace信息可以帮助我们追踪每一次的调用链路
日志注入trace信息可让我们对独立调用链路日志做快速筛选和时间维度分析, 根据traceId追踪同一条链路数据
{
"level":"error",
"ts":"2022-07-22T21:26:00.073+0800",
"caller":"api/foo_bar.go:38",
"msg":"[foo]bar",
"error":"Post \"https://xxx.com/abc/xxx\": context deadline exceeded",
"traceId":"0aee15dc63f617e751d17060xcf74b9c",
"content":{
"body":"json str",
"resp":""
}
}
监控体系监测业务项目运行状态
在具体的每一个接口或者任务脚本的重构过程中,我们也总结了之前开发遇到的一些典型问题,整理如下:
优化点 | 问题 | 收益 | 典型思想 |
数据库查询网络 I/O 优化 | 列表类接口 循环网络I/O | 减少各类列表接口 RT | 最小化网络 I/O |
削减接口返回字段 | 接口返回无效字段过多 | 减少干扰、削减流量 | 减少网络带宽占用 |
统一返回值字段数据类型 | 弱类型语言类型乱用 | 统一数据类型,便于管理 | 强类型 |
分类树递归生成优化 单次查询并重构复用 | 循环多层查询数据库 I/O过多 生成算法复杂度略高 | 统一分类树生成 集成过滤规则 | O(n)时间复杂度, 递归并防止数据异常而死循环 |
非强关联逻辑功能拆分 | 部分接口业务逻辑隔离 但是接口强相关 | 剥离不同模块为多个接口 | 分治 |
解决语法类问题 | 循环迭代中改变SET 数据类型不匹配 数据存储格式 等(Python) | 提升主流程稳定性 杜绝答案提交异步流程中断 | 数据与语法兼容 |
抽离相同逻辑 达成方法重用 | 相同逻辑方法分散不兼容 部分逻辑缺失、维护难度高 | 复用方法与逻辑 统一逻辑并降低维护难度 | 复用 |
离线数据同步优化 | 全量同步数据未分批 不支持断点补偿 | 限制批次数量分批同步 支持中断恢复 | 分批、控制上限 中断补偿 |
优化批量导入数据处理 | 业务导入数据处理逻辑有问题 关联比对多次查询且不用MAP | 减少算法时间复杂度 提升处理速度 | 批量提取 MAP对比的O(1)复杂度 |
Redis慢查询优化 低效缓存优化 清理或拆解大KEY | 未选对正确的数据结构 未对数据结构选择正确操作方法 例如: 存储set数据取单个元素使用 smembers 命令读取后比对,而不是使用 sismember | 部分smembers读取切换为sismember,RT>50ms查询平均减少5个/秒, 优化完几乎无RT>100ms请求,提升吞吐效率显著 | 数据结构 算法复杂度 Redis单线程模型阻塞 |
缓存淘汰机制优化 | 部分缓存无有效淘汰机制 代码SCAN淘汰管理难性能差 | 合理设计 利用Redis自动过期机制 | 合理利用缓存淘汰机制 |
SQL索引调优 | 低效索引,交叉索引干扰 | 提升查询性能 | 索引调优 索引覆盖查询 |
异步消费脚本优化 | MQ消费任务的幂等、事务性、补偿 逻辑需要确认 | 防丢、防重、补偿处理 | 幂等、事务 等 |
配置迁移到配置中心 | 部分配置硬编码需要迁移 | 统一管理配置 代码不包含敏感信息 | 分环境隔离配置 隐藏重要信息 |
只有生产者无消费者队列待处理 | 生产向无消费队列无限注入数据 导致队列无限膨胀 | 打通生产消费流程 取消无用队列减少空间占用 | 队列有生产必有消费 |
以上这些典型问题都是经过提炼总结过后的了,看起来只是有限的几个,但项目代码库中,每一个问题点都可能出现数次甚至数十次,所以才不得不将整体的代码都重构。
下面列举一些实际的例子:
列表类接口循环网络I/O的优化
// 这里代码就不贴了,我大概做个说明:
// 数据库ORM使用时候处理粗糙,额外写的GET方法,独立查询关联表
// 在列表接口中循环获取该数据即造成数据库网络I/O的循环调用
// 而通过ORM预加载的方式,则是削减查询并二次组装的数据
// 当然我们也可以自己查询列表数据,然后先遍历提取外键,将关联数据转换为map接口,再次遍历列表来组装结果数据
// 大量数据处理分批次进行
// 一是考虑内存用量、二是考虑I/O交互流量、三是考虑处理分块时间、四是考虑中断恢复
// 例如:从数据库批量获取数据,然后批量上传到某处
// 注意:分页批处理需要保证每一页数据不变化,否则会有错漏或者重复
page := 1
pageSize := 10000
for {
// 判断已经存在的断点数据,将条件恢复成断点条件
// 直接 模拟查询的结构列表
dataList := make([]itemStruct{}, 0)
// 此处处理分批后的业务逻辑
......
// 处理完当前批次可以记录断点
// 断点可以缓存在数据库、Redis等其他媒介中
// 分页查询,查不到数据或者低于pageSize可以当成数据处理完了
if (len(dataList) < pageSize) {
break
}
// 查询条件切换为下一页
page += 1
}
熟练使用当前项目所需要的开发语言,避免产生一些基础的语言问题
对数据库有比较深的了解,能根据实际情况设计比较合理的数据结构并做到适当优化
对常用的中间件使用比较熟悉,了解一些原理,避免在使用的时候只知其一不知其二从而踩坑又难以排查
计算机的基础扎实,操作系统知识,算法与数据结构知识熟悉,可以写高效的代码
了解高并发高可用架构,可以根据一些基本思想来指导开发与优化
最小维护难度:系统设计结构完整、逻辑算法简洁高效
单个接口最小RT:接口性能要高,RT尽可能的小
最小限度的数据交互:接口请求参数以及返回值尽量精简,以节约网络带宽
异步削峰限流等:使用异步方式剥离额外逻辑,提升接口性能并提升用户体验
最少访问频次:了解计算机各个硬件以及网络I/O的性能层级,尽量减少长耗时的I/O,转换为程序内部处理
最少数据写入:数据写入需要尽量削减,减少流量以及无效数据的产生
缓存与缓存一致性:多级缓存、缓存一致性、缓存淘汰策略等思路
分治与隔离:该思想除了在拆分资源上体现(对象储存CDN剥离图片文件等内容),还可以在业务模块上体现出来(界定业务边界,合理拆分具体的模块与流程)
高可用性:注重稳定性,整体项目流程稳定无差错,降级与灾备需要提前考虑
*文/ 预子
关注得物技术,每周一三五晚18:30更新技术干货