译者 | 李睿
审校 | 重楼
微服务架构已经成为现代应用程序开发的实际选择。虽然它解决了许多问题,但并不是灵丹妙药。像所有的软件一样,它也面临一些需要解决的独有挑战。这就需要了解微服务中的常见设计模式,并使用可重用的解决方案来解决这些挑战。
在深入研究设计模式之前,了解构建微服务架构的核心原则非常重要:
图1微服务架构核心实践
应用这些原则往往会带来一些挑战和问题。本文将提供关键微服务模式的细分、它们解决的常见问题以及它们提供的解决方案。
本文可以作为关键微服务模式和系统设计策略的参考,以有效地构建微服务架构。
分解设计模式
模式1:按业务能力分解
问题
微服务就是让服务松散耦合,从而应用单一责任原则。然而,将应用程序分解为更小的部分必须符合逻辑。那么,如何将应用程序分解为更小的服务?
解决方案
其中一种策略是按业务能力进行分解。业务能力是企业为了创造价值所做的事情,给定业务的能力取决于业务类型。例如,保险公司的能力通常包括销售、营销、承保、索赔处理、计费、合规等。每种业务能力都可以被视为一种服务——只不过它是面向业务的,而不是技术的。
模式2:按子域分解
问题
使用业务能力分解应用程序可能是一个良好的开端,但是可能遇到所谓的“上帝类” (God Classes)问题,它们不容易分解。这些类在多个服务中是通用的。例如,订单类将用于订单管理、订单接收、订单交付等。那么,如何分解它们?
解决方案
针对“上帝类”问题,领域驱动设计(DDD)提供了解决方案。它使用子域和有界场景概念来解决这个问题。领域驱动设计(DDD)将为企业创建的整个域模型分解为子域。每个子域都有一个模型,该模型的范围将被称为有界场景。每个微服务都将围绕有界场景进行开发。
注:识别子域并非易事,这需要对业务有所了解。就像识别业务能力一样,子域是通过分析业务及其组织结构并确定不同的专业领域来识别的。
模式3:扼杀者模式
问题
到目前为止所讨论的设计模式都是针对绿地(Greenfield)分解应用程序,但是80%的工作都是针对棕地(Brownfield)应用程序,也就是大型的单片应用程序。将之前讨论的所有设计模式直接应用于棕地应用程序将会面临巨大的挑战——在保持其运行的同时,尝试将其分解成更小的组成部分是一项极为艰巨的任务。
解决方案
扼杀者模式可以发挥重要作用。扼杀者模式基于藤蔓缠绕并扼杀树木的类比。这一解决方案适用于来回调用的Web应用程序,并且对于每个URI调用,可以将服务分解为不同的域并作为独立的服务托管。这个想法是每次处理一个域。就在同一个URI空间中创建了两个独立的应用程序。新重构的应用程序会“扼杀”或取代原来的应用程序,直到最终可以关闭单片应用程序。
集成模式
模式4:API网关模式
问题
当应用程序被分解为更小的微服务时,有几个问题需要解决:
1.如何调用抽象生产者信息的多个微服务。
2.在不同的渠道(例如台式机电脑、笔记本电脑和平板电脑)上,应用程序需要不同的数据来响应相同的后端服务,因为用户界面(UI)可能不同。
3.不同的消费者可能需要来自可重用微服务的不同格式的响应。谁将进行数据转换或字段操作?
4.如何处理不同类型的协议——其中一些可能不被生产者微服务支持。
解决方案
API网关可以帮助解决微服务实现过程中出现的许多问题,但不限于上述问题:
1.API网关是任何微服务调用的单一入口点。
2.它可以作为代理服务,将请求路由到相关的微服务,抽象生产者的详细信息。
3.它可以向多个服务发出请求,并将结果聚合并发送回消费者。
4.并没有一个万能的API能够解决所有消费者的需求,这个解决方案可以为每种特定类型的客户机创建细粒度的API。
5.它还可以将协议请求(例如AMQP)转换为另一个协议(例如HTTP),反之亦然,以便生产者和消费者可以处理它。
6.它还可以减轻微服务的身份验证/授权责任。
模式5:聚合器模式
问题
如上所述,在解决API网关模式中的聚合数据问题时通常面临着挑战。当将业务功能分解为几个较小的逻辑代码片段时,有必要考虑如何协作每个服务返回的数据。这个责任不能留给消费者,因为消费者可能需要理解生产者应用程序的内部实现。
解决方案
聚合器模式有助于解决这个问题。它讨论了们如何聚合来自不同服务的数据,然后将最终响应发送给消费者。这可以通过两种方式实现:
1.复合微服务将调用所有所需的微服务,聚合数据,并在发送回之前转换数据。
2.API网关还可以将请求划分为多个微服务,并在将其发送给消费者之前聚合数据。
如果要应用任何业务逻辑,建议选择复合微服务。否则,API网关为已建立的解决方案。
模式6:客户端用户界面组合
问题
当通过分解业务功能/子域来开发服务时,负责用户体验的服务必须从几个微服务中提取数据。在单片架构中,过去只有一个从用户界面(UI)到后端服务的调用来检索所有数据和刷新/提交用户界面(UI)页面。然而,现在情况不同了,所以需要了解如何做到这一点。
解决方案
对于微服务,用户界面(UI)必须被设计成包含屏幕/页面的多个部分/区域的框架。每个部分将调用一个单独的后端微服务来提取数据。这被称为组合特定于服务的用户界面(UI)组件。像AngularJS和ReactJS这样的框架可以很容易地做到这一点。这些屏幕被称为单页应用程序(SPA)。这使得应用程序可以刷新屏幕的特定区域,而不是整个页面。
数据库模式
模式7:每个服务使用的数据库
问题
开发团队经常面临如何为微服务定义数据库架构的挑战。以下是必须解决的问题:
1.服务必须松散耦合。它们可以独立开发、部署和扩展。
2.业务事务可以强制执行跨多个服务的不变量。
3.一些业务事务需要查询由多个服务拥有的数据。
4.数据库有时必须复制和分片才能扩展。
5.不同的服务有不同的数据存储需求。
解决方案
为了解决上述问题,必须为每个微服务设计一个数据库。它必须仅对该服务私有,并且只能由微服务API访问。其他服务无法直接访问它。
例如,对于关系数据库,可以使用“每个服务私有表”、“每个服务模式”或“每个服务数据库服务器”。每个微服务都应该有一个单独的数据库ID,这样就可以提供单独的访问权限,从而建立一个屏障,防止它使用其他服务表。
图2 每个服务的数据库架构
模式8:每个服务共享数据库
问题
之前讨论过,每个服务使用一个数据库是微服务架构的理想状态,但这只有在应用是全新开发的,并且采用领域驱动设计(DDD)时才能实现。如果应用程序是一个单体,并且试图分解成微服务,那么非规范化就不那么容易了。那么,在这种情况下,最合适的架构是什么?
解决方案
每个服务共享数据库并不理想,但这是针对以上场景的有效解决方案。大多数人认为这是微服务的反模式,但对于“棕地”应用程序,这是可以将应用程序分解为更小的逻辑组件的一个很好的起点。
在这种模式下,一个数据库可以与多个微服务对齐,但最多只能对齐2~3个;否则,扩展、自主和独立将难以执行:
图3共享数据库架构
模式9:命令查询职责分离(CQRS)
问题
一旦实现了每个服务的数据库,就需要查询,这需要来自多个服务的联合数据——这是不可能的。那么,如何在微服务架构中实现查询呢?
解决方案
命令查询职责分离(CQRS)建议将应用程序分成两部分——命令端和查询端。命令端处理创建、更新和删除请求。查询端通过使用物化视图来处理查询组件。事件溯源设计模式通常与它一起用于为任何数据更改创建事件。因此,物化视图通过订阅事件流来保持最新状态。
模式10:Saga模式
问题
当每个服务都有自己的数据库,并且业务事务跨越多个服务时,如何确保跨服务的数据一致性?例如,对于客户有信用额度的电子商务应用程序,应用程序必须确保新订单不会超过客户的信用额度。由于订单和客户在不同的数据库中,应用程序不能简单地使用本地ACID事务。
解决方案
Saga代表了一个由多个子请求组成的高级业务流程,每个子请求更新单个服务中的数据。每个请求都有一个补偿请求,当请求失败时执行。它可以通过两种方式实现:
1.编排——当没有中央协调机制时,每个服务都会产生并监听其他服务的事件,并决定是否需要采取行动。
2.编排——编排者(对象)负责一个Saga的决策和排序业务逻辑。
可观察性模式
接下来深入了解微服务模式的可观察性。以下是一个示例微服务架构图,供所有可观察性主题参考。
图4微服务可观察性图
模式11:日志聚合
问题
考虑一个用例,其中应用程序由在多台机器上运行的多个服务实例组成。请求通常跨越多个服务实例。每个服务实例都会生成一个标准化格式的日志文件。那么,如何通过特定请求的日志来理解应用程序的行为?
解决方案
需要一个集中的日志服务来聚合来自每个服务实例的日志。用户可以对日志进行查询和分析。他们还可以配置警报,当日志中出现特定消息时触发这些警报。例如,云平台即服务(PCF)确实有Loggeregator,它从PCF平台的每个组件(路由器、控制器、Diego等)以及应用程序收集日志。AWS Cloud Watch也提供了类似的功能。
模式12:性能指标
问题
当服务组合由于微服务架构而增加时,密切关注事务变得至关重要,以便在出现问题时可以监控模式并发送警报。那么,应该如何收集指标来监控应用程序性能?
解决方案
需要一个指标服务来收集有关单个操作的统计数据。该服务应该聚合应用程序服务的指标,以提供报告和警报功能。指标聚合有两种模型:
- Push(推送)模型将指标推送到指标服务,例如New Relic、AppDynamics等。
- Pull(拉取)模型从指标服务中提取指标,例如Prometheus。
模式13:分布式跟踪
问题
在微服务架构中,请求通常跨越多个服务。每个服务通过跨多个服务执行一个或多个操作来处理请求。那么,如何从端到端跟踪请求以解决问题呢?
解决方案
需要这样一项服务:
- 为每个外部请求分配一个唯一的外部请求ID。
- 将外部请求ID传递给所有服务。
- 在所有日志消息中包含外部请求ID。
- 记录在集中服务中处理外部请求时执行的请求和操作的信息(例如,开始时间,结束时间等)。
Spring Cloud sluth和Zipkin server是一个常见的实现示例。
模式14:健康检查
问题
当实现微服务架构时,有可能出现服务启动但无法处理事务的情况。在这种情况下,如何确保请求不会转到那些失败的实例?可以通过负载平衡模式实现来解决这个问题。
解决方案
每个服务都需要有一个端点,可用于检查应用程序的运行状况,例如/health。这个API应该检查主机的状态、与其他服务/基础设施的连接以及任何特定的逻辑。
Spring Boot Actuator实现了/health端点,并且可以自定义实现。
横切关注点模式
模式15:外部化配置
问题
服务通常也会调用其他服务和数据库。对于dev、QA、UAT和/或prod等每个环境,端点URL或其他配置属性可能不同。任何这些属性的更改都可能需要重新构建和重新部署服务。那么如何避免因配置更改而修改代码?
解决方案
外部化配置(包括端点URL和凭据)将缓解问题。应用程序应该在启动时或运行时加载它们。
Spring Cloud配置服务器提供了将属性外部化到GitHub并将其作为环境属性加载的选项。这些可以在启动时由应用程序访问,也可以在不重新启动服务器的情况下刷新。
模式16:服务发现
问题
当微服务出现时,需要在调用服务方面解决一些问题:
1.通过容器技术,IP地址可以动态地分配给服务实例。在每次地址更改时,消费者服务都可能中断并需要人工更改。
2.消费者必须记住每个服务URL,并使其紧密耦合。
那么消费者或路由器如何知道所有可用的服务实例和位置呢?
解决方案
需要创建一个服务注册表,它将记录每个生产者服务的元数据。服务实例应在启动时向注册表注册,并在关闭时注销。因此,消费者或路由器应该查询注册表并找出服务的位置。
注册表还需要对生产者服务进行健康检查,以确保只有服务的工作实例可用并能够通过它使用。有两种类型的服务发现:客户端和服务器端。客户端发现的一个示例是Netflix Eureka,服务器端发现的一个示例是AWS ALB。
模式17:断路器(Circuit Breakers)
问题
服务通常会调用其他服务来检索数据,下游服务可能会宕机。这样做有两个问题:首先,请求将继续向宕机的服务发送,耗尽网络资源并降低性能。其次,用户体验将会很差且不可预测。那么,如何避免级联服务故障并从容地处理故障?
解决方案
消费者应该通过代理调用远程服务,该代理的行为方式类似于断路器。当连续故障的数量超过阈值时,断路器跳闸,并且在超时期间,所有调用远程服务的尝试都将立即失败。在超时之后,断路器允许有限数量的测试请求通过。如果这些请求成功,则断路器恢复正常操作。否则,如果再次失败,超时期将重新开始。
Netflix Hystrix是断路器模式的良好实现。它还有助于定义一个回退机制,可以在断路器跳闸时使用。这提供了更好的用户体验。
模式18:蓝-绿部署
问题
使用微服务架构,一个应用程序可以有许多微服务。如果停止所有服务,然后部署增强版本,那么停机时间可能会很长,并影响业务。此外,任何回滚都将是一场噩梦。那么,如何避免或减少部署期间服务的停机时间呢?
解决方案
可以实现蓝绿部署策略来减少或消除停机时间。它通过运行两个相同的生产环境(蓝色和绿色)来实现这一点。假设绿色是现有的活动实例,而蓝色是应用程序的新版本。在任何时候,只有一个环境是活动的,活动环境为所有生产流量服务。所有云平台都提供了实现蓝绿部署的选项。
结论
还有其他几个关键的微服务架构模式,例如sidecar模式、链式微服务、分支微服务、事件溯源设计模式、大使模式等等。随着微服务架构的不断演进和发展,这个清单将会持续扩展。
原文标题:Microservices Design Patterns: Essential Architecture and Design Guide,作者:Rajesh Bhojwani