在单个进程上运行的单片应用程序中,组件使用语言级方法或函数调用彼此调用。如果使用代码创建对象(例如,new ClassName()),则可以强耦合这些对象;如果使用依赖注入,则可以通过引用抽象而不是具体的对象实例,以分离的方式调用这些对象。不管怎样,对象都在同一进程中运行。当从单一应用程序转变为基于微服务的应用程序时,最大的挑战在于改变通信机制。从进程内方法调用到服务的RPC调用的直接转换将导致在分布式环境中性能不佳的聊天和不高效的通信。正确设计分布式系统的挑战是众所周知的,甚至还有一个被称为分布式计算谬误的经典,它列出了开发人员在从单一设计转向分布式设计时经常做出的假设。
没有一个解决方案,只有几个。一种解决方案是尽可能地隔离业务微服务。然后在内部微服务之间使用异步通信,并用粗粒度通信替换对象之间的进程内通信中的典型细粒度通信。您可以通过对调用进行分组并将聚合多个内部调用结果的数据返回给客户端来完成此操作。
基于微服务的应用程序是在多个进程或服务上运行的分布式系统,通常甚至跨多个服务器或主机运行。每个服务实例通常是一个流程。因此,服务必须使用进程间通信协议(如HTTP、AMQP)或二进制协议(如TCP)进行交互,具体取决于每个服务的性质。
微服务社区提倡“智能端点和哑管道”的理念这一口号鼓励设计尽可能在微服务之间分离,并在单个微服务中尽可能具有凝聚力。如前所述,每个微服务都拥有自己的数据和域逻辑。但是,组成端到端应用程序的微服务通常只是通过使用REST通信而不是复杂的协议(如WS-*和灵活的事件驱动通信)进行编排,而不是使用集中的业务流程编排器。
这两种常用的协议是HTTP请求/响应和资源api(在大多数情况下查询时),以及在跨多个微服务通信更新时的轻量级异步消息传递。以下各节将更详细地解释这些问题。
通信类型
客户机和服务可以通过多种不同类型的通信进行通信,每种通信都针对不同的场景和目标。最初,这些类型的通信可以分为两个轴。
第一个轴定义协议是同步的还是异步的:
- 同步协议。HTTP是一种同步协议。客户端发送请求并等待服务的响应。这与客户端代码执行无关,客户端代码执行可以是同步的(线程被阻塞)或异步的(线程不被阻塞,响应最终将到达回调)。这里重要的一点是,协议(HTTP/HTTPS)是同步的,客户端代码只有在接收到HTTP服务器响应时才能继续其任务。
- 异步协议。其他协议如AMQP(许多操作系统和云环境支持的协议)使用异步消息。客户端代码或消息发送者通常不会等待响应。它只是在向RabbitMQ队列或任何其他消息代理发送消息时发送消息。
第二轴定义通信是否有一个或多个接收器:
- 单接收器。每个请求必须由一个接收者或服务处理。这种通信的一个例子是命令模式。
- 多个接收器。每个请求都可以由零到多个接收器处理。这种类型的通信必须是异步的。例如,在事件驱动架构等模式中使用的发布/订阅机制。当通过事件在多个微服务之间传播数据更新时,这基于事件总线接口或消息代理;通常通过服务总线或类似的工件(如Azure服务总线)通过使用主题和订阅来实现。
基于微服务的应用程序通常会使用这些通信样式的组合。最常见的类型是在调用常规Web API HTTP服务时使用HTTP/HTTPS之类的同步协议进行单接收器通信。微服务通常也使用消息传递协议在微服务之间进行异步通信。
这些轴是很好知道,所以你有可能的沟通机制,但他们不是重要的关注时,建设微服务。在集成微服务时,客户端线程执行的异步性和所选协议的异步性都不是重点。重要的是能够异步集成您的微服务,同时保持微服务的独立性,如下一节所述。
异步微服务集成增强了微服务的自主性
如前所述,构建基于微服务的应用程序时,重要的一点是集成微服务的方式。理想情况下,您应该尽量减少内部微服务之间的通信。微服务之间的通信越少越好。但在很多情况下,您必须以某种方式集成微服务。当您需要这样做时,这里的关键规则是微服务之间的通信应该是异步的。这并不意味着您必须使用特定的协议(例如,异步消息传递与同步HTTP)。这只是意味着微服务之间的通信应该只通过异步传播数据来完成,但不要依赖于其他内部微服务作为初始服务HTTP请求/响应操作的一部分。
如果可能,永远不要依赖多个微服务之间的同步通信(请求/响应),即使是查询。每个微服务的目标都是自治的,并可供客户机消费者使用,即使作为端到端应用程序一部分的其他服务已关闭或不正常。如果您认为需要从一个微服务调用其他微服务(如执行数据查询的HTTP请求)才能向客户端应用程序提供响应,那么您的体系结构在某些微服务失败时将无法恢复。
此外,如图4-15的第一部分所示,在微服务之间具有HTTP依赖关系(如使用HTTP请求链创建长请求/响应周期时),不仅使微服务不具有自治性,而且一旦该链中的某个服务性能不好,它们的性能就会受到影响。
在微服务(如查询请求)之间添加的同步依赖项越多,客户端应用程序的总体响应时间就越差。
图15 微服务间通信中的反模式和模式
如上图所示,在同步通信中,在为客户机请求提供服务的同时,在微服务之间创建请求的“链”。这是一种反模式。在异步通信中,微服务使用异步消息或http轮询来与其他微服务通信,但客户机请求会立即得到服务。
如果您的微服务需要在另一个微服务中引发附加操作,请尽可能不要同步执行该操作,并将其作为原始微服务请求和回复操作的一部分。相反,要异步进行(使用异步消息传递或集成事件、队列等)。但是,尽可能不要作为原始同步请求和应答操作的一部分同步调用操作。
最后(这是构建微服务时出现的大多数问题),如果初始微服务需要最初由其他微服务拥有的数据,不要依赖于对该数据进行同步请求。相反,通过使用最终一致性(通常是通过使用集成事件,如后面几节所述)将数据(仅需要属性)复制或传播到初始服务的数据库中。
正如前面在识别每个微服务部分的域模型边界中所指出的,在多个微服务之间复制某些数据并不是一个错误的设计相反,这样做时,您可以将数据转换为该附加域或有界上下文的特定语言或术语。例如,在eShopOnContainers应用程序中,有一个名为identity api的微服务,它负责用户的大部分数据,实体名为user。但是,当需要在Ordering microservice中存储有关用户的数据时,可以将其存储为名为Buyer的不同实体。买方实体与原始用户实体共享相同的标识,但它可能只有订购域所需的少数属性,而不是整个用户配置文件。
您可以使用任何协议在微服务之间异步通信和传播数据,以便最终保持一致性。如前所述,可以使用事件总线或消息代理来使用集成事件,甚至可以通过轮询其他服务来使用HTTP。没关系。重要的规则是不要在微服务之间创建同步依赖关系。
以下各节介绍了在基于微服务的应用程序中可以考虑使用的多种通信样式。
通信风格
根据要使用的通信类型,可以使用许多协议和选项进行通信。如果使用的是基于同步请求/响应的通信机制,那么HTTP和REST方法等协议是最常见的,尤其是在Docker主机或微服务集群之外发布服务时。如果您在服务之间进行内部通信(在Docker主机或微服务集群内),您可能还需要使用二进制格式的通信机制(如使用TCP和二进制格式的WCF)。或者,可以使用异步的、基于消息的通信机制,如AMQP。
还有多种消息格式,如JSON或XML,甚至是二进制格式,这样可以更高效。如果您选择的二进制格式不是标准格式,那么使用该格式公开发布您的服务可能不是一个好主意。您可以使用非标准格式在微服务之间进行内部通信。在Docker主机或微服务群集(例如Docker orchestrators)内的微服务之间通信时,或者在与微服务对话的专用客户端应用程序之间通信时,可以这样做。
基于HTTP和REST的请求/响应通信
当客户机使用请求/响应通信时,它向服务发送一个请求,然后服务处理该请求并发回一个响应。请求/响应通信特别适合于从客户端应用程序查询实时UI(实时用户界面)的数据。因此,在微服务架构中,您可能会对大多数查询使用这种通信机制,如图4-16所示。
图16 使用HTTP请求/响应通信(同步或异步)
当客户机使用请求/响应通信时,它假设响应将在短时间内到达,通常不到一秒,或最多几秒。对于延迟响应,您需要实现基于消息模式和消息技术的异步通信,这是我们在下一节中解释的另一种方法。
请求/响应通信的流行体系结构样式是REST。这种方法基于HTTP协议,并与之紧密耦合,包括GET、POST和PUT等HTTP动词。REST是创建服务时最常用的架构通信方法。在开发ASP.NET核心Web API服务时,可以实现REST服务。
当使用HTTP REST服务作为接口定义语言时,还有额外的价值。例如,如果使用夸张的元数据来描述服务API,则可以使用生成客户端存根的工具来直接发现和使用服务。
额外资源
- Martin Fowler. Richardson Maturity Model A description of the REST model.
https://martinfowler.com/articles/richardsonMaturityModel.html - Swagger The official site.
https://swagger.io/ - 基于HTTP的推送与实时通信
- 另一种可能性(通常用于与REST不同的目的)是与更高级别的框架(如ASP.NET signaler)和协议(如WebSockets)进行实时的一对多通信。
- 如图4-17所示,实时HTTP通信意味着您可以让服务器代码在数据可用时将内容推送到连接的客户端,而不是让服务器等待客户端请求新数据。
- 图17 一对一实时异步消息通信
- signaler是实现从后端服务器将内容推送到客户端的实时通信的好方法。由于通信是实时的,客户端应用程序几乎可以立即显示变化。这通常由WebSockets等协议处理,使用许多WebSockets连接(每个客户端一个)。一个典型的例子是,当一个服务同时向许多客户端web应用程序传递一个体育游戏的分数变化时。