03-服务间通信

原文链接:https://www.nginx.com/blog/building-microservices-inter-process-communication/

这是我们系列中关于使用微服务架构构建应用程序的第三篇文章。

  • 第一篇文章介绍了微服务架构模式,将其与单体应用架构模式进行了比较,并讨论了使用微服务的优缺点。
  • 第二篇文章描述了应用程序的客户端如何通过称为API网关的中介与微服务进行通信。

在本文中,我们将了解系统中的服务如何相互通信。 第四篇文章将探讨服务发现密切相关问题。

介绍

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

image

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

交互模式

在为服务选择IPC机制时,首先应考虑服务如何交互。有各种client⇔service交互模式。它们可以按两个维度进行分类:

  • 第一个维度:交互是一对一 or 一对多:
    • 一对一:每个客户端请求只由一个服务实例处理
    • 一对多:每个客户端请求都由多个服务实例处理
  • 第二个维度:交互是同步 or 异步:
    • 同步:客户端期望服务端及时响应,甚至可能在等待时阻塞
    • 异步:客户端在等待响应时不会阻塞,并且响应(如果有)不一定立即发送

下表显示了各种交互方式:

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

存在以下类型的一对一交互

  • (同步)请求/响应:客户端向服务发出请求并等待响应,客户端希望响应及时到达。在基于线程的应用程序中,发出请求的线程甚至可能在等待时阻塞
  • (异步)通知(例如,单向请求):客户端向服务发送请求但不等待回复
  • (异步)请求/异步响应:客户端向服务发送请求,该服务以异步方式进行回复,客户端在等待期间不会阻塞,并且假设响应可能不会在一段时间内到达

存在以下类型的一对多交互

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

每项服务通常使用这些交互方式的组合。对于某些服务,单个IPC机制就足够了。其他服务可能需要使用IPC机制的组合。下图显示了当乘客请求行程时,出租车应用程序中的服务可能如何交互。

image

这些服务使用:

  • 通知:乘客的智能手机向行程管理服务发送通知以请求找到可乘坐的出租车
  • 请求/响应:行程管理服务通过使用请求/响应来调用乘客管理服务来验证乘客的帐户是否有效
  • 发布/订阅:行程管理服务创建行程并使用发布/订阅来通知其他服务,包括分派系统(Dispatcher,用于定位可用的驱动程序)。

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

定义API

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

正如将在本文后面看到的,API定义的性质取决于使用的IPC机制:

  • 如果使用消息传递,则API由消息通道和消息类型组成
  • 如果使用HTTP,则API由URL以及请求和响应格式组成

稍后我们将更详细地描述一些IDL。

升级API

服务的API总是随着时间的推移而变化。

  • 在单体应用中,通常可以直接更改API并更新所有调用者。
  • 在微服务中,即使API的所有使用者都是同一应用程序中的其他服务,也要困难得多。

通常无法强制所有客户端与服务保持同步升级。此外,可能会逐步部署新版本的服务,以便同时运行旧版本和新版本的服务。制定处理这些问题的战略非常重要。

如何处理API的更改取决于这次API变更的大小

  1. 某些更改是次要的,并且这些变更能够向后兼容。

    例如,向请求或响应添加属性。此时,应该遵守稳健性原则来设计客户端和服务的API。保证使用旧版API的客户端能够继续访问新版本的服务。该服务为缺少的请求属性提供默认值,客户端忽略任何额外的响应属性。

    使用IPC机制和消息传递格式,使得我们可以轻松的升级API。

  2. 有时必须对API进行主要的,不兼容的更改。

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

处理部分失败

正如前一篇关于API网关的文章所述,在分布式系统中,存在部分失败的风险。由于客户端和服务是单独的进程,因此服务可能无法及时响应客户端的请求:

  • 由于故障或维护,服务可能会关闭
  • 服务可能过载并且对请求的响应非常缓慢

例如,前一篇文章中的商品详细信息方案。假设推荐服务没有响应,客户端的简单实现可能会无限期地阻止响应。这不仅会导致糟糕的用户体验,而且在许多应用程序中它会消耗诸如线程之类的宝贵资源。最终运行时将耗尽线程并变得无响应,如下图所示。

image

要防止出现此问题,必须设计服务以处理部分故障。

Netflix描述了一个很好的方法。处理部分失败的策略包括:

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

Netflix Hystrix是一个开源库,可以实现上述和其他模式。如果使用的是JVM,那么一定要考虑使用Hystrix。而且,如果在非JVM环境中运行,则应使用等效库。

IPC技术

