| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
parent directory.. | ||||
单体应用程序示例 (FTGO服务端架构图--其业务类似于美团)
FTGO的核心业务其实非常简单。消费者(Consumer)使用FTGO的网站或者移动应用在本地的餐馆(Restaurant)下订单,FTGO会协调一个由送餐员(Courier)组成的快递网络来完成订单食品(Order)的运送(Delivery)。显然,给送餐员和餐馆支付费用(Payment)也是FTGO的重要任务之一。这套应用程序使用了多个Web 服务,例如使用 Stripe 管理支付、使用Twilio 实现消息传递、使用Amazon SES(Simple Email Service)发送电子邮件,等等。 与其他陈旧的企业应用程序一样,FTGO 的应用程序是一个单体,它由一个单一的 Java WAR文件构成。随着时间的推移,这个文件变成了一个庞大的复杂的应用程序。尽管 FTGO开发团队做出了最大的努力,但这个应用程序已成为“泥球模式”的一个典型例子。泥球模式的作者Brian Foote 和Joseph Yoder 把这样的软件比喻为“随意架构的、庞大的、草率的、布满了胶带和线路,如同意大利面条一般的代码丛林”。软件交付的步伐已经放缓。更糟糕的是,FTGO应用程序是使用一些日益过时的框架编写的。FTGO应用程序展示了单体地狱的几乎所有症状。
结合游戏开发的示例如下图*(源自《Java 游戏服务器架构实战》)*,当游戏功能较少时,使用单体模式开发是比较快速的,但是随着功能增多、开发人员变动等因素影响就会出现功能重复、耦合验证等问题。
单体架构的好处
单体地狱的结果
过度的复杂性会吓退开发者
开发速度缓慢
从代码提交到实际部署的周期很长,而且容易出问题
从代码完成到运行在生产环境是一个漫长且费力的过程。一个问题是,众多开发人员都向同一个代码库提交代码更改,这常常使得这个代码库的构建结果处于无法交付的状态。 把更改推向生产环境的另一个挑战是运行测试需要很长时间。因为代码库如此复杂,以至于一个更改可能引起的影响是未知的,为了避免牵一发而动全身的后果,即使是一个微小的更改,开发人员也必须在持续集成服务器上运行所有的测试套件。系统的某些部分甚至还需要手工测试。如果测试失败,诊断和修复也需要更多的时间。因此,完成这样的测试往往需要数天甚至更长时间。
难以扩展
因为在有些情况下,应用的不同模块对资源的需求是相互冲突的。例如,餐馆数据保存在一个大型的内存数据库中,理想情况下运行这个应用的服务器应该有较大容量的内存。另外,图片处理模块又需要比较快的 CPU来完成图形运算,这需要应用部署在具有多个高性能CPU 的服务器之上。因为这些模块都是在一个应用程序内,因此FTGO在选用服务器时必须满足所有模块的需要
交付可靠的单体应用是一项挑战
系统不可靠的一个原因是应用程序体积庞大而无法进行全面和彻底的测试。缺乏可靠的测试意味着代码中的错误会进入生产环境。更糟糕的是,该应用程序缺乏故障隔离,因为所有模块都在同一个进程中运行。每隔一段时间,在一个模块中的代码错误,例如内存泄漏,将会导致应用程序的所有实例都崩溃。
扩展立方体
X轴扩展是扩展单体应用程序的常用方法。在负载均衡器之后运行应用程序的多个实例。负载均衡器在 N个相同的实例之间分配请求。这是提高应用程序吞吐量和可用性的好方法。
Z轴扩展也需要运行单体应用程序的多个实例,但不同于X轴扩展,每个实例仅负责数据的一个子集。图 1-5 展示了Z轴扩展的工作原理。置于前端的路由器使用请求中的特定属性将请求路由到适当的实例。例如,应用程序可能会使用请求中包含的 user Id 来路由请求。在这个例子中,每个应用程序实例负责一部分用户。该路由器使用请求 Authorization头部指定的user Id来从N个相同的应用程序实例中选择一个。对于应用程序需要处理增加的事务和数据量时,Z轴扩展是一种很好的扩展方式。
X轴和Z轴扩展有效地提升了应用的吞吐量和可用性,然而这两种方式都没有解决日益增长的开发问题和应用复杂性。为了解决这些问题,我们需要采用Y轴扩展,也就是功能性分解。Y轴扩展把一个单体应用分成了一组服务,如图 1-6 所示。
服务本质上是一个麻雀虽小但五脏俱全的应用程序,它实现了一组相关的功能,例如订单管理、客户管理等。服务可以在需要的时候借助X轴或Z轴方式进行扩展。例如,订单服务可以被部署为一组负载均衡的服务实例。 我对微服务架构的概括性定义是: 把应用程序功能性分解为一组服务的架构风格。请注意这个定义中并没有包含任何与规模有关的内容。重要的是,每一个服务都是由一组专注的、内聚的功能职责组成。
微服务特点:
微服务架构与SOA的异同
| SOA | 微服务 | |
|---|---|---|
| 技术栈 | 重量级技术,例如 SOAP 和其他类似的 WS* 标准 | 轻量级、开源的技术,如 REST 或 g RPC 等协议 |
| 服务通信方式 | 采用 ESB 进行服务集成,包含业务和消息处理逻辑的智能管道 | 使用哑管道(如消息代理)进行通信 |
| 数据处理方式 | 有全局的数据模型,共享数据库 | 每个服务有自己的数据库和领域模型 |
| 服务规模 | 集成大型、复杂的单体应用程序 | 通常比较小,应用由数十甚至上百个更小的服务组成 |
| 主要优点 | 可复用性高 | 粒度小、自治性高、弹性好 |
| 主要缺点 | 部署和维护复杂 | 跨服务事务管理复杂 |
微服务优点
使大型的复杂应用程序可以持续交付和持续部署每个服务都相对较小并容易维护。
服务可以独立部署。
服务可以独立扩展。
服务可以独立扩展,不论是采用X轴扩展的实例克隆,还是Z 轴扩展的流量分区方式此外,每个服务都可以部署在适合它们需求的硬件之上。这跟使用单体架构的部署和硬件选择是迥然不同的:单体应用中组件对硬件的需求不同(例如有些组件是 CPU 运算密集型的有些可能需要更多的内存空间),但是这些组件仍旧必须被部署在一起。
微服务架构可以实现团队的自治
更容易实验和采纳新的技术。
更好的容错性。
微服务的弊端
微服务架构并不是“银弹” 早在1986年,《人月神话》(Addison-Wesley Professional,1995)的作者 Fred Brooks 就曾说:软件工程的世界里没有银弹。换一种说法,并不存在一种或几种技术,可以把你的生产效率提升10倍。
目前的设计模式以及框架并不能有效提高系统效率,甚至会降低效率;应选择适合现状的架构,不应过度迷恋新技术,导致过度设计,从而导致系统的维护成本变高。
模式
描述解决一个特定问题的最佳实践或经验的通用形式。模式通常是一种抽象的、独立于具体实现的描述,它描述了一个通用的、可重复使用的解决方案,可以被应用到不同的具体场景中。模式通常包含了一组规则、约定、设计原则、代码实现等等,这些元素共同组成了一个完整的解决方案。
模式语言
一组相互关联的设计模式,它们共同构成一个更大的、综合性的设计思想或实践。模式语言通常包含了许多具体的设计模式,这些模式之间有着相互依赖和互补的关系,可以用来描述一个复杂的、大型的、具有复杂结构和相互作用的软件系统。模式语言的设计目的是为了让开发人员能够更好地理解和应对软件开发中的复杂性。
软件架构定义
应用程序的架构是将软件分解为元素(element)和这些元素之间的关系(relation)。由于以下两个原因,分解很重要:
软件架构的4+1视图模型,来描述一下软件架构
逻辑视图的架构风格
分层式:常见三层架构
六边形
六边形架构是分层架构风格的替代品。如图 2-2 所示,六边形架构风格选择以业务逻辑为中心的方式组织逻辑视图。应用程序具有一个或多个入站适配器,而不是表示层,它通过调用业务逻辑来处理来自外部的请求。同样,应用程序具有一个或多个出站适配器,而不是数据持久化层,这些出站适配器由业务逻辑调用并调用外部应用程序。此架构的一个关键特性和优点是业务逻辑不依赖于适配器。相反,各种适配器都依赖业务逻辑。
六边形架构将应用程序分成三个部分:
| 架构类型 | 分层架构 | 六边形架构 |
|---|---|---|
| 理念 | 将系统按照职责划分成若干层,每一层负责特定的任务 | 将系统按照用户角色和功能划分成若干层,每一层负责特定的任务 |
| 关注点 | 关注业务逻辑和数据访问层的实现 | 关注业务流程和交互逻辑的实现 |
| 测试性 | 容易进行单元测试,但需要进行大量集成测试 | 更容易进行集成测试,但需要进行大量单元测试 |
| 依赖性 | 存在层与层之间的依赖,需要避免底层对高层的影响 | 各层之间基于接口协作,各层之间相对独立,减少了依赖性 |
| 可维护性 | 修改某一层的实现可能会影响整个系统 | 更容易进行局部的修改和调整,保持系统的整体稳定性 |
| 可扩展性 | 可以通过增加新的层来扩展系统功能(如:三方服务层) | 可以通过增加新的角色和功能来扩展系统功能 (提供三方服务对应的接口) |
| 可重用性 | 存在较高的代码复用性,但需要注意层的耦合度 | 各层之间基于接口协作,便于组件的重用和二次开发 |
由于这种分离,单独测试业务逻辑要容易得多。另一个好处是它更准确地反映了现代应用程序的架构。可以通过多个适配器调用业务逻辑,每个适配器实现特定的 API或用户界面。业务逻辑还可以调用多个适配器,每个适配器调用不同的外部系统。六边形架构是描述微服务架构中每个服务的架构的好方法。
实现视图的架构风格
单体架构
微服务架构
它的实现视图由多个组件构成:一组可执行文件或 WAR文件。它的组件是服务,连接器是使这些服务能够协作的通信协议。每个服务都有自己的逻辑视图架构,通常也是六边形架构。
定义应用程序的微服务架构
系统操作(system operation)是应用程序必须处理的请求的一种抽象描述。它既可以是更新数据的命令,也可以是检索数据的查询。每个命令的行为都是根据抽象领域模型定义的,抽象领域模型也是从需求中派生出来的。系统操作是描述服务之间协作方式的架构场景。
该流程的第二步是确定如何分解服务。有几种策略可供选择。
但这些策略的最终结果都是围绕业务概念而非技术概念分解和设计的服务。
定义应用程序架构的第三步是确定每个服务的 API。为此,你将第一步中标识的每个系统操作分配给服务。服务可以完全独立地实现操作。或者,它可能需要与其他服务协作。在这种情况下,你可以确定服务的协作方式,这通常需要服务来支持其他操作。你还需要确定选用第3章中描述的哪种进程间通信机制来实现每个服务的API。
拆分服务的指导原则
单一职权原则
闭包原则
在包中包含的所有类应该是对同类的变化的一个集合,也就是说,如果对包做出修改,需要调整的类应该都在这个包之内。
拆分服务的难点
网络延迟。
同步进程间通信导致可用性降低。
如何处理进程间通信而不降低系统的可用性。例如,实现create Order()操作最常见的方式是让order Service 使用REST同步调用其他服务这样做的弊端是REST这样的协议会降低order Service 的可用性。如果任何一个被调用的服务处在不可用的状态,那么订单就无法创建了。有时候这可能是一个不得已的折中,但是在第 3 章中学习异步消息之后,你就会发现其实有更好的办法来消除这类同步调用产生的紧耦合并提升可用性。
在服务之间维持数据一致性。
获取一致的数据视图。
分解的另一个障碍是无法跨多个数据库获得真正一致的数据视图。在单体应用程序中,ACID 事务的属性保证查询将返回数据库的一致视图。相反,在微服务架构中,即使每个服务的数据库是一致的,你也无法获得全局一致的数据视图。如果你需要一些数据的一致视图,那么它必须驻留在单个服务中,这也是服务分解所面临的问题。幸运的是,在实践中这很少带来真正的问题。
上帝类阻碍了拆分
上帝类通常为应用程序的许多不同方面实现业务逻辑。它有大量字段映射到具有许多列的数据库表。大多数应用程序至少有一个这样的上帝类,每个类代表一个对领域至关重要的概念:银行账户、电子商务订单、保险政策,等等。因为上帝类将应用程序的许多不同方面的状态和行为捆绑在一起,所以将使用它的任何业务逻辑拆分为服务往往都是一个不可逾越的障碍。
微服务架构将应用程序构建为一组服务, 这些服务必须经常协作才能处理各种外部请求。因为服务实例通常是运行在不同机器的进程,所以它们必须使用进程间通信进行交互。因此,进程间通信技术在微服务架构中比在单体架构中扮演着更重要的角色。
在为服务的API选择进程间通信机制之前,首先考虑服务与其客户端的交互方式是非常重要的。考虑交互方式将有助于专注需求,并避免陷入特定进程间通信技术的细节。交互方式的选择会影响应用程序的可用性。
两种维度:
一对一/一对多
同步/异步
交互方式并不会因为具体的进程间通信技术而发生改变。例如,一个服务使用请求/响应的交互方式与其他服务进行交互,底层的进程间通信技术可以是REST,也可以是消息机制。
进程中通信技术选型
消息格式
在单体应用程序中,接口通常采用编程语言结构定义,如java interface,java接口定义了一组客户端可以调用的方法。微服务架构,服务的API是服务与其客户端之间的契约(Contract),由客户端可以调用的方法和服务发布的事件组成。
如何定义API取决于使用的进程间通信机制。
API不可避免地会随着应用功能的增减而发生变化。在单体应用中,变更API并更新所有调用方的代码相对简单,唯一的挑战在于变更的范围。在基于微服务架构的应用中改变服务的API就没这么容易了,服务的客户端可能是另外的服务,通常是其他团队所开发的,客户端也极有可能是由组织之外的人所开发和控制的。不能强制要求客户端和服务端的API版本保持一致。另外通过滚动升级的方式来更新服务,新旧版本的服务肯定会共存。具体的措施取决于API演化的实际情况。
语义化版本控制规范要求版本号由三部分组成:MAJOR.MINOR.PATCH。必须按如下的方式递增版本号:
不得不做
HTTP
进程间通信的本质是交换消息。确定消息数据的格式是一个非常重要的设计决策。消息格式的选择会对进程间通信的效率、API的可用性和可演化性产生影响。
消息的格式可以分为两大类:文本和二进制。
常用的格式有 XML、JSON。
优点:
缺点:
常用的格式有Protocol Buffers(tagged fields,较易于API版本升级)、Avro。
优点:
缺点:
客户端中的业务逻辑调用代理接口,这个接口由远程过程调用代理适配器类实现。远程过程调用代理向服务发出请求。该请求由远程过程调用服务器适配器类处理,该类通过接口调用服务的业务逻辑。然后它将回复发送回远程过程调用代理,该代理将结果返回给客户端的业务逻辑。
REST是一种使用HTTP协议的进程间通信机制。
REST的一个关键概念是资源,它通常表示单个业务对象或业务对象的集合。REST使用HTTP动词来操作资源,使用URL引用这些资源。如,GET请求返回资源的表现形式,POST请求创建资源,PUT请求更新资源。
Leonard Richardson为REST定义了一个成熟度模型,具体包含以下四个层次。
定义REST API 最流行的REST IDL是Open API规范,它是从Swagger开源项目发展而来的。Swagger项目是一组用于开发和记录REST API的工具。它包括从接口定义到生成客户端桩(stub,存根)和服务器骨架的一整套工具。 在一个请求中获取多个资源的挑战 REST资源通常以业务对象为导向。因此,设计REST API时的一个常见问题是如何使客户端能够在单个请求中检索多个相关对象。 把操作映射为HTTP动词的挑战 另一个常见的REST API设计问题是如何将要在业务对象上执行的操作映射到HTTP动词。 REST API应该使用PUT进行更新,但可能有多种方法来更新订单,包括取消订单、修改订单等。此外,更新可能不是幂等的,但这却是使用PUT的要求。一种解决方案是定义用于更新资源的特定方面的子资源。例如,Order Service具有用于取消订单的POST/orders/{orderId}/cancel端点,以及用于修订订单的POST/orders/{orderId}/revise端点。另一种解决方案是将动词指定为URL的查询参数。可惜的是,这两种解决方案都不是特别符合RESTful的要求。 REST的好处和弊端
优点:
缺点:
虽然存在这些缺点,但REST似乎是API的事实标准。
gRPC是一个用于编写跨语言客户端和服务端的框架。gRPC是一种基于二进制消息的协议,所以必须采用API优先的方法来进行服务设计。gRPC使用Protocol Buffer作为消息格式,可以使用基于Protocol Buffers的IDL定义gRPC API。
优点:
缺点:
分布式系统中,当服务试图向另一个服务发送同步请求时,永远都面临着局部故障的风险。因为客户端和服务端是独立的进程,服务端很有可能无法在有限的时间内对客户端的请求做出响应。服务端可能因为故障或维护的原因而暂停。或者,服务端也可能因为过载而对请求的响应变得极其缓慢。 客户端等待响应被阻塞,这可能带来的麻烦就是在其他客户端甚至使用服务的第三方应用之间传导,并导致服务中断。
例如,考虑图3-2所示的场景,其中Order Service无响应。移动客户端向API Gateway发出REST请求,如第8章所述,它是API客户端应用程序的入口点。API Gateway将请求代理到无响应的Order Service。
OrderServiceProxy将无限期地阻塞,等待响应。这不仅会导致糟糕的用户体验,而且在许多应用程序中,它会消耗宝贵的资源,例如线程。最终,API Gateway将耗尽资源,无法处理请求。整个API都不可用。 要通过合理地设计服务来防止在整个应用程序中故障的传导和扩散,这是至关重要的。解决这个问题分为两部分:
首先,我们将看看如何编写健壮的远程过程调用代理。 开发可靠的远程过程调用代理 服务同步调用时应采用的保护方法。
从服务失效故障中恢复
向其客户端返回错误。
在其他情况下,返回备用值(fallback value,例如默认值或缓存响应)可能会有意义。
服务实例具有动态分配的网络位置。此外,由于自动扩展、故障和升级,服务实例集会动态更改。因此,你的客户端代码必须使用服务发现。
服务发现在概念上非常简单:其关键组件是服务注册表,它是包含服务实例网络位置信息的一个数据库。 服务实例启动和停止时,服务发现机制会更新服务注册表。当客户端调用服务时,服务发现机制会查询服务注册表以获取可用服务实例的列表,并将请求路由到其中一个服务实例。
实现服务发现有以下两种主要方式:
服务实例使用服务注册表注册其网络位置。客户端首先通过查询服务注册表获取服务实例列表来调用服务,然后它向其中一个实例发送请求。
优点:
缺点:
部署平台为每个服务提供DNS名称、虚拟IP(VIP)地址和解析为VIP地址的DNS名称。客户端向DNS名称和VIP发出请求,部署平台自动将请求路由到其中一个可用服务实例。因此,服务注册、服务发现和请求路由完全由部署平台处理。
优点:
缺点:
使用消息机制时,服务之间的通信采用异步交换消息的方式完成。基于消息机制的应用程序通常使用消息代理,它充当服务之间的中介。另一种选择是使用无代理架构,通过直接向服务发送消息来执行服务请求。服务客户端通过向服务发送消息来发出请求。如果希望服务实例回复,服务将通过向客户端发送单独的消息的方式来实现。由于通信是异步的,因此客户端不会堵塞和等待回复。相反,客户端都假定回复不会马上就收到。
消息通过消息通道进行交换。发送方(应用程序或服务)将消息写入通道,接收方(应用程序或服务)从通道读取消息。
消息由消息头部和消息主体组成。消息头是名称与值对的集合,描述正在发送的数据的元数据,消息主体是以文本或二进制格式发送的数据。
有以下几种不同类型的消息。
消息通过消息通道进行交换。发送方中的业务逻辑调用发送端接口,该接口封装底层通信机制。发送端由消息发送适配器类实现,该消息发送适配器类通过消息通道向接收器发送消息。消息通道是消息传递基础设施的抽象。调用接收器中的消息处理程序适配器类来处理消息。它调用接收方业务逻辑实现的接收端接口。
有以下两种类型的消息通道:点对点(www.enterpriseintegrationpatterns.com/PointToPointChannel.html )和发布-订阅(www.enterpriseintegrationpatterns.com/PublishSubscribeChannel.html )。
当客户端和服务使用请求/响应或异步请求/响应进行交互时,客户端会发送请求,服务会发回回复。两种交互方式之间的区别在于,对于请求/响应,客户端期望服务立即响应,而对于异步请求/响应,则没有这样的期望。消息机制本质上是异步的,因此只提供异步请求/响应。但客户端可能会堵塞,直到收到回复。
客户端和服务端通过交换一对消息来实现异步请求/响应方式的交互。客户端发送命令式消息,该消息指定要对服务执行的操作和参数,这些内容通过服务拥有的点对点消息通道传递。该服务处理请求,并将包含结果的回复消息发送到客户端拥有的点对点通道。
服务的异步API规范必须指定消息通道的名称、通过每个通道交换的消息类型及其格式。你还必须使用诸如JSON、XML或Protobuf之类的标准来描述消息的格式。但与REST和Open API不同,并没有广泛采用的标准来记录通道和消息类型,你需要自己编写这样的文档。
服务的异步API包含供客户端调用的操作和由服务对外发布的事件。
基于消息传递的应用程序通常使用消息代理,即服务通信的基础设施服务。但基于消息代理的架构并不是唯一的消息架构。你还可以使用基于无代理的消息传递架构,其中服务直接相互通信。这两种方法具有不同的利弊,但通常基于代理的架构是一种更好的方法。
服务可以直接交换消息。ZeroMQ(http://zeromq.org )
优点:
缺点:
消息代理是所有消息的中介节点。发送方将消息写入消息代理,消息代理将消息发送给接收方。使用消息代理的一个重要好处是发送方不需要知道接收方的网络位置。另一个好处是消息代理缓冲消息,直到接收方能够处理它们。
流行的开源消息代理包括:
选择消息代理时,你需要考虑以下各种因素:
基于代理的消息的好处和弊端:
优点:
缺点:
为了同时处理消息,拥有多个实例是一个常见的要求。而且,即使单个服务实例也可能使用线程来同时处理多个消息。使用多个线程和服务实例来并发处理消息可以提高应用程序的吞吐量。但同时处理消息的挑战是确保每个消息只被处理一次,并且是按照它们发送的顺序来处理的。
常见解决方案是使用分片(分区)通道。该解决方案分为三个部分。
每个Order事件消息都将orderId作为其分片键。特定订单的每个事件都发布到同一个分片,而且该分片中的消息始终由同一个接收方实例读取。因此,这样做就能够保证按顺序处理这些消息。
当系统正常工作时,保证传递的消息代理只会传递一次消息。但是客户端、网络或消息代理的故障可能导致消息被多次传递。假设客户端在处理消息后、发送确认消息之前,它的数据库崩溃了,这时消息代理将再次发送未确认的消息,在数据库重新启动时向该客户端或客户端的另一个副本发送。
理想情况下,你应该使用消息代理,在重新传递消息时保留排序。
处理重复消息有以下两种不同的方法。
服务通常需要在更新数据库的事务中发布消息。传统的解决办法是在数据库和消息代理之间使用分布式事务。分布式事务对现今的应用程序而言并不是一个很好的选择。而且,很多新的消息代理,例如Apache Kafka并不支持分布式事务。因此,应用必须采用不同的机制确保消息的可靠发送。
假设应用程序正在使用关系型数据库。可靠地发布消息的直接方法是应用事务性发件箱模式。此模式使用数据库表作为临时消息队列。如图3-13所示,发送消息的服务有一个OUTBOX数据库表。作为创建、更新和删除业务对象的数据库事务的一部分,服务通过将消息插入到OUTBOX表中来发送消息。这样可以保证原子性,因为这是本地的ACID事务。
OUTBOX表充当临时消息队列。MessageRelay是一个读取OUTBOX表并将消息发布到消息代理的组件。
对于NoSQL数据库可以使用类似的方法。作为record存储在数据库中的每个业务实体都有一个属性,该属性是需要发布的消息列表。当服务更新数据库中的实体时,它会向该列表附加一条消息。这是原子的,因为它是通过单个数据库操作完成的。但是,挑战在于有效地找到那些拥有事件并发布事件的业务实体。
将消息从数据库移动到消息代理并对外发送有两种不同的方法。
如果应用程序使用关系型数据库,则对外发布插入OUTBOX表的消息的一种非常简单的方法是让MessageRelay在表中轮询未发布的消息。接下来,MessageRelay把这些消息发送给消息代理,它把每个消息发送给它们的目的消息通道。最后,MessageRelay把完成发送的消息从OUTBOX表中删除。
轮询数据库是一种在小规模下运行良好的简单方法。其弊端是经常轮询数据库可能造成昂贵的开销(导致数据库性能下降)。此外,你是否可以将此方法与NoSQL数据库一起使用取决于NoSQL数据库支持的查询功能。这是因为应用程序必须查询业务实体,而不是查询OUTBOX表,这可能会无法有效地执行。
每次应用程序提交到数据库的更新都对应着数据库事务日志中的一个条目。事务日志挖掘器可以读取事务日志,把每条跟消息有关的记录发送给消息代理。图3-14展示了这个方案的具体实现方式。 Transaction-Log-Miner读取事务日志条目。它将对应于插入消息的每个相关日志条目转换为消息,并将该消息发布到消息代理。此方法可用于发布写入关系型数据库中的OUTBOX表的消息或附加到NoSQL数据库中的记录的消息。
我们需要在不同的进程间通信机制之间权衡利弊。其中的一个重要权衡因素,就是进程间通信机制与系统的可用性之间的关系。
只要服务使用同步协议进行通信,就可能降低应用程序的可用性。
当服务必须从另外一个服务获取信息后,才能够返回它客户端的调用,这种情况都会导致可用性问题。即使服务使用异步消息的请求/响应方式的交互进行通信,也存在此问题。例如,如果通过消息代理向Consumer Service发送消息然后等待响应,则Order Service的可用性将会降低。
如果你想最大化一个系统的可用性,就应该设法最小化系统的同步操作量。
在必须处理同步请求的情况下,仍旧有一些方式可以最大限度地降低同步通信的数量。当然,最彻底的方式还是把所有的服务都改成异步API,但是在现实情况下这并不太可能,例如一些公用API总是采用RESTful方式,另外有些情况下服务也必须被设计为采用同步API。
幸运的是,总有一些办法在不发出同步调用请求的情况下来处理同步的调用请求。
理想的情况是,所有的交互都应该使用本章之前所描述的异步交互。例如,让我们假设FTGO采用请求/异步响应的交互方式来创建订单。客户端可以通过向Order Service发送一个请求消息交换消息的方式创建订单。这个服务随即采用异步交换消息的方式跟其他服务通信完成订单的创建,并向客户端发回一个返回消息。
客户端和服务端使用消息通道发送消息来实现异步通信。这个交互过程中不存在堵塞等待响应的情况。 这样的架构非常有弹性,因为消息代理会一直缓存消息,直到有服务端接收并处理消息。
服务维护一个数据副本,这些数据是服务在处理请求时需要使用的。这些数据的源头会在数据变化时发出消息,服务订阅这些消息来确保数据副本的实时更新。
具体办法:
当处理请求时,服务并不需要与其他服务直接进行同步交互。取而代之的是,服务异步向其他的服务发送消息。这种方式确保了服务之间的松耦合。
弊端是它使客户端更复杂。
在只访问一个数据库的单体应用中,事务管理是简单明了的。一些较为复杂的单体应用可能会使用多个数据库和消息代理。更进一步,微服务架构下的事务往往需要横跨多个服务,每个服务都有属于自己的私有数据库。在这种情况下,应用程序必须使用一些更为高级的事务管理机制来管理事务。
已createOrder()操作为例,在微服务架构下,所需要的验证数据散布在不同的服务中。createOrder()操作必须访问多个服务来获得它所需要的验证数据。
在多个服务、数据库和消息代理之间维持数据一致性的传统方式是采用分布式事务。其事实标准是X/Open XA。XA采用了两阶段提交来保证事务中的所有参与方同时完成提交,或在失败时同时回滚。应用程序的整个技术栈都需要满足XA标准。市面上绝大多数的SQL数据库和一部分消息代理满足XA标准。
问题:
Saga是一种在微服务架构中维护数据一致性的机制。它通过使用异步消息来协调一系列本地事务,从而维护多个服务之间的数据一致性,每个本地事务负责更新它所在服务的私有数据库,这些操作仍旧依赖于我们所熟悉的ACID事务框架和函数库。
Saga的实现包含协调Saga步骤的逻辑。当通过系统命令启动Saga时,协调逻辑必须选择并通知第一个Saga参与方执行本地事务。一旦该事务完成,Saga协调选择并调用下一个Saga参与方。这个过程一直持续到Saga执行完所有步骤。如果任何本地事务失败,则Saga必须以相反的方式执行补偿事务。
构建Saga协调逻辑的两种方法:
图解:
基于协同式的Saga参与方使用发布/订阅进行交互。
在实现基于协同的Saga时,必须考虑一些服务间通信相关的问题。
确保Saga参与方将其更新本地数据库和发布事件作为数据库事务的一部分。
确保Saga参与方必须能够将接收到的每个事件映射到自己的数据上。
好处:
弊端:
开发人员定义一个编排器类,该类唯一的职责是告诉Saga的参与方该做什么事清。Saga编排器使用命令 / 异步响应方式与Saga参与方服务通信。
基于编排式是Saga的每个步骤都包括一个更新数据库和发布消息的服务。
图解:
Order Service首先创建(实例化)一个Order对象和一个Create Order Saga编排器对象,一切正常后流程如下:
状态机是由一组状态和一组由事件触发的状态之间的转换组成。每个转换都可以有一个动作,对Saga来说动作就是对某个参与方对调用。
将Saga建模成状态机非常有用,因为它描述了所有可能的场景(可能成功也可能失败)。
好处:
弊端:
ACID事务的隔离性可确保同时执行多个事务的结果与顺序执行它们的结果相同。而Saga只满足ACD(原子性、一致性、持久性),不满足隔离性。
缺乏隔离将导致以下三个异常。
语义锁:应用程序级的锁;
交换式更新:把更新操作设计成可以按任何顺序执行;
悲观视图:重新排序Saga的步骤,以最大限度地降低业务风险;
重读值:通过重写数据来防止脏写,以在覆盖数据之前验证它是否保持不变;
版本文件:将更新记录下来,以便可以对它们重新排序;
业务风险评级(by value):使用每个请求的业务风险来动态选择并发机制;
一个Saga包含三个类型的事务。
事件溯源开发业务逻辑概述------事件溯源,将聚合以⼀系列事件的⽅式持久化保存,每个事件代表聚合的⼀次状态变化, 应⽤程序通过重放(Replaying)事件来重新创建聚合的当前状态。
事件溯源是⼀种以事件为中⼼的技术,⽤于实现业务逻辑和聚合的持久化。聚合作为⼀系 列事件存储在数据库中。每个事件代表聚合的状态变化。聚合的业务逻辑围绕⽣成和使⽤ 这些事件的要求⽽构建。让我们看看它是如何⼯作的。
当应⽤程序创建或更新聚合时,它会将聚合发出的事件插⼊到EVENTS表中。应⽤程 序通过从事件存储中检索并重放事件来加载聚合。加载聚合包含以下三个步骤: 加载聚合的事件 使⽤其默认构造函数创建聚合实例 调⽤apply()⽅法遍历事件。
事件代表状态的变化
使⽤事件溯源时,事件不再是可有可⽆的。每当聚合的状态发⽣变化时,它必须发出 ⼀个事件。例如,Order聚合必须在创建时发出OrderCreated事件,并在更新时发出 Order*事件。这是⼀个⽐以前更严格的要求,在此之前,聚合只需要发出那些外部接 收⽅感兴趣的事件。
聚合⽅法都和事件有关
业务逻辑通过调⽤聚合根上的命令⽅法(command method)来处理对聚合的更新请 求。在传统的应⽤程序中,命令⽅法通常会验证其参数,然后更新⼀个或多个聚合字 段。基于事件溯源的应⽤程序中的命令⽅法则通过⽣成事件来起作⽤
使⽤乐观锁处理并发更新
使⽤快照提升性能
在⼀个Order聚合的⽣命周期中往往不会发⽣很多次状态转换,因此它只有少量事件。查 询事件存储库,找到这些事件并重新构建Order聚合是可⾏的。但是,⻓⽣命周期的聚合 可能会有⼤量事件,例如:Account聚合可能包含⼤量事件,随着事件的推移,加载和重 放这些事件会变得越来越低效。 常⻅的解决⽅案就是定期持久化保存聚合状态的快照,应⽤程序通过加载最新快照以及加 载快照后发⽣的事件来快速恢复聚合状态
消息的幂等处理
如果可以使⽤相同的消息多次安全的调⽤消息接收⽅,则消息接收⽅式幂等的。例如,通 过检测和丢弃重复消息来实现幂等的消息处理。
关系型数据库事件存储库的幂等消息处理
如果应⽤程序使⽤基于关系型数据库的事件存储库,则它可以使⽤相同的⽅法来检测 和丢弃重复消息。它将消息ID插⼊PROCESSED MESSAGES表,作为插⼊EVENTS 表的事件的事务的⼀部分。
NOSQL数据库事件存储库的幂等消息处理
消息消费者将消息的ID存储在处理它时⽣成的事件中,通过验证聚合的所有事件中是 否包含该消息ID来进⾏重复检测。
事件溯源的优势
事件溯源的弊端
自动化测试通常包括四个阶段
测试替身(Test double)来消除被测系统的依赖性。测试替身是一个对象,该对象负责模拟依赖项的行为。有两种类型的测试替身:(stub)和模(mock)。术语和模拟通常可以互换使用,尽管它们的行为略有不同。桩是一个测试替身,它代替依赖项来向被测系统发送调用的返回值。模拟是也一个测试替身,用来验证被测系统是否正确调用了依赖项。此外,模拟通常也扮演桩的角色,向被测系统发送调用的返回值。
组织测试的第一种方法---测试象限
组织测试的方法--测试金字塔,它提供了关于每种测试类型要编写测试数量的指南
消费者契约测试侧重于验证提供者 API的参数定义是否符合消费者的期望。对于REST接口,契约测试将验证提供者程序实现的接口是否:
具有预期的HTTP方法和路径
接受预期的HTTP头部,如果有的话a
接受请求主体,如果有的话。
返回预期中的响应,包括状态代码、头部和主体。
重要的是要记住,契约测试不会彻底测试提供者的业务逻辑。这是单元测试的工作。稍你将看到RESTAPI的消费者契约测试实际上是通过模拟控制器进行的测试。
单元测试
| Back | FazBrowse Home | New Git URL |