一文搞懂SaaS应用架构:应用服务、应用结构、应用交互设计

大家好,我是汤师爷~

今天系统性地聊聊SaaS应用架构设计。

应用架构概述

我们已经完成了SaaS系统的定位分析,明确了系统的目标和核心能力。这为接下来的应用架构设计奠定了基础。

应用架构就像整个SaaS系统的骨架,决定了系统的整体结构和各个组件之间的关系。接下来,我们会深入探讨应用架构的三个核心要素:应用服务、应用结构和应用交互。这些要素共同构成了一个体系化的SaaS系统架构。如图所示。

通常,应用架构设计包括以下几个步骤:

  • 识别应用服务:根据业务架构,把业务需求转化为IT系统,找出关键的应用服务。
  • 划分应用结构:设计应用结构,以及与业务流程、数据之间的关系,明确各部分的职责。
  • 设计应用交互:规划各个应用结构之间如何交互和集成,确保系统各部分协调运作。

应用服务设计

在设计应用服务之前,我们先搞清楚什么是应用服务,以及重要性。

应用服务的概念

应用服务是对一个或一组密切相关的业务对象及其操作的封装。

应用服务要明确定义自己的责任范围,将相关业务功能和对象组合在一起,避免暴露内部细节。

它需要整合因为同一原因变化的功能和数据,同时分离那些因为不同原因变化的部分。这样的设计方法,确保了服务的内聚性和灵活性。

应用服务的概念源自SOA(面向服务的架构)和微服务架构的兴起。通过把系统功能拆分成多个独立的服务,可以提高系统的可维护性、可扩展性和灵活性。

应用服务如何划分?

应用服务在应用架构中非常重要,它把系统的核心功能“打包”起来,提供给外部的业务流程使用,可以看作是SaaS系统对外的“门面”。用户或者其他系统通过调用应用服务来实现特定的业务功能。那么,怎么设计应用服务呢?

1、对齐业务能力,划分粒度适中的应用服务,职责单一。

在划分应用服务粒度时,可以参考领域驱动设计(DDD)中的"限界上下文"概念。业务对象类似于限界上下文中的聚合根,是应用服务的核心。

通常情况下,我们会基于业务能力来划分应用服务,每个业务能力都对应一到多个独立的应用服务,每个应用服务用于支撑特定的业务能力。如图所示。

将应用服务与业务能力对齐,确保系统功能紧密贴合业务需求,避免技术实现与业务逻辑脱节。

如果一个应用服务支撑了过多的业务能力,需要评估其内部是否关联了过多的业务对象。在这种情况下,可以考虑将多个业务对象进行分组,从而将该应用服务拆分为多个更小、更专注的服务。

2、围绕业务对象,提供具体的业务功能,避免包含不相关的功能。

从外部来看,应用服务通常有明确的业务含义,主要围绕一个或一组密切相关的业务对象进行操作。

围绕业务对象设计服务可确保服务内部功能高度相关,提升内聚性。提供具体的业务功能让服务的边界更清晰,有利于业务团队和技术团队的协作与沟通。

例如,线上商城系统的”交易服务”专注于订单确认、下单和支付等功能,不应处理用户认证、商品推荐等其他业务。

服务化架构的价值

面向服务的架构最大的价值就在于它的敏捷性和灵活性。

敏捷性体现在服务可以快速调整,独立演进。灵活性体现在每个服务都有清晰的业务边界,功能内聚性强,能够单独管理生命周期。具体来说:

  1. 轻量级应用:采用基于服务设计的轻应用,各个服务独立开发、部署和运营,可以独立交付,影响范围小,提升交付效率。
  2. 服务复用、灵活编排:通过服务的复用和灵活编排,可以快速响应业务的变化,支持复杂的业务流程。
  3. 局部扩展性高:系统被拆分为独立的服务后,容易进行横向扩展,只需要扩展必要的部分,成本更低,效果更好。

示例:订单履约应用服务划分

如图所示,订单履约能力是零售企业业务能力地图中的 L2 级别业务能力。

订单履约能力可以细分为多个末级业务能力:面向消费者的履约服务、订单派单、订单管理、拣货管理、发货管理和逆向履约。

基于这些末级业务能力,我们就可以设计出对应的应用服务。

应用结构设计

在完成应用服务的设计后,我们需要深入地了解应用的内部结构。

