博客 | NGINX

构建微服务: 微服务架构中的进程间通信

NGINX-F5-horiz-black-type-RGB 的一部分
克里斯·理查森缩略图
克里斯·理查森
2015 年 7 月 24 日发布

编辑– 本系列文章共七部分,现已完成:

  1. 微服务简介
  2. 构建微服务: 使用 API 网关
  3. 构建微服务: 微服务架构中的进程间通信(本文)
  4. 微服务架构中的服务发现
  5. 微服务的事件驱动数据管理
  6. 选择微服务部署策略
  7. 将整体式架构重构为微服务

您还可以下载完整的文章集,以及有关使用 NGINX Plus 实现微服务的信息,作为电子书 -微服务: 从设计到部署。 另外,请查看新的微服务解决方案页面

这是我们关于使用微服务架构构建应用的系列文章的第三篇。 第一篇文章介绍了微服务架构模式,并将其与单片架构模式进行了比较,并讨论了使用微服务的优点和缺点。 第二篇文章描述了应用的客户端如何通过称为API 网关的中介与微服务进行通信。 在本文中,我们将了解系统内的服务如何相互通信。 第四篇文章探讨了密切相关的服务发现问题。

介绍

在单片应用中,组件通过语言级方法或函数调用相互调用。 相比之下,基于微服务的应用是在多台机器上运行的分布式系统。 每个服务实例通常都是一个进程。 因此,如下图所示,服务必须使用进程间通信 (IPC) 机制进行交互。


在微服务应用中,服务需要进程间通信 (IPC) 机制(而整体应用程序中的模块可以调用例程)

稍后我们将研究具体的 IPC 技术,但首先让我们探讨各种设计问题。

互动风格

当为服务选择 IPC 机制时,首先考虑服务如何交互是很有用的。 客户⇔服务互动风格多种多样。 它们可以按照两个维度进行分类。 第一个维度是交互是一对一还是一对多:

  • 一对一——每个客户端请求仅由一个服务实例处理。
  • 一对多——每个请求由多个服务实例处理。

第二个维度是交互是同步的还是异步的:

  • 同步——客户端期望服务及时响应,甚至可能在等待时阻塞。
  • 异步——客户端在等待响应时不会阻塞,并且如果有响应,则不一定会立即发送。

下表显示了各种交互风格。

  一对一 一对多
同步 请求/响应  — 
异步 通知 发布/订阅
请求/异步响应 发布/异步响应

一对一交互有以下几种类型:

  • 请求/响应——客户端向服务发出请求并等待响应。 客户期望及时收到答复。 在基于线程的应用中,发出请求的线程甚至可能会在等待时阻塞。
  • 通知(又名单向请求)——客户端向服务发送请求,但未预期或发送回复。
  • 请求/异步响应——客户端向服务发送请求,服务以异步方式回复。 客户端在等待时不会阻塞,并且设计时假设响应可能暂时不会到达。

一对多交互有以下几种类型:

  • 发布/订阅——客户端发布通知消息,该消息由零个或多个感兴趣的服务使用。
  • 发布/异步响应——客户端发布请求消息,然后等待一定时间以获取感兴趣的服务的响应。

每项服务通常都会结合使用这些交互方式。 对于某些服务来说,单一的IPC机制就足够了。 其他服务可能需要使用多种 IPC 机制。 下图展示了当用户请求行程时出租车叫车应用中的服务如何交互。


这些服务使用通知、请求/响应和发布/订阅的组合。 例如,乘客的智能手机向行程管理服务发送通知以请求接载。 行程管理服务使用请求/响应调用乘客服务来验证乘客的帐户是否活跃。 然后,行程管理服务创建行程并使用发布/订阅功能通知其他服务(包括调度员,用于定位可用的司机)。

现在我们已经了解了交互风格,让我们看看如何定义 API。

定义 API

服务的 API 是服务与其客户端之间的契约。 无论您选择哪种 IPC 机制,使用某种接口定义语言 (IDL) 精确定义服务的 API 都非常重要。 甚至有很好的论据支持使用API 优先方法来定义服务。 您可以通过编写接口定义并与客户端开发人员一起审查来开始服务的开发。 只有在迭代 API 定义之后,您才能实现该服务。 提前进行这种设计可以增加您构建满足客户需求的服务的机会。

