微服务体系结构中的通信

小窍门

此内容摘自电子书《适用于容器化 .NET 应用程序的 .NET 微服务体系结构》,可以在 .NET Docs 上获取,也可以下载免费的 PDF 以供离线阅读。

适用于容器化 .NET 应用程序的 .NET 微服务体系结构电子书封面缩略图。

在单个进程上运行的单体应用程序中,组件使用语言级方法或函数调用相互调用。 如果使用代码创建对象(例如 new ClassName()),则可以紧密耦合,或者如果正在通过引用抽象而不是具体对象实例来使用依赖项,则可以在耦合方法中调用。 无论哪种方式,对象都在同一进程中运行。 从单一应用程序更改为基于微服务的应用程序时,最大的挑战在于更改通信机制。 将进程内的方法调用直接转换为对服务的 RPC 调用将在分布式环境中导致效果不佳且效率低下的通信。 正确设计分布式系统的挑战已经众所周知,甚至有一个被称为分布式计算谬误的经典,其中列出了开发人员在从单一设计转向分布式设计时经常犯的假设错误。

没有一个解决方案,但有几个解决方案。 一种解决方案涉及尽可能多地隔离业务微服务。 然后,在内部微服务之间使用异步通信,并将在对象之间的进程内通信中典型的细粒度通信替换为粗粒度通信。 可以通过对调用进行分组,并将聚合多个内部调用结果的数据返回给客户端来实现此功能。

基于微服务的应用程序是在多个进程或服务上运行的分布式系统,通常甚至跨多个服务器或主机运行。 每个服务实例通常是一个进程。 因此,服务必须使用进程间通信协议(如 HTTP、AMQP 或 TCP 等二进制协议)进行交互,具体取决于每个服务的性质。

微服务社区推广了“智能终结点和愚蠢管道”的理念。 此口号鼓励在微服务之间尽可能分离的设计,并在单个微服务中尽可能有凝聚力。 如前所述,每个微服务都有自己的数据和自己的域逻辑。 但是,组成端到端应用程序的微服务通常通过 REST 通信(而不是像 WS-* 这样的复杂协议)简单编排,并采用灵活的事件驱动通信来替代集中的业务流程协调器。

这两种常用协议是包含资源 API 的 HTTP 请求/响应(查询大部分时),在跨多个微服务之间通信更新时进行轻型异步消息传送。 以下各节将更详细地介绍这些内容。

通信类型

客户端和服务可以通过多种不同类型的通信进行通信,每个通信针对不同的方案和目标。 最初,这些类型的通信可以分为两个轴。

第一个轴定义协议是同步还是异步的:

  • 同步协议。 HTTP 是同步协议。 客户端发送请求并等待来自服务的响应。 客户端代码的执行与它是同步的(线程被阻塞)还是异步的(线程未被阻塞,响应最终会到达回调)无关。 此处的要点是协议(HTTP/HTTPS)是同步的,客户端代码只能在收到 HTTP 服务器响应时继续其任务。

  • 异步协议。 其他协议(如 AMQP,该协议为许多操作系统和云环境所支持)使用异步消息。 客户端代码或消息发送方通常不会等待响应。 它只是在发送消息到 RabbitMQ 队列或任何其他消息代理时才发送消息。

第二个轴定义通信是否具有单个接收方或多个接收器:

  • 单个接收器。 每个请求必须由一个接收方或服务处理。 此通信的示例是 命令模式

  • 多个接收器。 每个请求都可以由零到多个接收方进行处理。 这种类型的通信必须是异步的。 例如,在事件驱动体系结构等模式中使用的发布/订阅机制。 当通过事件传播多个微服务之间的数据更新时,这依赖于事件总线接口或消息代理;通常通过服务总线或类似工具(如Azure 服务总线),利用主题和订阅来实现。

基于微服务的应用程序通常使用这些通信样式的组合。 最常见的类型是在调用常规 Web API HTTP 服务时,使用如HTTP/HTTPS之类的同步协议进行单接收方通信。 微服务通常还会使用消息传送协议在微服务之间进行异步通信。

这些轴是众所周知的,因此你可以清楚地了解可能的通信机制,但在构建微服务时,它们不是重要问题。 集成微服务时,客户端线程执行的异步性质和所选协议的异步性质都不是要点。 重要的是能够异步集成微服务,同时保持微服务的独立性,如以下部分所述。

异步微服务集成强制实施微服务的自主性

如前所述,构建基于微服务的应用程序时的要点是集成微服务的方式。 理想情况下,应尽量减少内部微服务之间的通信。 微服务之间的通信越少越好。 但在许多情况下,必须以某种方式集成微服务。 需要执行此作时,此处的关键规则是微服务之间的通信应该是异步的。 这并不意味着必须使用特定协议(例如,异步消息传送与同步 HTTP)。 它只是意味着微服务之间的通信只能通过异步传播数据来完成,但尝试不依赖于其他内部微服务作为初始服务的 HTTP 请求/响应作的一部分。

如果可能,从不依赖于多个微服务之间的同步通信(请求/响应),甚至不依赖于查询。 每个微服务的目标是自治且可供客户端使用者使用,即使属于端到端应用程序的其他服务已关闭或运行不正常也是如此。 如果认为需要从一个微服务调用其他微服务(例如对数据查询执行 HTTP 请求),以便提供对客户端应用程序的响应,则某些微服务发生故障时,体系结构将无法复原。