应用结构设计是把应用服务的概念转化为具体实现的关键步骤。它描述了应用服务内部的层次结构和组织关系,决定了系统的模块化程度,以及后续开发和维护的难度。

应用结构的抽象层次

在设计应用结构时,我们通常会把系统分成不同的层次,比如系统级、应用级、模块级和代码级。

这种分层的方式有助于我们在不同层面处理复杂问题,确保系统结构清晰、易于维护。如图所示:

  • 系统级:关注各个系统的整体布局和管理方式,比如系统之间的关系,以及它们如何协同工作。
  • 应用级:聚焦每个应用的整体架构,包括应用与其他应用的交互方式,以及它们在整个系统中的角色。
  • 模块级:对应用内部进行更细致的划分,涉及代码的模块化设计、数据和状态的管理等。通过合理的模块划分,可以提高代码的可维护性和可重用性,减少重复工作。
  • 代码级:关注代码本身的结构和实现方式,这一层的设计直接影响到代码的质量和实现细节。

抽象层次的存在,是为了帮我们更有效地管理系统的复杂性。

通过把系统分成不同的抽象层次,我们可以更好地组织和理解系统结构,简化开发过程,提高代码的可维护性和可扩展性。

这种分层方法让开发团队能在不同层次上专注于特定的问题,更好地应对大型软件系统的挑战。具体来说有以下作用:

1、分解复杂度

如果把所有的业务细节、技术细节都混在一起,整个系统就会变得难以理解、维护和扩展。通过设置不同的抽象层次,我们可以把系统的复杂性分解到各个层次,每个层次只需关注特定的功能和职责。

这种分层处理方式让开发人员在专注于系统某一部分时,不用过多关注其他部分的细节,大大简化了系统的设计和开发过程。

2、团队协作边界清晰

在大型项目中,通常会有多个团队同时开发。如果系统没有明确的边界,各团队之间很容易产生冲突和重复劳动。

通过清晰的抽象层次划分,不同团队可以专注于系统的不同层次或模块,互不干扰。

3、扩展性强

随着业务需求的变化,系统往往需要不断地扩展和升级。如果系统的架构设计没有合理的抽象层次,扩展和升级就会变得非常困难,甚至可能需要对系统进行全面重构。

而在有抽象层次的系统中,变更通常只需聚焦在特定的层次上进行,而不会影响整个系统。比如,一次业务改动只影响模块级别,我们就可以在不改变系统整体架构的情况下,替换或新增某个模块,满足新的业务需求。

应用结构如何划分?

在前面,我们提到了应用服务的设计方法,那么怎么把这些应用服务一步一步地转化成代码结构呢?

其实,应用服务是通过一系列的应用结构来实现的。如图所示。

基于应用服务的划分,我们可以进一步细化应用结构,更好地组织和管理系统功能。这个过程涉及到多个层次的设计方法:

  1. 系统和子系统的划分要和业务域、业务子域的粒度保持一致。 这样,我们就能更好地把业务需求映射到技术实现上。
  2. 一个或多个相关的应用服务,可以组合成一个可独立部署的应用。
  3. 在应用内部,可以进一步分层。 比如,参考领域驱动设计(DDD)的分层方法,可以分为用户接口层、应用层、领域层和基础设施层。
  4. 应用的各个层级内部,还可以细分为多个模块,每个模块又包含多个代码单元。

那么,具体来说,我们该怎么划分应用呢?可以参考以下几点原则:

1、业务划分原则

应用划分的关键是看应用服务的边界。

应用服务的核心目标是帮助企业实现业务能力,所以它们需要和业务能力保持一致。而应用是实际的物理部署单元,应用服务最终要部署在特定的应用上。

因此,一个或多个相关的应用服务,可以组合成一个可独立部署的应用。

应用服务可以单独部署,也可以多个服务合并部署。那么,如何判断何时选择独立部署,何时选择合并部署呢?这需要参考技术层面的成本和稳定性风险等因素。

2、技术划分原则

在业务初期,尽量从单体应用开始,避免过早地把应用拆得太细,这样可以减少分布式数据库事务和数据不一致的问题,并可以降低技术部署成本。然而,即使在单体应用内部,也需要将应用服务划分为界限分明的模块。

避免应用之间出现循环依赖或双向依赖。在应用单元内部,可以进一步分层,始终保持不同层级之间的单向依赖关系,高层级可以依赖低层级,同层级之间不应互相依赖。