正如您将在本文后面看到的,API 定义的性质取决于您所使用的 IPC 机制。 如果您正在使用消息传递,则 API 由消息通道和消息类型组成。 如果您使用 HTTP,API 由 URL 和请求和响应格式组成。 稍后我们将更详细地描述一些 IDL。

不断发展的 API

服务的 API 总是会随着时间而改变。 在单片应用中,更改 API 并更新所有调用者通常很简单。 在基于微服务的应用中,这会变得更加困难,即使 API 的所有使用者都是同一应用中的其他服务。 通常,您无法强制所有客户端与服务同步升级。 此外,您可能会逐步部署服务的新版本,以便服务的新旧版本可以同时运行。 制定解决这些问题的策略非常重要。

如何处理 API 更改取决于更改的大小。 一些变化很小并且与以前的版本向后兼容。 例如,您可以向请求或响应添加属性。 设计客户端和服务以使其遵守稳健性原则是有意义的。 使用旧版 API 的客户端应继续使用新版本的服务。 服务为缺失的请求属性提供默认值,客户端忽略任何额外的响应属性。 使用 IPC 机制和消息传递格式非常重要,它们使您能够轻松改进 API。

但有时,您必须对 API 进行重大的、不兼容的更改。由于您无法强迫客户端立即升级,因此服务必须在一段时间内支持旧版本的 API。 如果您使用基于 HTTP 的机制(例如 REST),一种方法是将版本号嵌入 URL 中。 每个服务实例可能同时处理多个版本。 或者,您可以部署不同的实例,每个实例处理特定的版本。

处理部分故障

正如上一篇关于 API 网关<.htmla>的文章中提到的,分布式系统中始终存在着部分故障的风险。 由于客户端和服务是独立的进程,服务可能无法及时响应客户端的请求。 服务可能由于故障或维护而停止。 或者服务可能会超载并且对请求的响应极其缓慢。

例如,考虑该文章中的产品详细信息场景。 假设推荐服务没有响应。 客户端的简单实现可能会无限期地阻塞以等待响应。 这不仅会导致糟糕的用户体验,而且在许多应用中还会消耗线程等宝贵的资源。 最终,运行时将耗尽线程并变得无响应,如下图所示。


微服务应用程序必须设计为能够处理部分故障,否则当客户端因等待无响应的服务而阻塞时,运行时可能会耗尽线程

为了防止出现此问题,必须设计您的服务来处理部分故障。

一个值得遵循的好方法是Netflix 描述的方法。 处理部分故障的策略包括:

  • 网络超时——永远不要无限期地阻塞,并且在等待响应时始终使用超时。 使用超时可确保资源不会被无限期地占用。
  • 限制未完成请求的数量——对客户端对特定服务的未完成请求数量设置上限。 如果已达到限制,则进行其他请求可能毫无意义,并且这些尝试需要立即失败。
  • 断路器模式——跟踪成功和失败的请求的数量。 如果错误率超过配置的阈值,则跳闸以便进一步的尝试立即失败。 如果大量请求失败,则表明服务不可用,发送请求毫无意义。 超时后,客户端应再次尝试,如果成功,则关闭断路器。
  • 提供后备方案——当请求失败时执行后备逻辑。 例如,返回缓存数据或默认值(如空的建议集)。

Netflix Hystrix是一个实现这些和其他模式的开源库。 如果您正在使用 JVM,那么您绝对应该考虑使用 Hystrix。 并且,如果您在非 JVM 环境中运行,则应该使用等效库。

IPC技术

有很多不同的 IPC 技术可供选择。 服务可以使用同步基于请求/响应的通信机制,例如基于 HTTP 的 REST 或 Thrift。 或者,他们可以使用异步的基于消息的通信机制,例如 AMQP 或 STOMP。 还有多种不同的消息格式。 服务可以使用人类可读的基于文本的格式,例如 JSON 或 XML。 或者,他们可以使用二进制格式(更高效),例如 Avro 或 Protocol Buffers。 稍后我们将讨论同步 IPC 机制,但首先让我们讨论一下异步 IPC 机制。

异步、基于消息的通信