有许多不同的IPC技术可供选择。

  • 服务可以使用基于HTTP的REST或Thrift等基于请求/响应的同步通信机制
  • 可以使用异步的,基于消息的通信机制,例如AMQP或STOMP,还有各种不同的消息格式
  • 服务可以使用人类可读的基于文本的格式,例如JSON或XML
  • 可以使用二进制格式(更有效),例如Avro或Protocol Buffers

稍后我们将研究同步IPC机制,但首先让我们讨论异步IPC机制。

异步,基于消息的通信使用消息传递时

进程通过异步交换消息进行通信。客户端通过向服务发送消息来向服务发出请求。

  • 如果希望服务回复,则通过向客户端发送单独的消息来实现。由于通信是异步的,因此客户端不会阻塞等待回复
  • 否则,客户端会认为回复不会立即收到。

消息由消息头(元数据,如发件人)和消息体组成,并且,消息是通过通道交换。任何数量的生产者都可以向通道发送消息,同样,任何数量的消费者都可以从通道接收消息。有两种渠道:

  • 点对点:点对点通道向正在从通道读取的一个消费者传递消息。服务使用点对点通道来实现前面描述的一对一交互样式
  • 发布/订阅:通道将每条消息传递给所有订阅的消费者。服务使用发布/订阅通道来实现上述的一对多交互样式

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

image

行程管理服务通过将生产的消息写入发布/订阅通道来向感兴趣的服务(如分派系统Dispatcher)通知新的行程。分派系统通过向发布/订阅通道写入驱动响应消息来查找可用的驱动程序并通知其他服务。

有许多消息系统可供选择,应该选择一个支持各种编程语言的:

  • 某些消息传递系统支持标准协议,如AMQP和STOMP
  • 其他消息系统具有专用(有参考文档)的协议。有大量的开源消息系统可供选择,包括:
    • RabbitMQ
    • Apache Kafka
    • Apache ActiveMQ
    • NSQ

在较高的层面上,它们都支持某种形式的消息和通道,它们都力求稳定,高性能和可扩展。但是,每个消息代理的消息传递模型的细节存在显着差异。

使用消息传递有许多优点

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

使用消息传递还有一些缺点

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

现在我们已经考虑使用基于消息传递的IPC,让我们查看基于请求/响应的IPC。

同步,请求/响应IPC

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

REST

今天,以RESTful风格开发API是时髦的。REST是一种(几乎总是)使用HTTP的IPC机制。

  • REST中的一个关键概念是资源,它通常表示业务对象,如客户或商品,或业务对象的集合。
  • REST使用HTTP谓词来操作资源,这些资源使用URL引用。例如:
    • GET请求返回资源的表示形式,该形式可能是XML文档或JSON对象的形式。
    • POST请求创建新资源,PUT请求更新资源。

引用REST的创建者Roy Fielding:"REST provides a set of architectural constraints that, when applied as a whole, emphasizes scalability of component interactions, generality of interfaces, independent deployment of components, and intermediary components to reduce interaction latency, enforce security, and encapsulate legacy systems.”---《Architectural Styles and the Design of Network-based Software Architectures》

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

下图显示了出租车应用程序可能使用REST的方法之一。

image

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

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

  • 级别0:API的客户端通过向其唯一的URL端点发出HTTP POST请求来调用该服务。每个请求指定要执行的操作,操作的目标(例如业务对象)和任何参数
  • 级别1:API支持资源的概念。要对资源执行操作时客户端会发出POST请求并指定要执行的操作和参数
  • 级别2:API使用HTTP谓词执行操作:
    • 检索GET
    • 创建POST
    • 更新PUT

请求查询参数和请求体(如果有)来指定操作的参数。这使服务能够利用Web基础结构,例如缓存GET请求。

  • 级别3:API的设计基于HATEOAS(Hypertext As The Engine Of Application State,超文本作为应用程序状态引擎)原则。基本思想是GET请求返回的资源表示包含用于对该资源执行允许操作的链接。例如,客户端可以使用为响应发送的GET请求而返回的Order结果中的链接来取消订单在一个检索订单的请求中。

    HATEOAS的好处包括:

    1. 不再需要将URL硬连接到客户端代码中
    2. 由于资源的表示包含允许操作的链接,因此客户端不必猜测可以对当前状态的资源执行哪些操作

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

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

使用HTTP有一些缺点:

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

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

Thrift

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 Buffers和Apache Avro。这两种格式都提供了一个用于定义消息结构的类型IDL。区别是:
    • Protocol Buffers使用标记字段
    • Avro消费者需要知道模式才能解释消息。

    因此,试用Protocol Buffers的API演变比使用Avro更容易

这篇博文是Thrift,Protocol Buffers和Avro的绝佳比较。

总结

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

上次修改: 14 April 2020