只有当真正遇到技术上的痛点,比如规模、性能、安全等问题时,才考虑拆分应用。如果不拆分会严重影响业务的稳定性,那就应该拆分。但不要为了拆分而拆分,因为每次拆分都会增加系统的复杂度。

3、组织规模原则

划分后的单个应用的项目团队规模通常建议保持在10~12人左右。

因为团队成员越多,沟通的渠道就会成倍增加,可能导致信息传递变慢或者失真。一个10到12人的团队,可以确保大家的沟通更直接、更高效,减少信息障碍。

小团队更容易管理,项目经理或者团队领导能更好地了解每个成员的工作状态和需求,进行更有效的协调和支持。

同时,小团队有助于建立更紧密的合作关系,成员之间更容易培养出默契,提升整体工作效率和项目质量。

5.5.5 示例:订单履约系统的应用结构划分

如图所示,是订单履约系统的应用结构划分。

  • 用户接口层:直接与用户交互的层级,负责向用户显示信息,响应用户命令。
  • 应用层:定义软件的应用功能,它负责接收用户请求,协调领域层能力来执行任务,并将结果返回给用户,核心模块包括:
  • 领域层:业务逻辑的核心,专注于表示业务概念、业务状态流转和业务规则,沉淀可复用的服务能力。

应用交互设计

应用交互就是指不同应用之间怎么“沟通”和“交流”。

在一个复杂的系统中,各个应用可不是孤立存在的,它们需要互相配合,才能完成更复杂的业务流程。

应用交互的设计,就是为了确保这些系统和组件能够顺畅地“对话”,一起实现系统的整体目标。

应用交互的方式有很多种,包括同步调用、异步消息通信等等。每种方式都有特定的应用场景和优缺点。

通过合理的交互设计,系统中的各个部分能够高效协作,降低耦合度,增强系统的灵活性。

同时,好的交互设计还能显著提升系统的性能和容错能力,即使在高并发、大流量的情况下,也能保持稳定运行。

应用服务的上下游

应用服务是系统对外提供的核心业务功能。

虽然应用服务可以独立发展和演化,但它们必须相互交互,才能实现整个系统目标。

那么,如何设计应用服务之间的交互呢?首先,我们需要了解服务的上下游概念。

1、服务上下游的概念

服务的上下游关系可以通过DDD(领域驱动设计)的建模方法来定义,主要涉及“限界上下文”(bounded context)和“上下文映射”(context mapping)这两个概念。

上下游表示上下文之间的映射关系,下游需要了解上游的领域知识来实现业务,而上游不需要了解下游。

换句话说,上游服务不需要关心下游服务的存在,但下游服务的实现却依赖于上游服务提供的能力。

这个概念听起来有些抽象,确实让许多人犯迷糊。让我们通过线上商城的几个应用服务来具体说明:

  • 用户服务:管理用户的账户信息,包括注册、登录、认证、个人资料等,处理用户的权限和角色管理。
  • 商品服务:管理商品的基本信息,包括名称、描述、价格、图片、分类等,提供商品的查询、筛选和浏览功能。
  • 库存服务:管理商品的库存数量,处理库存的预占、扣减和回补操作。
  • 交易服务:处理订单的创建、修改、取消和查询,管理订单的状态和生命周期。
  • 支付服务:处理支付事务,支持多种支付方式,管理支付状态。
  • 履约服务:处理订单的履约,包括拣货、包装、发货等,管理物流信息和配送状态。

如图所示,我们可以看出各个服务的上下游关系。

商品服务和用户服务是上游服务,它们提供基础数据,其他服务依赖这些数据。

交易服务位于中间位置。对于用户服务和商品服务来说,交易服务是下游,因为它依赖这两个服务的基础数据。

对于库存服务来说,交易服务也是下游,因为交易下单过程中,需要库存服务来预占、扣减库存。

对于履约服务而言,交易服务是上游,因为它提供订单数据,驱动后续的订单履约流程。

2、为什么要区分上下游?

区分上下游关系的核心目标是为了解耦。

"解耦"这个词相信大家都不陌生,但它的含义往往过于抽象和模糊。在这里,我们探讨一下解耦到底指什么。

耦合是指两个或多个结构之间的相互作用和影响。在软件开发中,这可以理解为不同模块、系统或团队之间的相互依赖和影响。

随着业务需求越来越复杂,单个系统或团队很难独立实现所有功能。因此,解耦的目的并不是完全消除耦合,而是减少不必要的依赖关系。

