大厂对业务系统是如何做架构升级的
本文介绍大厂的一次重大架构升级,快速发展过程中,系统的迭代速度和其他方面的设计遇到了很多困难,这次升级就是为解决这些困难。
1 挑战
业务急速发展之中大家会思考,怎么做才能使这些还不稳定或者还没有想清楚的业务很好地迭代起来。
最简单的,如果新业务跟某个旧业务非常类似但又不完全一样,就把旧业务的旧代码cv并修改,这样新业务就出来了。但这导致积压大量难题:
服务端问题并非性能,而是在于巨大的耦合导致数据紊乱和迭代速度越来越慢。
2 现状
系统架构:
- 最顶层用户应用,每一个用户应用就是一个端,也就是用户所能看到的入口
- 接入层,非常传统结构,用Nginx,还专门做TCP 接入层
- 业务层,Web大集群,大代码,只对业务做了分割,有策略引擎、司机调度
- 数据层,有KV 集群、MySQL 集群、任务队列、特征存储
初创公司都应有的架构,仅在这技术体系里把业务逻辑实现。
Web App
由于历史原因,所有业务入口耦合在出租车代码中,业务修改入口需要更改出租车的仓库
发送订单之后页面跳转到各个业务
前端代码没有模块化,仅做了简单的代码合并
所有渠道使用同一份代码,充斥着黑魔法
没有公共组件,也没有机制来沉淀组件
最大的问题在耦合性。以前只有出租车这个业务,最开始Web App 只有出租车,后来专车上线了,就在出租车里面加了专车入口,只是业务名不同界面会有小区别,后来加入了快车、代驾,都跟出租车差不多,没遇到太大问题。再后来有了顺风车,顺风车跟其他功能不一样,整体界面是预约型的,有乘客和车主两种模式。如果在老首页开发顺风车成本太大了,需要和出租车业务线的人一起开发业务模块,如果未来做迭代,这种开发模式将非常痛苦。老首页的模块也没有做拆分,代码散落各地,只是通过打包工具拼接在一起,没有做模块化,所以整体很糟糕。
API结构
API 层仅存在业务线级别的划分
业务内缺乏模块划分,所有 API混合在一个仓库中
API和后台通过数据库直接共享信息,没有 model 封装
API的日志没有统一规范,监控、大数据分析、反作弊等不统一API函数长度惊人,且存在大量重复代码
快车 API项目总代码量达到百万行
API 稍好至少在业务维度分开,出租车与专车、快车是分开的两个系统,放在两个仓库。不过API 也有很大问题,业务代码没做服务化拆分,没有model封装,业务所有的API 和后台MIS 都在一个仓库里,对系统是大患。
3 如何入手
基本思路是把所有事情分类,就像整家务,要将东西分门别类。因此,最关键了解到底哪些东西该放一起,用颜色来比喻模块或代码归属,核心问题就变成这些模块到底啥色。
3.1 重构思路
将类似的模块归类及合并,再逐步优化。先从前面,即用户入口拆分,要先保证所有模块足够内聚,由统一的团队负责。如出租车业务线可完全控制自己的代码,能够写自己的客户端,也能够写自己的Web App,最终只是通过一些工程构建手段将多个业务整合起来变成一个完整的端。做到这一点之后,所有的业务迭代问题就迎刃而解了,因为业务间已经没有依赖和耦合了。之后就是重新梳理业务,让业务根据自己模型特点重构。
3.2 从前到后、从粗到细
- 乘客 App 代码按业务拆分
- Web App 首页代码按业务拆分
- 提供各种方便组件下沉的构建流程和开发工具
API服务化改造 平台服务下沉
最开始,考虑
3.3 代码治理和模块下沉
代码治理本质就是把各种模块染色、再把它们归类。代码治理最难在消除错综复杂的依赖:
- 把不同模块代码放在不同仓库,使模块物理隔离。特别是Java这种静态编译语言,一旦把代码仓库隔离就完全没有办法直接对其他模块产生依赖,至少绝对不会再出现循环依赖
- 看如何把循环依赖通过一些间接层隔离开,如通过抽象接口隔离开,一点点把代码拆到不同仓库
- 有了这样一个简单拆分,就需要考虑怎么让模块能独立开发、测试、上线。独立的流程一旦独立起来,就意味着拆分基本上成功
模块下沉与代码治理息息相关。若只是要求把所有代码拆分,而没有合适拆分,这件事情无法推进。对于程序员,封装一个很有意思的模块给更多程序员用。大家并非不想封装,只是如果封装并共享出来的代价太大,就影响热情。
模块下沉是一种机制:
- 应该鼓励
- 还应该让大家发现这是一件不得不做的事
若仅对内公开模块列表让大家自由选择,达不到模块下沉目的。因为人都懒,不想思考太多,只想尽快把事完成,往往倾向cv,也不愿额外下沉。咋办?给所有业务提供一个统一SDK,里面包含所有能用组件,大家必须使用它开发。若业务模块稳定且比较通用,我们有工具和相应的简单机制把业务模块下沉下来,变成SDK的一部分,长期下去SDK会越来越大,只要SDK里做好分类和规划,上层就会越来越轻,才可真正专注业务开发。
还有最核心一点,把所有业务做到
3.4 无状态 && 异步化
无状态
服务端易理解。一般倾向把各种业务做到无状态,这样易水平扩展。客户端也是一个理,也要考虑横向扩展。一个简单框架往往提供一些最基础控件,如按纽、列表,这些都不会耦合任何业务逻辑,所以易用。但当业务起来,大家习惯将一些状态放到业务控件,这在一定程度方便了,可一旦要将业务重构或模块化下沉,就造成极大困难。
如一个模块若大量通过全局变量或单例跟上下游耦合,那这模块就很难复用和重构,这些全局变量或单例就是状态。所以,客户端也提出使用“无状态”,把存储的信息都放到外。
异步化
也是解耦方式。服务端的RPC 类似于函数调用,若参数变了,实现和调用的双方都要改变,这很不透明,也不能渐进式上线。我们用订阅/发布模式对 RPC 解耦,要求所有接口都异步返回。客户端也是这样,如做数据缓存,想优化网络,不能够期待这个函数是一个同步函数,一定用回调接受所有参数。所以设计时,只要可能发生网络请求或访问磁盘,在客户端也尽量异步请求数据。
业务模型的理想形态:
3.5 业务形态
出行到底想要什么?就是到达自己想去的地方。实际上,我们的模型可以做得非常抽象和简单。比如,我想要打快车去机场,我就是一个需求方,我的需求会发到很多服务者那里去,服务者会根据特征进行一些匹配。最基本的特征是服务能力,如果服务者能够开快车并通过了能力验证,这个需求就有可能发给他。如果开出租车的也有能力开快车,但是他还没有在平台上验证这个能力,就只能开出租车。一个人可以验证很多服务,白天可以开快车,晚上可以做代驾,做不同的事。
服务和需求的匹配是通过计价模型和匹配策略来实现的。发送需求的时候需要选择计价模型和车的类型。快车和专车服务过程大同小异,但价格差别明显,专车价格会贵很多。通过匹配策略可以实现各种需求的匹配。如:
- 选拼车,这需求会尽量匹配已有拼友和顺路的车
- 选专车,可要求这辆车在指定时间来接人,这时候匹配策略会优化倾向这种
所有业务基本上都是以这种模式运转,所有功能都是核心主干或者旁路,只要把业务模型抽象出来,基本上就能够满足大部分业务。
高度可配置的出行工具:
为此,就思考如何设计真正高度抽象的工具。简单起见,把出行过程抽象成框架(上图)不完整。有颜色地方表示出租车、快车、专车、代驾共同流程,只要组合各种流程就可以实现整个业务形态的能力。在这个框架里可以定制所有业务形态的车标、提示语、匹配的模型、计价模型等功能。当时梳理这个抽象感觉兴奋,因为这意味着在这个基础之上就可简易扩展未来业务形态。只要公司还是在做需求和服务匹配,基本离不开这套路。
4 服务器 API 咋拆
Plan A:可拼接的业务组件
将业务按照抽象的可复用的组件进行拆分
基于长连接的应答式可靠安全的自定义通信协议
基于“消息”的订阅/发布模式,每一个 worker都是一个最小业务单元,可通过配置服务随意拼接worker 顺序、插入新worker、复制流量
统-的订单系统 optimus
统一的账号、支付、分单-引擎等核心组件
关于服务器 API 的拆分,我们最开始希望一次性实现理想方案,但是这个理想方案遇到一些问题。
理想方案是啥?
公司业务一般都是基于订单流转推动各种业务动作。为啥会订单流转?是因为对乘客和司机做了些操作,若想象成一个客户端系统,类似触发各种用户事件。客户端动作根本决定信息如何流转,所有事情都应该在客户端触发,触发后来到组件层,所有动作进行消费,然后进行下一步。如用户提出一个需求,发单对需求过滤,判断哪种需求,然后检查。快车有拼车、不拼车,发单就可知是哪种,对统一订单系统来说这就是标志。无论拼否,这单对用户都一样,无非消耗多少钱、消耗几个座位还是消耗整辆车。之后分单系统会进行订单匹配。一旦匹配成功,客户端有很多动作,司机确认接单,乘客可以看到确认。若直接做成消息,客户端和服务端用一条总线连接就解决。
函数式思想真的好吗?
这其中很大优点——可拼接,所有东西都组件化。但是最大问题是抽象程度非常高。这是函数式思想,要求所有的 Worker 都是纯函数,纯函数高要求,上下文状态须通过参数才行。很难做到这点,因为所有系统须有状态,一旦这样这个纯函数就不是纯函数,要依赖外部变量。与OOP思路差非常大,做函数式设计易陷入一些抉择当中,如何定义输入、输出,如何划分流程。有一些流程划分成三段式,中间流程异步调出去,又异步调回来继续后续流程,这种设计让人纠结。函数很依赖异步化,异步会让数据流复杂。我们思考数据流的流向及每次数据流在流转的时候都需设置的输入、输出。最终,该方案并未实施。
重新思考问题,这次是比较简单和现实的方法。先进行一些代码隔离,把代码分开,之后对系统按刚才说的模块进行面向对象的抽象,如发单就是单独系统,订单也是一个单独的系统,支付的收银体系是一个系统,评价体系是一个系统。每一个系统变得很简单,互相之间用 RPC 调用关联。
这有啥缺点?不易扩展。现在我们设计的模型源于当前业务现状,如业务改变,比如多种车型,就会遇到如何扩展抉择:
- 提供更多 API 接口满足新业务功能
- 还是在原有 API 修改提供更多参数
看起来都可,但本质无论用哪种方案都使模块本身越来越臃肿,都是把很多种东西融合,并不理想。当一个服务臃肿到一定程度又会出现以前问题,又要再拆分重构,甚至整个 RPC 调用流程都大动。
从项目整体实施效果,这次重构最主要是能让迭代更快。重构前客户端 crash 率非常高,重构中我们对代码进行了非常多的修改,同时还在用户体验上做了很多优化,但最终 crash 率反而大幅下降,从以前 1% 降低到 0.3%。重构后各业务团队开发模式发生根本变化,以前是各业务各耦合在一起进行开发,现在各个业务都能独立开发,互不干扰,同时平台还会不断产出更多公共组件。
5 避免重蹈覆辙
重新思考业务模型:抽象、抽象、再抽象。
所有设计应自上而下,先从产品层面上规划核心业务的模式,然后考虑如何让产品技术实现它。如果把业务模式描述成如图所示的核心循环,会非常清楚。不仅要考虑现在,还要考虑未来。如果让整个架构保持健康,就要考虑什么功能是真正紧密相关的。
如服务端,直觉感觉各种不同发单应该在一起,但实际上并不是。不同车型的发单接口互相之间并没有什么联系,每一种发单都会有独特的个性化定制,这些定制才是真正应该跟发单紧耦合的东西。所以我们应该从产品角度上考虑,把一种发单所调用的所有相关 API 放在一起,服务端变化,调用的组件也会变化,做到发单闭环。
刚提到的服务端重构方法,实际上并未让各子系统打通,很遗憾。未来如果开发一些新需求,肯定还会涉及多个模块、团队,避免不了沟通成本。
形成从客户端到服务端的 Feature Team:
#24秋招避雷总结##设计人秋招体验最好的公司#面向 2024 校招/社招全网最新最全的系统设计面试。八年开发经验,毕业四年成为技术专家兼架构师,乐于知识分享,擅长图文讲解各种软件技术! 现如今,牛客网人均某马点评,但本质都是系统设计考量点,如: 1.多级缓存设计,如何保证缓存跟数据库的一致性? 2.设计模式,各种业务流程,到底何时何地使用何种模式? 3.玩转分布式框架 ... 更多技术重难点设计,尽在本专栏!