本文将介绍微服务架构设计中的设计模式、原则及最佳实践。我们将使用适当的架构设计模式和技术。
通过本文,你将了解到如何从单体架构一步步演进到事件驱动的微服务架构,如何利用微服务分布式架构设计一个高可用、高可扩展、低延迟且对网络故障有弹性的系统,以处理数以百万计的请求。
我们将从基本的软件架构设计入手,设计一个可以处理少量请求的单体架构的电子商务应用。
架构设计之旅
之后,我们将介绍该架构如何一步步演进:
本文既有理论知识,又有实用信息:
因此,我们会对架构进行迭代设计,从单体架构逐步演进为事件驱动的微服务架构。
我们将根据以下问题来演进架构:
因此,我们是从以下几个方面来改进架构:
可扩展性和可靠性可以衡量应用程序能够为终端用户提供何种程度的服务。如果我们的电子商务应用可以为数百万用户提供服务而不出现可以觉察的停机,那么我们就可以说这个系统是高度可扩展和可靠的。可扩展性和可用性可能是设计良好的架构需要考虑的主要因素。
可扩展性 = 电子商务应用程序应能为数百万用户提供服务
可用性 = 电子商务应用应该 7*24 小时可用
可维护性 = 电子商务应用应该可以发展数年之久
效率 = 电子商务应用的响应延迟在可接受的范围内,如小于 2 秒,即低延迟
现在让我们看下可接受的延迟。如果我们的应用程序用户越来越多,我们如何让应用程序的延迟在可接受的范围内?请看下表:
从表中可以看出,我们的电子商务应用是一个小型应用,开始只有 2K 并发用户,每秒 500 个请求。我们将根据预期的体量来设计该电子商务应用的架构。
之后,随着业务不断增长,它将需要更多的资源来适应更大的请求数,你将看到我们如何根据这些数值来演进我们的架构。
几十年的软件开发演变出了许多方法和模式,它们各有各的优势,也都面临着各自的挑战。
因此,我们将从理解现有的方法入手来设计电子商务应用的架构,并逐步演进转移到云上。
为了理解云原生微服务,我们需要理解什么是单体应用程序,以及我们如何从单体架构迁移到微服务架构。
对于遗留应用,可以说大部分都是以单体架构为主实现的。
如果一个项目的所有功能都在一个代码库中,那么该应用就是单体应用。在单体模式中,用户界面、业务代码和数据访问的所有东西都在同一个代码库里。
所有应用关注点都包含在一个大的部署中。即使是单体应用也可以设计出不同的层次,如表现层、业务层和数据层,然后将该代码库部署为单个 jar/war 文件。
单体方法也有不少优点,我们将在即将推出的视频中讨论它们。在这里,我将介绍一些主要的优缺点。
由于只有一个代码库,所以很容易拉取并参与其中。而且,因为在同一个项目中,所以不同模块间的业务交互很容易调试。
遗憾的是,单体架构有许多许多缺点,如:
如你所见,我们了解单体架构。
虽然单体架构有很多缺点,但如果你正在构建一个小型应用程序,那么单体架构仍然是你可以在项目中采用的最佳架构之一。因为,在许多方面,单体应用程序都比较简单。
它们很容易:
与需要有经验的开发人员来识别和开发服务的微服务架构相比,它的开发相对简单。它更容易部署,因为只需要部署一个 jar/war 文件。
在这一节中,我们将使用单体架构一步一步地设计我们的电子商务应用程序。我们将根据需求逐步对架构设计进行迭代。
我们应该总是从编写 FR(功能需求)和 NFR(非功能需求)开始。
此外,最好在架构图中加上规则,以免忘记。
我们在设计架构时会考虑这些规则。
如你所见,我们使用单体架构设计了电子商务应用。
我们添加了一个大的 E-Commerce 框,它是电子商务应用程序的组成部分,其中包括商店用户界面、分类服务、SC 服务、折扣服务、订单服务。如你所见,这个传统 Web 应用程序的所有模块是容器中的一个工件。
这个单体应用有一个庞大的代码库,其中包括所有模块。如果要在这个应用程序中增加新模块,就必须对现有的代码进行修改,然后将代码修改后的工件部署到 Tomcat 服务器上。简单起见,我们遵循 KISS 原则。
我们将根据需求重构我们的设计,并一步步进行迭代。
从图中可以看出,我们增加了 2 台应用服务器,对单体架构做了横向扩展,并在单体应用的客户端和电子商务应用之间加了一个负载均衡器。
在单体架构中,为了实现扩展,我们需要增加 E-Commerce 应用服务器,并在应用程序之前放一个负载均衡器。
本质上,负载平衡器将接受请求并使用一致性哈希算法将请求发送到电子商务应用服务器,保证服务器的负载都一样。
现在我们看下技术选项——适配技术栈。
从图中可以看出,我们已经为电子商务单体应用选出了潜在的选项。负载均衡,NGINX 是很好的选择,还有 Java——Oracle 为这类应用提供了标准实现。
微服务是小型的业务服务,它们可以自主 / 独立部署,协同工作。
以下内容来自 Martin Fowlers 介绍微服务的文章:
微服务架构风格是一种将单个应用开发成一套小型服务的方法,每个服务都在自己的进程中运行,并通过轻量级的机制进行通信,通常是 HTTP 或 gRPC API。
因此,我们可以说,微服务架构是一种云原生的架构方法,利用这种方法设计出的应用程序由许多松耦合的、独立部署的小型组件组成。
——有自己的技术栈,包括数据库和数据管理模型;
——通过 REST API、事件流和消息代理等相互通信;
——按业务能力来组织,划分服务的界限通常被称为有界上下文。
在接下来的章节中,我们还将看到,如何利用有界上下文解耦微服务。
微服务的特点是小、独立和松耦合。一个小型开发团队就可以编写和维护一个服务。每个服务都有一个独立的代码库,可以由一个小型开发团队来管理。
服务可以独立部署。团队可以更新一个现有的服务,而不需要重新构建和部署整个应用程序。
服务负责持久化它们自己的数据或外部状态。这点与传统模式不同,在传统模式中,有一个单独的数据层处理数据持久性。
微服务最重要的一个特点是小,可以独立部署。
微服务应该足够小,以至于一个单功能团队就可以构建、测试和部署它。
微服务可以独立扩展,你可以单独扩展某个子服务,而无需扩展整个应用程序。
微服务应用程序有很多服务组成,这些服务需要协同工作来创造价值。由于服务很多,与单体应用相比,这意味着更多的移动部件。
由于微服务很小,而且服务之间需要通信,所以我们要管理网络问题。
微服务有自己的数据持久化。因此,数据一致性会成为一项挑战。
在这一节中,我们将一步步地设计微服务架构,并根据需求,逐步迭代架构设计。
在设计微服务架构时,我们遵循了"服务独享数据库模式"。微服务是单体应用模块分解而成的独立服务。
如此一来,现在这些数据库就可以是混合持久化。也就是说,根据每个微服务的存储需求不同,Product 微服务可以使用 NoSQL 文档数据库,SC 微服务可以使用 NoSQL 键值对数据库,Order 微服务可以使用关系型数据库。
让我们看下这个微服务架构图,并思考一下这个架构缺少了什么?这个架构的痛点是什么?我们怎么改进这个架构,才能提供更高的可扩展性、可用性,并且支撑更多的并发请求?
我们看到,UI 和微服务是直接通信的,这看上去很难管理。我们现在应该重点关注下微服务通信。
当迁移到基于微服务的应用程序时,最大的挑战之一是通信机制的变化。因为微服务是分布式的,微服务之间的通信是通过网络层面的服务间通信完成的。每个微服务都有自己的实例和进程。
因此,服务必须使用服务间通信协议,如 HTTP、gRPC 或消息代理协议 AMQP 进行交互。
由于微服务拥有复杂的结构,服务都是独立开发和部署,所以我们在考虑通信类型时应该谨慎,并在设计阶段做妥善处理。
如果你想基于微服务设计和构建具有多个客户端应用程序的复杂的大型应用程序,则建议使用 API 网关模式。
该模式提供了一个反向代理,将请求重定向或路由到内部微服务端点。API 网关为客户端应用程序提供一个单一的端点,它会在内部将请求映射到内部微服务。我们应该在客户端和内部微服务之间使用 API 网关。
API 网关可以处理像授权这样的横切关注点。因此,授权可以在集中式的 API 网关中处理,并发送给内部微服务,而不是在每个微服务中编写相关代码。同时,API 网关控制到内部微服务的路由,并能够将几个微服务的请求汇总到一个响应中。
总之,API 网关位于客户端应用程序和内部微服务之间。作为一个反向代理,它将请求从客户端路由到后端服务。它还提供横切关注点,如身份认证、SSL 终止和缓存。
我们将对电子商务应用程序的架构进行迭代,增加 API 网关模式。
从上图可以看出,客户端请求由单个入口点收集并路由到内部微服务。
它将处理客户端请求,并提供内部微服务路由。它还可以聚合多个内部微服务来响应一个客户端请求,并提供横切关注点,如身份认证和授权、速率限制和节流等等。
我们将继续演进我们的架构,但请看一下当前的设计,考虑下我们可以如何改进?
这里,有多个客户应用程序连接到单个 API 网关。我们应该小心这种情况,因为如果我们在这里只放置一个 API 网关,这意味着这里存在单点故障的风险。如果客户端应用程序增加,或 API 网关中业务逻辑的复杂性增加,这将是一种反模式。
因此,我们应该用 BFF-backends-for-frontends 模式解决这个问题。
基本上,Backends for Frontends 模式是针对特定的前端应用程序设置单独的 API 网关。我们有多个供前端应用消费的后端服务,我们在它们之间设置了 API 网关,用于处理路由和聚合操作。
不过,这产生了单一故障点。为了解决这个问题,BFF 提供了多个 API 网关,并根据客户端应用程序的边界进行分组,然后划分到不同的 API 网关。
单个复杂的 API 网关存在风险,并且会成为架构的瓶颈。通常,比较大的系统会按照客户端类型(如移动、Web 和桌面功能)暴露多个 API 网关。当你不想为多个界面定制单一的后端时,BFF 模式很有用。
所以我们应该根据用户界面的不同创建多个 API 网关。这些 API 网关可以与前端环境实现最佳匹配,而不用担心影响其他前端应用程序。
Backend for Frontends 模式为实现多网关指明了方向。
我们将根据 Backends for Frontends(BFF)模式迭代我们的电子商务应用架构,增加 API 网关。
如你所见,我们在应用程序中加入了多个 API 网关。
我们已经在微服务架构中创建了 API 网关,而且已经说过,来自客户端的所有同步请求都通过 API 网关进入内部微服务。
但是,如果客户端请求需要访问多个内部微服务怎么办?我们如何处理内部微服务之间的通信?
在设计微服务应用程序时,我们应该注意后端内部微服务之间的通信方式。最好的做法是尽可能地减少服务间通信。
然而,在某些情况下,由于客户的要求或所请求的操作需要访问几个内部服务,我们无法减少内部通信。
例如,对照上图考虑这样一种情况:用户想要结账并创建一个订单。
我们该如何满足这个请求?
这些调用把微服务耦合在了一起,在我们的例子里,微服务 Product 和 Pricing 就会相互依赖并耦合。如果其中一个微服务发生故障,它就不能向客户端返回数据,所以它没有任何容错性。如果微服务之间的依赖性和耦合性增加,就会产生很多问题,并缩小微服务架构的优势。
如果客户要对购物车进行结账,这将触发一系列的操作。因此,如果我们试图用请求 / 响应这种同步消息模式来执行这个下单用例,那就会像上面这个图一样。
可以看到,一个客户端的 http 请求会触发 6 个同步 http 请求。所以显然会增加延迟并对系统的性能、可扩展性和可用性产生负面影响。
如果我们有这样的用例,如果第 5 步或第 6 步失败了,或者中间的某些服务中断了怎么办?即使没有中断,某些服务也可能非常繁忙,无法及时响应,造成不可接受的高延迟。
那么,这类需求的解决方案是什么?
我们可以通过两种方法来解决这种问题:
为了尽量减少服务之间的通信,我们可以使用服务聚合模式。基本上,服务聚合设计模式是接收来自客户端或 API 网关的请求,然后分配给内部多个后端微服务,再将结果合并,并在一个响应结构中发给请求发起人。
通过实现服务聚合模式,可以减少客户端和微服务之间的通信量和通信开销。
在这一节中,我们将通过添加服务聚合模式 / 服务注册模式,来迭代我们的电子商务应用架构。
如上图所示,我们在电子商务应用的架构中应用了服务聚合模式 / 服务注册模式。
如果通信只是在少数几个微服务之间进行,那么同步通信就很好。但当涉及到多个微服务相互调用,并且要等待一些长时间的操作完成时,我们应该使用异步通信。
否则,微服务的相互依赖和耦合会导致瓶颈和严重的架构问题。
如果你有多个微服务需要彼此交互,而且,你希望这种交互没有任何依赖性或是松耦合的,那么我们就应该在微服务架构中使用基于异步消息的通信。
因为基于异步消息的通信有赖于事件,所以我们称这种通信为事件驱动的通信。
发布 - 订阅是一种消息传递模式,它的消息发送者被称为发布者,而特定的接收者被称为订阅者。
因此,发布者不是直接将消息发送给订阅者,而是将发布的消息进行归类,并送入消息代理系统,但并不知道有哪些订阅者。同样地,订阅者只接收感兴趣的消息,而不知道哪些发布者在发布消息。
在这一节中,我们将添加发布 / 订阅消息代理,提供微服务异步通信设计,完成电子商务应用的架构迭代。
可以看到,我们应用了发布 / 订阅消息代理。
如果要适配技术栈,那么我们首先会考虑选用什么发布 / 订阅消息代理。下面这两个都是不错的选项:
在单体架构中,查询不同的实体非常方便,因为是由单个数据库来管理数据,这会很简单。多表关联查询也很简单。对数据的任何修改都会一起更新,或是一起回滚。关系型数据库具有严格的一致性,有 ACID 事务保证,所以管理和查询数据都很容易。
但在微服务架构中,当我们使用“混合持久化”时,这意味着每个微服务都有不同的数据库,包括关系型数据库和 NoSQL 数据库,我们应该制定一个策略,在进行用户交互时管理好这些数据。
因此,这意味着我们在处理微服务之间的数据交互时有几种模式和做法,我们将在本节中学习这些模式和原则。
微服务是独立的,只执行特定的功能要求。在我们的电子商务应用中,我们有产品、购物车、折扣、订单等微服务,它们需要彼此交互来满足客户的要求。这意味着它们需要频繁地交互。而这些交互大多是查询每个服务的数据以进行聚合或执行逻辑。
CQRS 是跨微服务查询的重要模式之一。我们可以使用 CQRS 设计模式,以避免复杂的查询,摆脱低效的连接。CQRS 是命令和查询责任隔离的意思。本质上,这种模式实现了数据库读取和更新操作的分离。
为了隔离命令和查询,最好的做法是用 2 个数据库物理地分离读和写数据库。此时,如果我们的应用程序是读密集型的,也就是读比写多,我们就可以通过定义自定义数据模式来优化查询。
物化视图模式是实现读数据库的一个很好的例子。因为通过这种方式,我们可以用预定义的细粒度数据来避免复杂的连接和映射,进行查询操作。
通过这种隔离,对于读数据库和写数据库,我们甚至可以使用不同的数据库类型,如使用 NoSQL 文档数据库进行读取,使用关系数据库进行 CRUD 操作。
我们已经学习了 CQRS 模式,该模式主要是与事件源模式一起使用。当搭配使用 CQRS 与事件源模式时,主要的理念是将事件存储到写数据库中,这将是作为真相来源的事件数据库。
之后,CQRS 设计模式的读数据库通过非规范化表提供数据的物化视图。当然,这个物化视图的读数据库消费了来自写数据库的事件,并将它们转换为非规范化的视图。
事件源模式改变了数据保存至数据库的操作。它不是将数据的最新状态保存到数据库,而是将所有事件按数据事件发生的顺序保存到数据库。这个事件数据库称为事件存储。
它不更新数据记录的状态,而是将每个修改追加到一个事件的顺序表中。因此,事件存储成为数据的真实来源。之后,这些事件存储通过物化视图转换为读数据库。这种转换操作可以通过发布 / 订阅模式来处理,实现方式是用消息代理系统发布事件。
我们将在电子商务应用的架构中应用 CQRS、事件源、最终一致性、物化视图。
因此,当用户创建或更新订单时,我将使用关系型写数据库,而当用户查询订单或订单历史时,我将使用 NoSQL 读数据库,并在通过发布 / 订阅模式使用消息代理系统同步两个数据库时使它们保持一致。
现在我们可以考虑这些数据库的技术栈了,我打算用 SQL Server 作为关系型写数据库,用 Cassandra 作为 NoSQL 读数据库。当然,我们将使用 Kafka 的发布 / 订阅主题交换来同步这两个数据库。可以看到,我们已经完成了微服务数据库模式的设计。现在,让我们深入了解下微服务中的事件驱动架构。
本质上,事件驱动的微服务架构是指通过事件消息传递实现微服务之间的通信。在微服务异步通信那一节,我们已经从发布 / 订阅模式和 Kafka 消息代理系统中了解了这种方式。
我们说,通过事件驱动架构,我们可以实现异步行为和松耦合的结构。例如,服务不是在需要数据时发送请求,而是通过事件来消费它们。这可以提高性能。
事件驱动的微服务架构也有很大的创新,比如使用实时消息平台、流处理、事件中心、实时处理、批处理、数据智能等等。
因此,我们可以使这种事件驱动的方法更加通用,并通过实时事件处理特性来演进架构。
在这个新的事件驱动的微服务架构中,所有通信都是通过事件中心(Event-Hub)进行的。可以认为,事件中心是一个可以完成实时处理的大型事件存储数据库。
我们将利用事件驱动的微服务架构来设计我们的电子商务应用。
现在,让我们确定下这个架构的技术栈。当然,我们应该选择 Apache Kafka 作为事件中心,将 Apache Spark 作为实时和准实时流处理应用,对数据流进行转换或回应。
如你所见,现在我们用事件驱动的微服务架构实现了反应式设计。
现在,我们可以问同样的问题:我们的设计可以满足多大的并发请求?
这个最新的事件驱动的微服务架构(通过容器和编排器来部署),可以在低延迟的情况下满足目标并发请求。
这个架构是完全松耦合的,并且做了高可扩展性和高可用性设计。
如你所见,我们完成了电子商务微服务架构的设计,这个过程涉及了所有的设计原则和模式。通过学习,你已经了解如何在设计中使用这些设计模式了,现在你可以设计自己的架构了。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8