一 前言
架构设计按照实施过程可分为工程架构,业务架构,部署架构等多个维度,一个好的系统架构标准应该具备可扩展、可维护、可靠性、安全性和高性能等特点。尽管这些特点大家都熟知,但在实际落地时,我们更为迫切的想知道实现这些要求的关键路径,以便在架构设计中融入这些特点。只有这样,才能确保系统能够适应未来的业务增长和交付效率。本文将重点围绕如何进行工程架构设计展开探讨。
二 价值为先
在方案出现歧义时,站在产品(商业)价值的视角审视方案并作出决策,这一点非常重要;
技术容易陷入的两个误区:
1.来者不拒:产品经理提的需求,都是有道理的,我负责完成;
2.技术驱动:这种技术实现特别巧妙,让产品特性适配于技术实现;
以上两类误区,很容易让研发对产品价值的理解形成偏差,容易对后续的技术迭代产生颠覆性的影响。站在产品(商业)价值维度,能够让协作各方站在平等的视角看问题,不仅能够容易达成共识,也能更好的为业务演进和技术迭代做好规划。
软件也是产品,在系统设计的时候,也会围绕着市场,组织,资源几个生产要素展开。
1.市场就是我们产品的目标,这是我们的搭建系统的根本;
2.组织就是围绕着产品交付过程中的资源协调和保障机制;
3.资源就是围绕着产品投入的机器,人员,时间,运营等生产资料;
软件开发是围绕着投入产出比(ROI)展开的生产经营活动。可扩展,可维护,可靠性,安全性,高性能都是我们产品的特性,每一项特性都需要投入相当的成本来实现。
1.跑车速度快,是最突出的特性,它牺牲了路况适应性,乘坐舒适性和驾驶安全性;
2.越野车突出的是路况适应性,它牺牲了速度和舒适性;
3.轿车在路况适应性,乘坐舒适性,驾驶安全性和行驶速度之间做到了相对均衡,成为了常见的代步工具;
正所谓:“将军赶路,不追小兔”,总是有所取舍。我们不追求打造一个完美的复杂系统,但可以在限定的前提下追求卓越!
三 架构设计
架构模式描述了软件系统中各个组件之间的关系、职责和交互方式,从而为软件设计提供了一种规范和约束,进而提高软件生产效率。主要体现在一下两个方面:
1.帮助开发人员更好地组织和设计软件系统;
2.促进团队之间的协作和沟通,使得团队成员更容易理解和分工;
3.1 工程框架
新系统往往从搭建项目的工程基础框架开始,包括目录结构、配置文件、代码模板等工程约束,主要用来规范项目结构、职责边界和代码风格,从而提高代码质量和可维护性。具体包括以下几个方面:
1.约定了各个模块的依赖关系和交互方式;
2.规范接口交互协议;
3.统一异常编码、捕获和处理;
4.规范日志打印格式;
5.其它公共规范约束;
下面就最常用的分层架构和DDD架构给出一些实践思路。
3.1.1 分层架构
分层架构有多种形式,例如MVC、六边形架构等,它们是随着业务和技术的发展逐步演化而来的。
在互联网初期,由于计算机硬件性能差、网络速度慢、存储成本高等因素的限制,互联网产品的形态相对单一,只能实现简单的门户网站、BBS论坛等相对简单的产品。当时的技术架构没有分层的概念,主要使用ASP、JSP、PHP等脚本语言,在这些脚本文件中混合着编写HTML、JavaScript、CSS和SQL是很常见的。随着互联网技术的发展以及更多复杂业务的线上化诉求,动态脚本语言的劣势也逐渐显现,以JSP脚本语言为例:
1.复杂性:JSP脚本语言的开发和维护比较复杂,因为需要处理Java代码和HTML代码的混合;
2.安全性:JSP脚本语言容易受到SQL注入攻击等安全漏洞的影响,从而导致系统不稳定或被攻击;
3.扩展性:脚本语言的可扩展性比较有限,因为需要在HTML页面中直接编写Java代码,从而导致系统结构不够清晰;
为了解决上述问题,出现了各种框架,如Spring、Struts等。这些框架逐渐替代了JSP脚本语言,同时也提出了分层架构的概念。其中最典型的就是MVC(模型、视图和控制器)架构模式,其主要目的是解耦应用程序的不同部分,使其更易于维护和扩展。具体实现方式如下:
1.分离关注点:将应用程序分为三个主要部分,使得每个部分都可以独立开发和测试,从而更好地分离关注点;
2.提高可维护性:因为做了三个层面的关注点分离,更容易维护和修改应用程序的不同部分;
3.提高可扩展性:展示逻辑和业务逻辑控制分离,更容易扩展应用程序的不同部分;
在多层架构中,视图层通常会使用基于模板的框架(如Thymeleaf、Freemarker、Velocity)或前后端分离的技术栈(如Vue.js、React)。这些技术的演进能够解决更加复杂的问题,如金融保险和电子商务等场景,但同时也会带来一些新的痛点:
1.学习曲线较陡峭:由于MVC架构模式需要开发人员了解和掌握多个概念和技术,学习曲线较陡峭;
2.提高了复杂性:由于MVC架构模式需要将应用程序分为多个部分,增加了应用程序的复杂性;
3.增加了开发时间:需要进行更多的测试和集成工作,增加了开发时间;
为了提高产品交付效率并降低技术门槛,现代研发工作通常会拆分为多个岗位,包括前端开发、后端开发、质量测试、运维保障等。这些岗位需要协同工作,共同完成产品的研发任务。为了保证多业务线和多岗位之间的有序协作,有效个管控过程风险,通常还会设有项目管理岗位。
MVC架构是对整个业务实现进行了关注点分离,但在更为复杂的大型项目中,特别是多人协作,多业务并行的场景下,MVC架构往往显得力不从心。此时需要对其进行更细粒度的拆分,以达到多业务线并行,而不会存在大的任务资源冲突问题。当然,不同的业务场景会有不同的拆分模式,最常见的拆分模式是多层架构模式,如下图:
通过横向的分层架构我们实现了研发分工协作,所有的经验约束在这里得以体现。 上图中,将控制层进行了二次细分。也可以按照实际应用场景进行重新调整。比如web模块能否依赖RPC模块就可以在POM文件中进行限定,如此以来,大家按照既有的工程约定,实施开发工作就可以了。
简单描述一下各个模块分层的作用:
1.数据访问层:将业务逻辑层和数据存储层进行解耦,属于模型层的范畴。它与底层数据源(MySQL、Hbase,EleasicSearch)进行数据交互,常见框架有:MyIbatis,Hibernate等;
2.远程调用层:即RPC层,与DAO层平行的数据访问层,区别是它是通过第三方接口或平台服务提供访问能力。和DAO层的区别在于数据归属权和领域事务控制权;
3.事务管理层:也叫通用业务处理层,它有如下特征:
◦对上层业务,进行业务和技术共用能力下沉,比如:多个业态的统一订单生产能力,通用的分布式事务一致性的解决方案等;
◦对下层依赖,组合DAO层和RPC层的能力,实现单一业务的事务管理;
◦对于简单的业务系统,Manager层的职责可以由Service层替代;
4.业务逻辑层:相对具体的业务逻辑服务层,主要负责业务流程的组装和编排,真正的灵活性和扩展性主要体现在这里;
5.请求处理层:主要是对访问控制进行转发,入参整形,出参定制等,其职责是直接面向的是各个终端或第三方服务方;
6.开放服务层:定义对外提供的RPC服务,功能职责和Web层类似,同样需要考虑网关安全控制、流量控制等因素;
7.终端显示层: 各个端的模板渲染并执行显示,velocity ,React,IOS移动端等;
传统的软件设计往往会导致各个组件之间紧密耦合,从而导致代码难以维护和扩展。六边形架构模式是分层模式的一种变体,通过将业务逻辑与框架、库等技术细节分离,从而实现了松耦合的设计,使得代码更易于维护和扩展。 同时,六边形架构模式还可以帮助开发人员更好地实现单元测试和集成测试,从而提高软件质量。这在各种技术中台性质的业务场景下,非常有用,如下图:
3.1.2 DDD架构
领域驱动设计(DDD)是一种软件开发方法,它以业务领域为中心,通过深入理解业务领域的知识,将业务逻辑封装在领域模型中,以此来实现更好的代码可维护性、可扩展性和可重用性。
DDD属于松散的分层架构,每层职责和作用如下:
1.用户接口层:web请求,rpc请求,mq消息等外部输入请求;
2.应用层:负责编排、转发、校验等,这与MVC中的service层中存储着大量业务逻辑有所不同;
3.领域层:也就是模型层,负责表达业务概念,业务状态以及业务规则。包含了该领域所有复杂的业务知识抽象和规则定义,包含实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等;
4.基础设施层:为领域模型提供持久化机制及其它通用技术支持能力,如消息通信,通用工具,配置等实现;
为什么DDD常年热度不减,但在我们实际的系统开发过程中,却很少有完全落地的项目呢?或者说MVC架构风格的系统很常见,但DDD架构风格的系统却很少见到。这得回归到DDD本身:它是解决复杂业务的一种软件开发方法论。
如果将普通的CRUD业务系统也按照这套模式实现,反而会增加系统的复杂度。总体来说,DDD模式适用于以下几种场景:
1.支持处理复杂业务逻辑场景:当应用程序需要处理复杂的业务逻辑时,DDD可以将业务逻辑封装在领域模型中,从而更好地反映业务需求和业务流程,降低了系统架构的复杂度;
2.高度可维护和可扩展性场景:DDD将应用程序拆分成多个子域,每个子域都有自己的领域模型,这样可以更好地管理业务复杂性;
3.需要快速迭代和交付的场景:每个子域都可以独立开发、部署和扩展,这样可以使得团队可以快速迭代和交付应用程序;
为了评估业务的复杂程度,我们需要从多个方面进行考虑,业务流程、产品规则、数据结构以及需求变化频率等。一般情况下,采用这种架构模式需要慎重的评估,因为实施这种开发模式会面临以下几个挑战:
1.需要深入理解业务领域:DDD是一种以业务领域为中心的设计方法,因此需要深入理解业务领域的知识,才能设计出符合业务需求的领域模型;
2.需要跨部门协作:实施DDD需要跨部门协作,包括业务人员、开发人员、测试人员等,需要大家共同合作才能达成共识;
3.技术难度较高:DDD需要理解很多复杂的概念,如领域事件、聚合根、领域服务等,需要开发人员具备一定的技术水平;
总之,无论是团队协作模式、个人技术能力要求、业务共识的达成,各个方面都具有很大的挑战。但这并不意味着DDD在普通业务系统中,就没有用武之地。其解决复杂问题的思想仍然能够让我们受益。常用的工具框架,如CQRS框架、事件驱动架构和微服务框架,都有DDD的设计思想的影子。
以微服务架构为例,先看以下几个问题:
•微服务应该如何设计呢?
•微服务是根据什么进行拆分的?
•微服务是如何划分边界的?
微服务拆分的太细,更多的服务会提高运营和管理难度;拆的太粗,功能耦合度高,在灵活性和扩展性方面又存在不足。所以这是一个比较棘手的问题。
确定业务和应用边界,是解决微服务困境的关键。而DDD就很好的解决了业务边界的问题,它提供了一种划分业务领域范围的方法论。
微服务就是将应用程序拆分成多个子域,每个子域都以微服务的方式对外开放能力。微服务将复杂的业务流程和规则限定在领域范围内,即内部实现各自的领域模型和数据存储。从应用层看,这规范并统一了领域服务的实现方式,大大简化了代码逻辑,更好地管理了业务复杂性。
3.2 技术选型
工程架构的搭建除了基础框架外,还有一部分重要内容,就是各类基础中间件的选择,也就是我们常说的技术选型。下面结合示例跟大家展开讲述一下,关于技术选型需要关注的要点。
3.2.1 业务需求
了解业务需求,明确系统的功能、性能、安全以及未来的扩展需求。
示例:在系统模块划分的时候,有的系统会拆分成【WEB 】+ 【JSF微服务】两组应用进行分开部署,而有的系统只会部署一个【WEB】应用。这中间的判断标准是什么?拆出来【JSF微服务】的作用是什么?
能力复用:微服务层具有更通用的模型设计,具有更强的多业务场景复用的能力。在服务运营的过程中,可以按照业务进行垂直部署;
资源隔离:按业务垂直部署,可以更精细化的优化网络,机器等硬件资源。另一方面,将上层WEB应用与底层的微服务进行资源隔离,同样可以实现更精细化的资源分配。
综上所述:如果你的服务没有多端复用和资源运营的需求,就没有必要拆开部署,增加调用链路和机器资源的多倍投入。反之,进行服务拆分,益处则更大。
3.2.2 技术特性
评估不同技术的特性,包括可用性、性能、安全性、可扩展性、可维护性等方面。
示例:曾经遇到过一个系统,底层的存储层用的是db4o(一款开源的面向对象数据库),这个中间件拥有很多优点:
•直接以存对象的方式存取数据;
•不需要数据库服务器,只需要一个数据文件,且dll大小仅为300多k;
•数据查询,操作简便且功能强大,甚至不需要使用SQL;
但这里还是不建议使用它,因为我们是分布式集群服务,这个数据库文件只能存储在单机上面,即存在单点故障问题,这是最致命的。有时候为了弥补类似的缺陷,你可能需要花费更多的成本。反过来说,如果是作为嵌入式数据库,应用在某些单片机上,它的这些优势就能够显现出来了。
3.2.3 社区支持
考虑技术的社区支持程度,包括是否有活跃的社区、是否有大量的文档和教程、是否有成熟的第三方库等。
示例:分布式调度框架中tbschedule算是开源比较早的了,但是开源之后很早就没有人维护了,如果在普通的业务中轻度使用,应用层做好监控,应该问题不大。但如果是作为基础中间件大范围的使用,显然它在调度过程可观测性方面,zk重连机制方面,调度异常自动恢复等方面急需升级优化。但现实是社区早就已经停止维护了,这就是一个比较麻烦的事情。
3.2.4 团队技能
根据团队的技能水平选择合适的技术,避免使用过于复杂或陌生的技术。这一点非常重要,否则后期的维护成本和迭代效率提升将成为一个大的难题。
示例:Cobol语言是上个世纪70年代,一种被广泛应用于金融行业的编程语言。它可以处理大量的数据和复杂的计算,而且有着高度的可靠性和安全性。直到2015年,它还运行着全球43%的银行系统和95%的ATM。
但在2023年3月份,日本就宣布,计划全部银行系统Cobol转JAVA语言。原因就是精通这门古老语言的技术人员非常稀缺,Cobol生态跟不上机器学习、云集成等新的发展了,整个系统的维护成本和迭代效率远远低于现代的JAVA生态体系。
3.2.5 成本效益
评估不同技术的成本效益,包括开发成本、运维成本、许可证费用等方面。
1.如果有成熟的开源插件可用,我们应该尽量使用它们,而不是重新发明轮子;
2.对于其他团队已经完成的任务,我们需要考虑是否可以复用;
示例:当前大多数技术中间件都需要JDK8或以上版本的支持,因此在进行技术选型时,我们需要考虑合适的JDK版本。随着Spring Boot 3的发布,其默认支持的JDK版本为17,不再支持JDK8。这对于新系统而言,选择新版本似乎更为合适。而对于存量系统,则需要考虑新版本升级对于系统的改造成本以及带来收益是否匹配,而不是想当然的追求新技术。
3.2.6 风险评估
评估不同技术的风险,包括技术成熟度、安全漏洞、依赖关系等方面。
示例:Fastjson是开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。具有执行效率高的特点,应用范围广泛。现在,在进行技术选型的时候,就需要当心了,原因就是最近两年它频繁的爆出安全漏洞,依赖的应用需要跟着频繁的升级版本,修复漏洞。这还只是表象,更为深层次的原因是显现出的安全保障方面的不足,这在技术选型时,是不得不考虑的因素。
3.2.7 小结
在选择技术方案时,没必要对最新的,最热门的技术抱有执念,综合考虑业务需求和团队技能储备等多重因素,以选择最适合的方案为宜。当然,为了适应不断变化的业务需求和技术发展趋势,也要有及时进行技术评估和更新的意识。
四 规范共识
共识的重要性在于确保团队成员之间的沟通和理解达成一致。通过制定规范和流程,可以减少重复工作和错误,避免冲突和误解,这有利于提高研发效率和质量。
4.1 数据分层
4.1.1 对象转换
在分层架构中,各层之间存在相互依赖和引用,数据则通过参数对象进行传递。为了确保每一层内部结构的稳定性,我们需要进行防腐设计。这是实现高内聚,低耦合的关键。
示例:模型层一张表有20个字段,那么对应的PO对象就有20个属性。但终端显示层只要显示 10 个字段,请求处理层(Web)在获取数据时,没有必要把整个 PO 对象传递回来,这时我们就可以用只有这10个属性的DTO对象来传递结果到请求处理层,这样也不会暴露服务端表结构和一些敏感数据。
数据防腐设计常用的手段就是各层定义自己的数据结构,常见的有:
1.VO(View Object):视图对象,主要对应界面显示的数据对象;
2.DTO(Data Transfer Object):数据传输对象,主要用于远程调用等需要大量传输对象的地方;
3.DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体;
4.PO(Persistent Object):持久化对象,它跟持久层(通常是数据库)的数据结构形成对应的映射关系;
在实际的开发中,为了方便起见,不一定需要为每个服务层定义自己的数据对象,可以根据实际情况来灵活处理。例如,在某些简单的业务场景中,可以跳过DO层对象,直接将PO对象转换为VO对象。
4.1.2 对象复用
在迭代了许久的系统中,很容易碰到一个问题,就是一些对象的作用域失控了,其典型特征有:
1.一个入参对象,有好几个方法在共用,调整一个属性值定义,影响范围大,风险高;
2.直接使用Map容器作为自己服务的入参或出参对象,没有人能讲得清楚容器里面到底有多少内容;
3.一个对象定义里面,存在着多个相似的属性定义。新的需求来了,为了降低风险,索性就再新定义一个,如此循环往复;
对象的作用范围失控问题会导致系统整体的稳定性和迭代效率显著下降。这个问题通常是一个缓慢的积累过程,在不知不觉中形成。其弊端,往往在大的系统调整时集中爆发。
解决此类问题,可以从以下几个方面入手:
1.预防:在进行架构设计的时候就给出清晰的规范定义;
2.发现:定期进行设计和代码评审,发现问题后,及时纠正;
3.止损:发现了此类系统,需要考虑微重构,防止持续腐坏下去;
4.复盘:适时的对系统进行定期复盘,对好的演进进行鼓励,对不足的进行引导,养成好的技术氛围;
4.2 异常管理
4.2.1 捕获异常
异常捕获也容易走两种极端,一种是每个方法都try-catch,一个方法里有多组。另一种是整个链路都没有一个try-catch,处于裸奔的状态。那么到底该如何进行异常捕获呢?先看一下捕获异常的目的:
1.对异常进行预判处理,让流程得以继续下去;
2.快速发现并定位问题,保证系统的稳定性;
基于异常处理的目的,对应的处理策略也就清晰了:
1.如果是为了流程继续下去,那么异常就必须在对应的节点捕获并处理;
2.如果是为了快速发现定位问题,那么就可以通过在调用入口处进行统一捕获处理,异常堆栈里会有详细的异常的原因;
总之,异常是需要捕获的,但是具体需要在哪里捕获,如何捕获,我们可以按照目的进行灵活处理。
4.2.2 处理异常
1.业务和系统异常要留有痕迹,方便日后问题定位和统计分析,比如日志,消息等;
2.对各类异常进行有规则的编码,可以快速定位问题,方便设置应急预案,规则可以参照HTTP的请求响应编码;
3.打印异常堆栈信息,这是快速定位问题原因的重要手段;
4.对异常数据进行纵向统计和对比,方便识别系统健康状态;
4.3 日志管理
1.统一日志框架,建议使用SLF4J日志门面框架,具体实现选择Log4j2、Logback等;
2.配置日志框架,包括日志输出格式、输出位置、输出级别,输出方式(异步打印)等;
3.使用不同的级别来记录不同类型的信息,并分别打印到不同的文件中;
4.定期检查和清理日志文件,以避免占用过多磁盘空间;
5.根据需要,可以将日志信息发送到其他系统或者进行分析处理,以便更好地监控和管理系统;
6.必要的情况下,建设动态调整日志级别的能力;
4.4 监控管理
1.系统性能监控:监控系统的CPU、内存、磁盘、网络等资源的使用情况,以及应用程序的运行状态。如Nagios、Zabbix;
2.日志监控:监控系统和应用程序的日志信息,引入traceId、业务身份Id,及时发现异常情况。如ELK(Elasticsearch、Logstash、Kibana);
3.安全监控:监控系统和应用程序的安全状态,及时发现潜在的安全威胁。如Snort、Suricata;
4.业务监控:监控业务系统的各项指标,访问量、响应时间、错误率等,及时发现业务异常情况。如Grafana、Prometheus;
5.调用链路跟踪:可以跟踪一个请求在整个分布式系统中的调用链路,记录每个服务节点的处理时间和状态,并将这些信息聚合起来,形成一个完整的调用链路图,以便于分析和排查问题。如:Zipkin、SkyWalking;
6.监控预警:各种监控工具是辅助快速定位问题的有效途径,要想第一时间发现问题,完善有效的预警触达机制必不可少。如邮件,企业微信,短信,电话等;
4.5 协作共识
4.5.1 HTTP服务请求都使用POST方式?
最近,我们的APP遇到了一个问题。在某些情况下,服务调用返回了“HTTP 414 URI Too Long”的响应错误。这个问题的根本原因是Tomcat默认的get请求长度限制(包括请求行和请求头)超过了8192个字符。为了解决这个问题,有以下几种方案:
1.通过修改server.xml文件中的Connector元素中的maxHttpHeaderSize属性值(比如:改为16384)来放宽限制;
2.将服务的请求协议由只支持GET方式,调整为同时支持POST请求方式,因为POST请求方式没有这个大小的限制;
3.精简Header请求参数,规范并限制cookie和业务参数的写入;
方案一,扩大Tomcat的容器限制,短期看起来可以,但是这是一个公共问题,要调整的应用容器可能需要成千上万台,而且治标不治本。
方案二,将所有GET请求方式,调整为同时支持POST请求方式,涉及到的应用又有成百上千个,工作量也不少。
方案三,精简Header请求参数,这个最为合理和稳妥,也是出现问题的本质原因,但是涉及到两个APP相互交互以及几十上百个部门协同梳理和改造,难度同样很大。
如果是你,该如何选择方案呢?
4.5.2 前端不做逻辑处理,只做数据渲染?
前端视角:由于APP发版,涉及到版本审核,用户下载更新等流程,一方面周期长,另一方用户可以拒绝升级。这就导致前端研发提出来,“前端不做业务逻辑处理,只做数据渲染”的口号。如果前端承接了业务逻辑处理,一方面,出了bug,想要修复的代价很高,如果用户不升级版本甚至无法修复。另一方面,前端承接了部分业务逻辑,将会和后端出现职责边界难以划分清楚的情况,给协作埋下了的隐患。
后端视角:一个默认背景图,一句提示文案,一个字体颜色…这些可预见的不会做出调整的数据,都需要我们来下发吗?提高了数据复杂度,增加了网络带宽。而且前端也有热更新技术,容易变化的复杂页面还可以通过H5来实现,怎么就不能做一些简单的业务逻辑了!
这又该如何进行方案选择呢?
4.5.3 小结
很多技术问题的解决方案并没有明显的偏向性,这取决于当时的环境和立场。例如,对于HTTP的GET请求参数超长问题,最合理的解决方案是精简Header参数,但这需要长期的努力,而在短期内很难实现。因此,在解决当前问题时,我们可以考虑其他两种方案。同样地,对于前端是否应该处理业务逻辑的问题,我们需要考虑到我们对APP的定位以及前后端各自基础能力的建设情况。好方案的评判标准应该是 :能够低成本地解决当前问题,并且不引入新问题。
五 总结
本文详细介绍了搭建系统工程架构时需要关注的几个重要方面。基于产品的价值,做出决策。并从系统工程架构的演进、技术方案的选型、系统规范共识的达成等方面入手,对实施过程中的常见问题给出了解决思路。最后,借用《楞伽经》中的“标月指”作为结束语,与读者共勉:“如愚见指月,观指不观月。记着名字者,不见我真实。”