前面提到,上游服务不需要关心下游服务的存在,但下游服务的实现却依赖上游服务提供的能力。

因此,当下游服务的团队在迭代新功能时,无需评估是否影响上游服务,因为基于明确的上下游关系,可以快速判断不会影响上游服务。只需评估是否影响自己的下游服务。

比如,交易服务的功能发生变更时,只需通知履约服务的团队,评估是否会影响到他们,上游服务团队则无需知晓。

这种方式能大大减少影响面的评估工作,提高团队协作效率。

相反,如果上下游关系混乱,存在各种循环依赖,那么任何一个服务的改动都难以准确评估影响面。此时,就需要召集所有服务的团队,逐一评估是否有影响。

在实际场景中,如果每次项目会议都需要拉一大群人才能评估出影响面,这样的协作效率就太低了。

3、上下游关系的核心使用场景

在软件研发过程中,上下游关系在很多关键场景中都发挥着重要作用:

  • 明确服务之间的依赖关系:上下游关系让开发者清晰地了解服务间的依赖。这有助于减少不合理的依赖,确保服务的独立性和模块化设计。同时,它也避免了服务间的循环依赖,降低了一个服务出现故障引发连锁反应的风险。
  • 评估影响面:当上游服务变更时,可以预见其对下游服务的影响,从而制定相应的应对策略。
  • 指导团队协作:上下游关系有助于明确各团队的职责和工作范围。上游团队需要考虑下游团队的需求,提供稳定的接口和服务;下游团队则需要适应上游的变化。

应用服务的交互方式

应用服务之间的交互方式有很多,最主要的就是同步调用和异步消息。

1、同步调用

同步调用是一种通信方式,调用方(客户端)向被调用方(服务端)发送请求,然后等待服务端处理完再返回结果。

在这期间,调用方会被“堵住”,直到收到服务端的响应。这种方式要求双方都在线,而且调用方在等待响应时,没法做别的事。

在微服务架构中,常用的同步调用协议包括 HTTP、REST API、Dubbo、Thrift、gRPC 和 SOAP 等。

同步调用适用于下游服务需要立刻从上游服务获取数据或功能的场景。这种方式简单直接,但需要处理服务之间的可用性问题。

举个例子,用户下单时,订单服务需要同步调用商品服务,获取商品的最新价格和库存信息,确保订单有效。

通常来说,上游服务不应同步调用下游服务。如果上游服务同步调用下游服务,会导致上游需要了解下游的领域知识,违背DDD上下游的设计原则,加深系统耦合,并增加团队协作复杂性。

此外,这种做法还可能引发级联故障,降低系统可靠性。如果上下游直接互相调用,那下游服务发生故障时,也将直接影响上游服务的可用性,可能导致整个系统都瘫痪。

2、异步消息

异步消息是另一种通信方式,消息的发送者(生产者)和接收者(消费者)通过消息队列或消息中间件进行通信。

发送者发完消息就可继续其他操作,不用等接收者处理完。消息被发送到消息队列后,接收者从队列中异步获取并处理。这样一来,发送者和接收者的处理时间就不耦合了,双方可以各自独立运作,提高了系统的灵活性和可扩展性。

在微服务架构中,异步消息通常通过消息中间件实现,比如 RabbitMQ、Kafka、RocketMQ 等。

异步消息适用于上游服务向下游服务发布事件或通知的场景,能有效解耦服务,提高系统的弹性和可靠性。下游服务也可以通过异步消息向上游服务反馈信息,实现双向通信。

比如,用户提交订单后,订单服务调用支付服务发起支付。用户完成支付后,支付服务发布一个“支付成功”的消息,订单服务接收到消息后,更新订单状态并发送通知。

3、其他交互方式

1)共享数据库方式

多个服务访问同一个数据库,直接读取或写入数据。

在微服务架构中,通常不建议采用共享数据库的方式,因为这违反了服务自治的原则,增加了服务之间的耦合度。

2)文件传输

服务之间通过共享文件系统或 FTP 等方式交换数据文件。这种方式一般适用于批处理的场景,实时性较差。

3)服务总线(ESB)

使用统一的通信总线来连接不同的服务和系统。服务之间不直接通信,而是通过总线来“中转”,适用于需要集成多种异构系统和服务的大型企业级系统。

但是,这种方式引入了额外的架构层,增加了系统的复杂性。所有服务都耦合到总线上,存在单点故障的风险。

本文已收录于,我的技术网站:tangshiye.cn 里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。