基于服务的架构是微服务架构风格的混合体,由于其架构的灵活性,被认为是最实用的架构风格之一。尽管基于服务的架构是一种分布式架构,但它的复杂性和成本与其他分布式架构(如微服务或事件驱动架构)不同,这使得它成为许多商业应用非常受欢迎的选择。
拓扑结构
基于服务的架构的基本拓扑结构遵循分布式宏观分层结构,由单独部署的用户界面、单独部署的远程粗粒度服务和单体数据库组成。基本拓扑结构如图13-1所示。
这种架构风格中的服务通常是粗粒度的“应用程序的一部分”(通常称为领域服务),它们是独立的、单独部署的。服务的部署方式通常与任何单体应用(如EAR文件、WAR文件或程序集)的方式相同,因此不需要容器化(尽管可以在Docker等容器中部署领域服务)。因为这些服务通常共享一个单一的数据库,所以应用上下文中的服务数量通常在4到12个之间,平均约为7个服务。
在大多数情况下,在基于服务的架构中每个领域服务只有一个实例。然而,基于可伸缩性、容错性和吞吐量需求,一个领域服务的多个实例肯定是可以存在的。一个服务的多个实例通常需要用户界面和领域服务之间的某种程度上的负载均衡能力,以便用户界面可以定向到一个健康和可用的服务实例。
从用户界面远程访问服务需使用远程访问协议。REST通常用于从用户界面访问服务,但也可以使用消息传递、远程过程调用(RPC)甚至SOAP。虽然由代理或网关组成的API层可用于从用户界面(或其他外部请求)访问服务,但在大多数情况下,用户界面使用嵌入在用户界面、API网关或代理中的服务定位模式直接访问服务。
基于服务的架构的一个重要方面是,它通常使用集中共享数据库。这使得服务能够像传统的单体分层架构那样利用SQL查询和连接。由于服务数量很少(4到12个),数据库连接在基于服务的架构中通常不是一个问题。但是,数据库变更可能是一个问题。“数据库分区”一节描述了在基于服务的架构中处理和管理数据库变更的技术。
拓扑结构变体
基于服务的架构风格中存在许多拓扑结构变体,这使得它可能是最灵活的架构风格之一。例如,单个单体用户界面,如图13-1所示,可以分解成多个用户界面领域,甚至到与每个领域服务相匹配的程度。这些用户界面变体如图13-2所示。
类似地,可能存在将一个单体数据库拆分为多个独立数据库的机会,甚至可以扩展到与每个领域服务匹配的领域范围数据库(类似于微服务)。在这些情况下,确保每个独立数据库中的数据在另一个领域服务不需要使用是很重要的。这就避免了领域服务之间的通信(在基于服务的架构中绝对要避免这种情况),也避免了数据库之间的数据复制。这些数据库变体如图13-3所示。
最后,还可以在用户界面和服务之间添加一个由反向代理或网关组成的API层,如图13-4所示。当向外部系统暴露领域服务能力,或者在整合共享的横切关注点并将它们移出用户界面(如度量、安全性、审核需求和服务发现)时,这是一个很好的实践。
服务设计和颗粒度
由于基于服务的架构中的领域服务通常是粗粒度的,因此每个领域服务通常都是使用由API 外观层、业务层和持久层组成的分层架构风格来设计的。另一种流行的设计方法是使用类似于模块化单体架构风格的子域对每个领域服务进行领域划分。图13-5说明了这些设计方法。
不管服务设计如何,领域服务必须包含某种API访问外观,用户界面与之交互以执行某种业务功能。API访问外观通常负责编排来自用户界面的业务请求。例如,考虑一个来自用户界面的下单业务请求(也称为目录签出)。这个由OrderService领域服务内的API访问外观接收到的单个请求在内部协调单个业务请求:下订单、生成订单ID、付款以及为每个订购的产品更新产品库存。在微服务架构风格中,这可能需要协调许多单独部署的远程单一用途的服务来完成请求。内部类级编排和外部服务编排之间的这种差异指出了基于服务的架构和微服务架构在颗粒度方面的许多显著差异之一。
因为领域服务是粗粒度的,所以需要使用涉及数据库提交(commit)和回滚(rollback)的常规的ACID(原子性、一致性、隔离性、持久性)数据库事务来确保单个领域服务中的数据库完整性。另一方面,像微服务这样的高度分布式架构通常具有细粒度的服务,并使用称为BASE事务(基本可用性、软状态、最终一致性)的分布式事务技术,该技术依赖于最终的一致性,因此不支持与基于服务的架构中ACID事务相同级别的数据库完整性。
为了说明这一点,考虑一个基于服务的架构中的目录签出过程的示例。假设客户下了一个订单,而且用于付款的信用卡已过期。因为这是同一个服务中的原子事务,所以添加到数据库中的所有内容都可以通过回滚来删除,同时发送给客户一个通知说明付款没有生效。现在,在具有较小细粒度服务的微服务架构中考虑这个相同的过程。首先,OrderPlacement服务将接受请求,创建订单,生成订单ID,并将订单插入到订单相关的表中。完成后,订单服务将远程调用PaymentService,后者将尝试进行付款。如果由于信用卡过期而无法完成付款,则订单没有完成且数据处于不一致状态(订单信息已插入但尚未批准)。在这种情况下,该订单的库存情况如何处理?是否应标记为已订购和递减库存?如果库存不足,而另一个客户希望购买该商品,该怎么办?应该允许新客户购买它,还是应该为那些试图用过期信用卡下单的客户预留库存?这些只是在使用多个细粒度服务编排业务流程时需要解决的少数几个问题。
作为粗粒度的领域服务,允许有更好的数据完整性和一致性,但也有一个权衡。对于基于服务的架构,对OrderService中的下订单功能所做的变更将需要测试整个粗粒度服务(包括支付处理),而对于微服务,相同的变更只会影响较小的OrderPlacement服务(不需要更改PaymentService)。此外,由于部署了更多的代码,基于服务的架构存在更大的风险,一些东西可能会被破坏(包括支付处理),而对于微服务,每个服务都有一个单一的责任,因此在变更时破坏其他功能的可能性较小。
数据库分区
尽管不是必需的,因为在给定的应用上下文中服务数量很少(4到12个),基于服务的架构中的服务通常共享一个独立的、单一的数据库。这种数据库耦合可能会导致数据库表模式(table schema)变更方面的问题。如果操作不当,表模式更改可能会对每个服务产生潜在影响,从而使数据库变更在工作量和协调方面成为一项非常昂贵的任务。
在基于服务的架构中,表示数据库表模式(通常称为实体对象)的共享类文件驻留在所有领域服务使用的自定义共享库中(例如JAR文件或DLL)。共享库也可能包含SQL代码。创建实体对象的单个共享库的做法是实现基于服务的架构的最不有效的方法。对数据库表结构的任何变更也需要更改包含所有相应实体对象的单个共享库,因此需要对每个服务进行更改和重新部署,而不管这些服务是否实际访问更改的表。共享库版本控制可以帮助解决这个问题,然而,对于单个共享库,如果没有手动的详细分析,很难知道哪些服务实际上受到数据库表变更的影响。这个单一的共享库场景如图13-6所示。
减轻数据库变更的影响和风险的一种方法是对数据库进行逻辑分区,并通过联结共享库清楚显示逻辑分区。注意在图13-7中,数据库在逻辑上划分为五个独立的域(common、customer、invoicing、order和tracking)。同样注意领域服务使用了五个与数据库中的逻辑分区相匹配的共享库。使用这种技术,对特定逻辑域(在本例中为invoicing)中的表所做的变更与包含实体对象的相应共享库(可能还有SQL)相匹配,只影响使用该共享库的那些服务,在本例中是invoicing服务。其他服务不受此变更的影响。
注意在图13-7中,所有服务使用的公共域和相应的common_entities_lib共享库。这是一种相当常见的情况。这些表对所有服务都是通用的,因此,对这些表的更改需要协调访问共享数据库的所有服务。减轻对这些表(以及相应实体对象)的变更的一种方法是锁定版本控制系统中的公共实体对象,并限制只有数据库团队能进行变更访问。这有助于变更控制,并强调对所有服务使用的公共表进行更改的重要性。
提示
使数据库中的逻辑分区尽可能细粒度,同时仍然维护定义良好的数据域,以便在基于服务的架构中更好地控制数据库变更。
架构示例
为了说明基于服务的架构风格的灵活性和强大功能,请考虑一个用于回收旧电子设备(如iPhone或Galaxy手机)的电子设备回收系统的真实示例。回收旧电子设备的处理流程如下:首先,客户询问公司(通过网站或电话亭)他们可以从旧电子设备中获得多少钱(称为报价quoting)。如果觉得满意,客户将把电子设备送到回收公司,回收公司将接收物理设备(称为接收receiving)。收到后,回收公司将对设备进行评估,以确定设备是否处于良好的工作状态(称为评估assessment)。如果设备处于良好的工作状态,公司将向客户发送回收设备承诺的资金(称为记账accounting)。这个过程当中,客户可以随时到网站查看项目的状态(称为项目状态item status)。根据评估结果,设备回收后被安全销毁或转售(称为回收recycling)。最后,公司定期根据回收活动(称为报告reporting)生成临时和预定的财务和运营报告。
图13-8说明了该系统使用基于服务的架构。请注意前面描述中标识的每个领域区是如何实现为单独部署的独立领域服务的。可扩展性只能通过扩展那些需要更高吞吐量的服务来实现(在本例中,是面向客户的Quoting服务和ItemStatus服务)。其他服务不需要扩展,因此只需要一个服务实例。
还要注意用户界面应用是如何联结到它们各自的领域中的:面向客户、接收、回收和记帐。这种联结使用户界面获得容错性、可扩展性和安全性(外部客户没有通向内部功能的网络路径)。最后,注意在本例中有两个独立的物理数据库:一个用于面向客户的外部操作,另一个用于内部操作。这可以使内部数据、操作与外部操作(用垂直线表示)驻留在一个分隔的网络区域中,从而提供更好的安全访问限制和数据保护。通过防火墙的单向访问允许内部服务访问和更新面向客户的信息,但不能反向操作。或者,根据所使用的数据库,也可以使用内部表镜像和表同步。
这个例子说明了基于服务的架构方法的许多好处:可扩展性、容错性和安全性(数据和功能保护和访问),以及灵活性、可测试性和可部署性。例如,评估服务为了在收到新产品时添加评估规则而不断地进行变更。这种频繁的变更被隔离到单个领域服务中,提供了敏捷性(快速响应变更的能力),以及可测试性(测试的容易性和完整性)和可部署性(部署的便利性、频率和风险)。
架构特性评级
特性评级表中的一星级评级(如图13-9所示)意味着特定的架构特性在某种架构中没有得到很好的支持,而五星评级意味着架构特性是某种架构风格中最强大的特性之一。记分卡中确定的每个特性的定义见第4章。
基于服务的架构是一种领域划分的架构,这意味着它的结构是由领域驱动的,而不是从技术角度考虑(如表示逻辑或持久性逻辑)。考虑前面的电子设备回收应用示例。每个服务都是一个单独部署的软件单元,其作用域是一个特定的领域(例如项目评估)。在此领域中所做的变更只影响特定的服务、相应的用户界面和相应的数据库。不需要修改任何其他内容来支持特定的评估更改。
作为一个分布式架构,量子数可以大于或等于1。尽管可能有4到12个单独部署的服务,如果这些服务都共享同一个数据库或用户界面,那么整个系统将只是一个量子。然而,如“拓扑结构变体”一节所描述,用户界面和数据库都可以联合,从而在整个系统中产生多个量子。在电子设备回收示例中,系统包含两个量子,如图13-10所示:一个用于包含单独的客户用户界面、数据库和服务集(报价和项目状态)的面向客户的部分的应用;一个用于接收、评估和回收电子设备的内部操作。请注意,即使内部操作量子包含多个单独部署的服务和两个单独的用户界面,但它们都共享同一个数据库,从而使应用程序的内部操作部分成为一个单独的量程。
尽管基于服务的架构不包含任何五星级评级,不过在许多重要和必不可少的领域中的评级仍然很高(四星级)。使用这种架构风格将应用拆分为单独部署的领域服务,可以实现更快的变更(敏捷性)、由于领域范围有限而获得更好的测试覆盖率(可测试性),以及相比庞大的单体应用能够更频繁地、更低的风险地进行部署(可部署性)。这三个特性可以加快产品上市时间,使组织能够以相对较高的速度提供新特性和修复缺陷。
对于基于服务的架构,容错性和整体应用可用性也很高。尽管领域服务往往是粗粒度的,但四星评级基于这样一个事实:在这种架构风格下,服务通常是自包含的,并且由于数据库和代码共享而无需服务间通信。因此,如果一个领域服务停止运行(例如,电子设备回收应用示例中的接收服务),它不会影响其他六个服务中的任何一个。
由于服务的粗粒度特性,可扩展性仅为三星级,相应地,弹性仅为两星级。尽管这种架构风格可以实现编程的可扩展性和弹性,但是与细粒度服务(如微服务)相比,更多功能是复制的,因此机器资源的利用率不高,也不具有成本效益。通常基于服务的架构中只有单个服务实例,除非需要更好的吞吐量或故障转移。电子设备回收应用示例就是一个很好的例子,只有报价和项目状态服务需要扩展以支持高用户访问量,而其他操作服务只需要单个实例,使得更容易支持单个内存缓存和数据库连接池。
简单性和总体成本是使这种架构与其他更昂贵和更复杂的分布式架构(如微服务、事件驱动架构,甚至是基于空间的架构)区分开来的另外两个驱动因素。这使得基于服务的架构成为实现起来最简单、最经济的分布式架构之一。虽然这是一个有吸引力的提议,但在所有包含四星级评级的特性中,成本节约和简单性之间存在权衡。成本和复杂性越高,这些评级就越好。
由于领域服务的粗粒度特性,基于服务的架构往往比其他分布式架构更可靠。更庞大的服务意味着到服务之间的网络通信量更少,分布式事务更少,使用的带宽更少,因此提高了网络的整体可靠性。
何时使用这种架构风格
这种架构风格的灵活性(参见“拓扑结构变体”一节)加上三星和四星的架构特性评级的数量,使基于服务的架构成为最实用的架构风格之一。当然还有其他更强大的分布式架构风格,但是一些公司发现功能强大伴随着价格的急剧升高,而另一些公司发现他们根本不需要那么强大的功能。这就像拥有法拉利的动力、速度和敏捷性,但只用于在高峰时段以每小时50公里的速度来回行驶上班,这看起来很酷,但这绝对是对资源和金钱的浪费!
基于服务的架构天然适合进行领域驱动设计。因为服务是粗粒度的和按领域划分的,所以每个域都很好地适合于一个单独部署的领域服务。基于服务的架构中的每个服务都围绕一个特定的领域(例如电子设备回收应用中的回收),因此将该功能划分为一个软件单元,从而更容易在该领域里进行变更。
维护和协调数据库事务始终是分布式架构的一个问题,因为它们通常依赖于最终的一致性,而不是传统的ACID(原子性、一致性、隔离性和持久性)事务。然而,由于领域服务的粗粒度特性,基于服务的架构比任何其他分布式体系结构都能更好地维护ACID事务。在某些情况下,用户界面或API网关可能编排到两个或多个域服务,在这些情况下,事务将需要依赖sagas和BASE事务。然而,在大多数情况下,事务的作用域限定在特定的领域服务,这使得可以使用在大多数单体应用中常见的传统提交和回滚事务功能。
最后,基于服务的架构在实现良好的体系结构模块化,而不必纠结于粒度的复杂性和陷阱时是一个很好的选择。随着服务变得更加细粒度,围绕服务编制(Orchestration)和编排(choreography)的问题开始出现。当必须协调多个服务以完成某个业务事务时,服务编制和编排是必需的。编制是通过使用单独的中介服务来协调多个服务,该服务控制和管理事务的工作流(就像管弦乐队中的指挥)。另一方面,编排是多个服务之间的协调,每个服务彼此之间进行对话,而不需要使用中央中介(就像舞蹈中的舞者)。随着服务变得更加的细粒度,编制和编排都是将服务捆绑在一起以完成业务事务所必需的。然而,由于基于服务的架构中的服务往往更粗粒度,因此它们不需要像其他分布式架构那样需要协调。