使用消息传递时,进程通过异步交换消息进行通信。 客户端通过发送消息向服务发出请求。 如果希望服务回复,它会向客户端发送单独的消息。 由于通信是异步的,因此客户端不会阻塞等待答复。 相反,客户端假设不会立即收到答复。

消息由标头(元数据,例如发件人)和消息正文组成。 信息通过渠道进行交换。 任意数量的生产者都可以向一个频道发送消息。 类似地,任意数量的消费者都可以从一个频道接收消息。 有两种类型的渠道:点对点发布订阅。 点对点通道将消息准确地传递给正在从该通道读取消息的其中一个消费者。 服务使用点对点通道来实现前面描述的一对一交互风格。 发布订阅渠道将每条消息传递给所有连接的消费者。 服务使用发布订阅渠道来实现上面描述的一对多交互方式。

下图展示了出租车叫车应用如何使用发布订阅渠道。


出租车叫车应用中的微服务使用发布-订阅渠道进行调度员与其他服务之间的通信

行程管理服务通过向发布订阅频道写入“行程创建”消息来通知调度程序等感兴趣的服务有关新的行程的信息。 调度程序找到可用的驱动程序,并通过向发布订阅频道写入驱动程序建议消息来通知其他服务。

有很多消息传递系统可供选择。 您应该选择一个支持多种编程语言的。 一些消息系统支持标准协议,例如 AMQP 和 STOMP。 其他消息传递系统具有专有但有记录的协议。 有大量开源消息系统可供选择,包括RabbitMQApache KafkaApache ActiveMQNSQ 。 从高层次上讲,它们都支持某种形式的消息和渠道。 他们都力求做到可靠、高性能和可扩展。 然而,每个代理的消息传递模型的细节存在显著差异。

使用消息传递有很多优点:

  • 将客户端与服务分离——客户端只需向适当的渠道发送消息即可发出请求。 客户端完全不知道服务实例。 它不需要使用发现机制来确定服务实例的位置。
  • 消息缓冲——使用同步请求/响应协议(例如 HTTP),客户端和服务在交换期间都必须可用。 相比之下,消息代理将写入通道的消息排队,直到它们可以被消费者处理为止。 例如,这意味着即使订单履行系统缓慢或不可用,在线商店也可以接受来自客户的订单。 订单信息只需排队即可。
  • 灵活的客户服务互动——消息传递支持前面描述的所有互动风格。
  • 显式进程间通信——基于 RPC 的机制试图使调用远程服务看起来与调用本地服务相同。 然而,由于物理定律和部分失效的可能性,它们实际上是完全不同的。 消息传递使这些差异变得非常明确,因此开发人员不会陷入虚假的安全感。

然而,使用消息传递也存在一些缺点:

  • 额外的操作复杂性——消息系统是另一个必须安装、配置和操作的系统组件。 消息代理必须具有高可用性,否则系统可靠性会受到影响。
  • 实现基于请求/响应的交互的复杂性——请求/响应式交互需要一些工作才能实现。 每个请求消息必须包含一个回复通道标识符和一个关联标识符。 服务将包含关联ID的响应消息写入回复通道。 客户端使用关联 ID 将响应与请求进行匹配。 使用直接支持请求/响应的IPC机制通常更容易。

现在我们已经了解了使用基于消息的 IPC,让我们来研究一下基于请求/响应的 IPC。

同步,请求/响应 IPC

当使用同步的、基于请求/响应的 IPC 机制时,客户端会向服务发送请求。 该服务处理请求并返回响应。 在许多客户端中,发出请求的线程在等待响应时被阻塞。 其他客户端可能使用异步的、事件驱动的客户端代码,这些代码或许由 Futures 或 Rx Observables 封装。 然而,与使用消息传递不同,客户端假定响应将及时到达。 有许多协议可供选择。 两种流行的协议是 REST 和 Thrift。 我们首先来看一下 REST。

休息

如今,以RESTful风格开发 API 十分流行。 REST 是一种 (几乎总是) 使用 HTTP 的 IPC 机制。 REST 中的一个关键概念是资源,它通常表示业务对象(例如客户或产品)或业务对象的集合。 REST 使用 HTTP 动词来操作资源,这些资源通过 URL 来引用。 例如, GET请求返回资源的表示,该表示可能是 XML 文档或 JSON 对象的形式。 POST请求创建新资源,而PUT请求更新资源。 引用 REST 的创建者 Roy Fielding 的话:

REST 提供了一组架构约束,当作为一个整体应用时,它强调组件交互的可扩展性、接口的通用性、组件的独立部署以及中间组件,以减少交互延迟、加强安全性并封装传统系统。

下图展示了出租车叫车应用可能使用 REST 的方式之一。


在基于微服务的出租车叫车应用中,乘客智能手机发送 POST 请求,行程管理微服务将其转换为 GET 请求并发送给乘客验证微服务

乘客的智能手机通过向行程管理服务的/trips资源发出POST请求来请求行程。 该服务通过向乘客管理服务发送有关乘客信息的GET请求来处理该请求。 在验证乘客有权创建行程后,行程管理服务将创建行程并返回201响应智能手机。

许多开发人员声称他们基于 HTTP 的 API 是 RESTful。 然而,正如菲尔丁在这篇博文中所描述的,事实上并非所有人都是如此。 Leonard Richardson(无关)为 REST 定义了一个非常有用的成熟度模型,它由以下级别组成。

  • 级别 0 – 级别 0 API 的客户端通过向其唯一的 URL 端点发出 HTTP POST请求来调用服务。 每个请求都指定要执行的操作、操作的目标(例如业务对象)以及任何参数。
  • 级别 1 — 级别 1 API 支持资源的概念。 要对资源执行操作,客户端需要发出POST请求,指定要执行的操作和任何参数。
  • 级别 2 — 级别 2 API 使用 HTTP 动词来执行操作: GET表示检索、 POST表示创建、 PUT表示更新。 请求查询参数和正文(如果有)指定操作的参数。 这使得服务能够利用 Web 基础设施,例如GET请求的缓存。
  • 级别 3 – 级别 3 API 的设计基于名称糟糕的 HATEOAS(超文本作为application状态引擎)原则。 基本思想是, GET请求返回的资源表示包含对该资源执行允许操作的链接。 例如,客户端可以使用响应于发送以检索订单的GET请求而返回的订单表示中的链接取消订单。 HATEOAS 的好处包括不再需要将 URL 硬连线到客户端代码中。 另一个好处是,由于资源的表示包含允许操作的链接,因此客户端不必猜测在当前状态下可以对资源执行哪些操作。

使用基于 HTTP 的协议有许多好处:

  • HTTP 简单且熟悉。
  • 您可以使用Postman等扩展程序在浏览器中测试 HTTP API,也可以使用curl在命令行中测试(假设使用 JSON 或其他文本格式)。
  • 它直接支持请求/响应式通信。
  • 当然,HTTP 是防火墙友好的。
  • 它不需要中间代理,从而简化了系统架构。

使用 HTTP 有一些缺点:

  • 它仅直接支持请求/响应风格的交互。 您可以使用 HTTP 进行通知,但服务器必须始终发送 HTTP 响应。
  • 由于客户端和服务直接通信(没有中介来缓冲消息),所以它们必须在交换期间一直运行。
  • 客户端必须知道每个服务实例的位置(即URL)。 正如上一篇有关 API 网关的文章中所述,这在现代应用中是一个不平凡的问题。 客户端必须使用服务发现机制来定位服务实例。

开发者社区最近重新发现了 RESTful API 接口定义语言的价值。 有几个选项,包括RAMLSwagger 。 一些 IDL(例如 Swagger)允许您定义请求和响应消息的格式。 其他规范(例如 RAML)要求您使用单独的规范(例如JSON Schema ) 。 除了描述 API 之外,IDL 通常还具有根据接口定义生成客户端存根和服务器骨架的工具。

节约

Apache Thrift是 REST 的一个有趣的替代品。 它是一个用于编写跨语言RPC客户端和服务器的框架。 Thrift 提供了 C 风格的 IDL 来定义您的 API。 您使用 Thrift 编译器生成客户端存根和服务器端骨架。 该编译器为多种语言生成代码,包括 C++、Java、Python、PHP、Ruby、Erlang 和 Node.js。