此外,在微服务之间具有 HTTP 依赖项(例如,使用 HTTP 请求链创建长请求/响应周期时),如图 4-15 的第一部分所示,不仅使微服务不自主,而且当该链中的某个服务性能不佳时,它们的性能就会受到影响。

在微服务(如查询请求)之间添加同步依赖项越多,客户端应用的总体响应时间就越差。

图表展示微服务之间三种通信类型。

图 4-15. 微服务之间通信中的反模式和模式

如上图所示,在同步通信中,在为客户端请求提供服务时,微服务之间会创建请求的“链”。 这是一种反模式。 在异步通信微服务中,使用异步消息或 http 轮询与其他微服务通信,而客户端请求会被立即处理。

如果微服务需要在另一个微服务中引发额外的动作,请不要同步执行该动作,并且不要将其作为原始微服务请求和回复操作的一部分。 可以采用异步的方式进行此操作(使用异步消息传递或集成事件、队列等)。 但是,尽量不要将操作作为原始同步请求和答复的一部分进行同步调用。

最后(这也是在生成微服务时大多数问题出现的地方),如果最初的微服务需要其他微服务最初所拥有的数据,则不应该依赖于向这些数据发送同步请求。 相反,可以通过使用最终一致性(通常通过使用集成事件,如后续部分所述),将这些数据(仅需要的属性)复制或传播到初始服务的数据库中。

如前面在 “标识每个微服务”部分的“标识域模型边界 ”中所述,跨多个微服务复制某些数据并不是一种不正确的设计,相反,在执行此作时,可以将数据转换为该附加域或边界上下文的特定语言或术语。 例如,在 eShopOnContainers 应用程序中,您有一个名为 identity-api 的微服务,它负责大多数用户的数据,并包含一个名为 User 的实体。 但是,如果需要在微服务中 Ordering 存储有关用户的数据,请将其存储为名为 Buyer另一个实体。 该 Buyer 实体与原始 User 实体具有相同的身份,但它可能仅有 Ordering 域所需的几个属性,而非整个用户配置文件。

可以使用任何协议在微服务之间异步通信和传播数据,以实现最终一致性。 如前所述,可以通过事件总线或消息代理使用集成事件,或者甚至可以通过轮询其他服务使用 HTTP。 这并不重要。 重要的规则是不要在微服务之间创建同步依赖项。

以下部分介绍了可以在基于微服务的应用程序中使用多种通信样式。

通信样式

有许多协议和选项可用于通信,具体取决于要使用的通信类型。 如果使用基于同步请求/响应的通信机制,则 HTTP 和 REST 方法等协议最为常见,尤其是在 Docker 主机或微服务群集外部发布服务时。 如果要在内部(在 Docker 主机或微服务群集内)之间通信,可能还需要使用二进制格式通信机制(例如使用 TCP 和二进制格式的 WCF)。 或者,可以使用异步的基于消息的通信机制,例如 AMQP。

还有多种消息格式,如 JSON 或 XML,甚至二进制格式,可以更高效。 如果所选的二进制格式不是标准格式,则使用该格式公开发布服务可能不是一个好主意。 可以使用非标准格式在微服务之间进行内部通信。 在 Docker 主机或微服务集群内(例如 Docker 编排工具),或与微服务通信的专有客户端应用程序之间进行通信时,您可能会执行此操作。

使用 HTTP 和 REST 进行请求/响应通信

当客户端使用请求/响应通信时,它会向服务发送请求,然后服务处理请求并发送回响应。 请求/响应通信特别适用于从客户端应用查询实时 UI(实时用户界面)的数据。 因此,在微服务体系结构中,可能会将此通信机制用于大多数查询,如图 4-16 所示。

显示实时查询和更新的请求/响应通信的关系图。

图 4-16. 使用 HTTP 请求/响应通信(同步或异步)

当客户端使用请求/响应通信时,它假定响应将在短时间内到达,通常少于一秒或几秒。 对于延迟响应,需要基于 消息传送模式消息传送技术实现异步通信,这是下一部分介绍的一种不同的方法。

请求/响应通信的常用体系结构样式是 REST。 此方法基于 HTTP 协议并 紧密耦合,采用 GET、POST 和 PUT 等 HTTP 谓词。 REST 是创建服务时最常用的体系结构通信方法。 开发 ASP.NET Core Web API 服务时,可以实现 REST 服务。

将 HTTP REST 服务用作接口定义语言时,还有其他价值。 例如,如果使用 Swagger 元数据 来描述服务 API,则可以使用生成可直接发现和使用服务的客户端存根的工具。

其他资源

基于 HTTP 的推送和实时通信

另一种可能性(通常不同于 REST),是与更高级别的框架(如 ASP.NET SignalR )和 WebSocket 等协议的实时和一对多通信。

如图 4-17 所示,实时 HTTP 通信意味着可以在数据可用时让服务器代码将内容推送到连接的客户端,而不是让服务器等待客户端请求新数据。

显示基于 SignalR 的推送和实时通信的关系图。

图 4-17. 一对多实时异步消息通信

SignalR 是实现从后端服务器将内容推送到客户端的实时通信的好方法。 由于通信是实时的,客户端应用几乎即时显示更改。 这通常由 WebSocket 之类的协议使用多个 WebSocket 连接(每个客户端一个)处理。 一个典型的示例是,当服务同时将运动游戏分数的更改传达给许多客户端 Web 应用时。