这七个系列的文章现在完成:

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

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

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

介绍

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

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

互动样式

当为服务选择IPC机制时,首先考虑服务如何交互是有用的。有各种client⇔service交互风格。它们可以沿着两个维度分类。第一个维度是交互是一对一还是一对多:

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

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

  • 同步 - 客户端期望来自服务的及时响应,并且甚至可以在其等待时阻塞。
  • 异步 - 客户端在等待响应时不阻止,并且响应(如果有)不一定立即发送。

下表显示各种交互样式。

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

有以下种类的一对一交互:

  • 请求/响应 - 客户端向服务器发出请求并等待响应。 客户期望响应及时到达。 在基于线程的应用程序中,发出请求的线程甚至可能在等待时阻塞。
  • 通知(也称为单向请求) - 客户端向服务器发送请求,但不期望或发送回复。
  • 请求/异步响应 - 客户端向服务异步回复的服务发送请求。 客户端在等待时不阻塞,并且设计有假设响应可能不会到达一段时间。

有以下种类的一对多交互:

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

每个服务通常使用这些交互风格的组合。对于某些服务,单个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网关的文章所述,在分布式系统中,存在部分故障的永远存在的风险。由于客户端和服务是单独的进程,因此服务可能无法及时响应客户端的请求。由于故障或维护,服务可能会关闭。或者服务可能过载并且对请求响应非常慢。

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

为了防止这个问题,你必须设计你的服务来处理部分失败。

一个好的方法来跟随是Netflix描述的。处理部分故障的策略包括:

  • 网络超时 - 不要无限期地阻止,并且在等待响应时始终使用超时。 使用超时可确保资源永远不会无限制地关闭。
  • 限制未完成请求的数量 - 对客户端可能拥有的特定服务的未完成请求的数量施加上限。 如果已达到限制,则可能没有做出额外的请求,这些尝试需要立即失败。
  • 断路器模式
    • 跟踪成功和失败请求的数量。 如果错误率超过配置的阈值,请断开断路器,以便进一步尝试立即失败。 如果大量请求失败,则表明服务不可用,并且发送请求是无意义的。 超时时间后,客户端应再次尝试,如果成功,请关闭断路器。
  • 提供回退 - 请求失败时执行回退逻辑。 例如,返回缓存数据或默认值,例如空的推荐集。

NetflixHystrix是一个实现这些和其他模式的开源库。如果你使用JVM,你一定要考虑使用Hystrix。并且,如果您在非JVM环境中运行,则应使用等效库。

IPC技术

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

异步,基于消息的通信

当使用消息传递时,进程通过异步交换消息进行通信。客户端通过向服务发送消息向服务发出请求。如果服务期望回复,它通过发送单独的消息回到客户端这样做。由于通信是异步的,所以客户端不会阻塞等待应答。相反,客户端被写为假定不会立即接收到答复。

一个消息由头(元数据,例如发件人)和消息体。消息通过通道交换。任何数量的生产者可以向一个信道发送消息。类似地,任何数量的消费者可以从频道接收消息。有两种类型的通道,点对点发布订阅。点对点信道向正在从信道读取的消费者中的一个消费者传递消息。服务使用点对点通道用于前面描述的一对一交互风格。发布 - 订阅频道将每个消息传递给所有附加的消费者。服务使用发布 - 订阅频道来实现上述的一对多交互方式。

下图显示了出租汽车应用程序如何使用发布订阅频道。

旅行管理服务通过向发布订阅频道写入旅行创建消息来通知感兴趣的服务,例如调度员关于新旅行。Dispatcher找到一个可用的驱动程序并通过向发布订阅通道写入一个驱动程序建议消息来通知其他服务。

有许多消息系统可供选择。你应该选择一个支持各种编程语言。一些消息系统支持标准协议,如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/trips资源发出请求来请求旅行。此服务通过向GET乘客管理服务发送有关乘客信息的请求来处理请求。在验证乘客被授权创建旅行之后,旅行管理服务创建旅行并且201向智能电话返回响应。

许多开发人员声称他们的基于HTTP的API是RESTful的。但是,正如Fielding在这篇博客中描述的,并不是所有的都是。Leonard Richardson(无关系)定义了一个非常有用的REST成熟度模型,由以下级别组成。

  • 级别0 - 级别0的客户端通过 POST 向其唯一的URL端点 发出HTTP 请求来 调用服务 。 每个请求指定要执行的操作,操作的目标(例如业务对象)和任何参数。
  • 第1级 - 第1级API支持资源的想法。 要对资源执行操作,客户端会发出一个 POST 请求,指定要执行的操作和任何参数。
  • 第2级 - 第2级API使用HTTP动词执行操作: GET 检索, POST 创建和 PUT 更新。 请求查询参数和正文(如果有)指定动作的参数。 这使服务能够利用Web基础架构,如缓存 GET 请求。
  • 级别3 - 3级API的设计基于可怕的命名HATEOAS(超文本作为应用程序状态引擎)原理。 基本思想是由 GET 请求 返回的资源的表示 包含用于对该资源执行允许的动作的链接。 例如,客户端可以使用响应于 GET 发送 的 请求 返回的Order表示中的链接来取消订单 以检索订单。 HATEOAS的好处 包括不再需要将URL硬编码到客户端代码中。 另一个好处是,因为资源的表示包含用于允许的动作的链接,所以客户端不必猜测对其当前状态下的资源可以执行什么动作。

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

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

使用HTTP有一些缺点:

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

开发人员社区最近重新发现了RESTful API的接口定义语言的价值。有几个选项,包括RAMLSwagger。一些IDL(如Swagger)允许您定义请求和响应消息的格式。其他如RAML要求您使用单独的规范,如JSON模式。除了描述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。如果你可以选择消息格式,流行的选项包括Protocol BuffersApache Avro。这两种格式都提供了一个类型化的IDL来定义消息的结构。然而,一个区别是协议缓冲器使用标记字段,而Avro消费者需要知道该模式以解释消息。因此,使用协议缓冲区的API演化比使用Avro更容易。这篇博文是Thrift,Protocol Buffers和Avro的一个很好的比较。

概要

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

这七个系列的文章现在完成:

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

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

results matching ""

    No results matching ""