一个Thrift接口由一个或多个服务组成。 服务定义类似于 Java 接口。 它是强类型方法的集合。 Thrift 方法可以返回一个(可能是 void)值,也可以定义为单向的。 返回值的方法实现了请求/响应风格的交互。 客户端等待响应并可能会引发异常。 单向方法对应于通知风格的交互。 服务器未发送响应。

Thrift 支持多种消息格式: JSON、二进制和紧凑二进制。 二进制比 JSON 更高效,因为它解码速度更快。 而且,顾名思义,紧凑二进制是一种节省空间的格式。 当然,JSON 对人类和浏览器都是友好的。 Thrift 还为您提供了传输协议的选择,包括原始 TCP 和 HTTP。 原始 TCP 可能比 HTTP 更高效。 但是,HTTP 对防火墙、浏览器和人类都是友好的。

消息格式

现在我们已经了解了 HTTP 和 Thrift,让我们来研究一下消息格式的问题。 如果您使用消息系统或 REST,您可以选择消息格式。 其他IPC机制(例如Thrift)可能仅支持少数消息格式,也许只支持一种。 无论哪种情况,使用跨语言消息格式都很重要。 即使您今天只用一种语言编写微服务,将来也可能会使用其他语言。

消息格式主要有两种:文本和二进制。 基于文本的格式的示例包括 JSON 和 XML。 这些格式的优点在于它们不仅是人类可读的,而且是自我描述的。 在 JSON 中,对象的属性由名称值对的集合表示。 类似地,在 XML 中,属性由命名元素和值表示。 这使得消息的消费者能够挑选出它感兴趣的值并忽略其余的值。 因此,消息格式的微小变化可以轻松向后兼容。

XML 文档的结构由XML 模式指定。 随着时间的推移,开发者社区逐渐意识到 JSON 也需要类似的机制。 一种选择是使用JSON Schema ,可以是独立的,也可以是 IDL 的一部分,比如 Swagger。

使用基于文本的消息格式的缺点是消息往往很冗长,尤其是 XML。 因为消息是自描述的,所以每条消息除了包含属性值之外还包含属性的名称。 另一个缺点是解析文本的开销。 因此,您可能需要考虑使用二进制格式。

有多种二进制格式可供选择。 如果您使用 Thrift RPC,则可以使用二进制 Thrift。 如果您选择消息格式,流行的选项包括协议缓冲区Apache Avro 。 这两种格式都提供了一种类型化的 IDL 来定义消息的结构。 然而,一个区别是,协议缓冲区使用标记字段,而 Avro 消费者需要知道模式才能解释消息。 因此,使用协议缓冲区比使用 Avro 更容易进行 API 演变。 这篇博文对 Thrift、Protocol Buffers 和 Avro 进行了出色的比较。

概括

微服务必须使用进程间通信机制进行通信。 在设计服务的通信方式时,您需要考虑各种问题:服务如何交互、如何为每个服务指定 API、如何发展 API 以及如何处理部分故障。 微服务可以使用两种IPC机制:异步消息传递和同步请求/响应。 在本系列的下一篇文章中,我们将研究微服务架构中的服务发现问题。

编辑– 本系列文章共七部分,现已完成:

  1. 微服务简介
  2. 构建微服务: 使用 API 网关
  3. 构建微服务: 微服务架构中的进程间通信(本文)
  4. 微服务架构中的服务发现
  5. 微服务的事件驱动数据管理
  6. 选择微服务部署策略
  7. 将整体式架构重构为微服务

您还可以下载完整的文章集,以及有关使用 NGINX Plus 实现微服务的信息,作为电子书 -微服务: 从设计到部署

客座博主 Chris Richardson 是原CloudFoundry.com的创始人,该网站是针对 Amazon EC2 的早期 Java PaaS(平台即服务)。 他现在为各组织提供咨询服务,以改进他们开发和部署应用的方式。 他还定期在http://microservices.io上撰写有关微服务的博客。


“这篇博文可能引用了不再可用和/或不再支持的产品。 有关 F5 NGINX 产品和解决方案的最新信息,请探索我们的NGINX 产品系列。 NGINX 现在是 F5 的一部分。 所有之前的 NGINX.com 链接都将重定向至 F5.com 上的类似 NGINX 内容。”