本书主要介绍如何使用微服务架构构建应用程序,这是本书的第三章。第一章介绍了微服务架构模式,将其与单体架构模式进行对比,并讨论了使用微服务的优点与缺点。第二章描述了应用程序客户端通过扮演中间人角色的 API 网关与微服务进行通信。在本章中,我们来了解一下系统中的服务是如何相互通信的。第四章将详细探讨服务发现方面的内容。
在单体应用程序中,组件可通过语言级方法或者函数相互调用。相比之下,基于微服务的应用程序是一个运行在多台机器上的分布式系统。通常,每个服务实例都是一个进程。
因此,如图 3-1 所示,服务必须使用进程间通信(IPC)机制进行交互。
稍后我们将了解到多种 IPC 技术,但在此之前,我们先来探讨一下涉及到的各种设计问题。
当为服务选择一种 IPC 机制时,首先需要考虑服务如何交互。有许多种客户端 — 服务交互方式。可以从两个维度对它们进行分类。第一个维度是交互方式是一对一还是一对多:
第二个维度是交互是同步的还是异步的:
下表展示了各种交互方式。
– | 一对一 | 一对多 |
---|---|---|
同步 | 请求/响应 | – |
异步 | 通知 | 发布/订阅 |
异步 | 请求/异步响应 | 发布/异步响应 |
表 3-1、进程间通信方式
一对一交互分为以下列举的类型,包括同步(请求/响应)与异步(通知与请求/异步响应):
一对多交互可分为以下列举的类型,它们都是异步的:
客户端发布请求消息,之后等待一定时间来接收消费者的响应。
通常,每个服务都组合着使用这些交互方式。对一些服务而言,单一的 IPC 机制就足够了,但其他服务可能需要组合多个 IPC 机制。
图 3-2 显示了当用户请求打车时,打车应用中的服务可能会发生交互。
服务使用了通知、请求/响应和发布/订阅组合。例如,乘客的智能手机向 Trip Management 微服务发送一条通知以请求一辆车。Trip Management 服务通过使用请求/响应来调用 Passenger Management 服务以验证乘客的帐户是否可用。之后,Trip Management 服务创建路线,并使用发布/订阅通知其他服务,包括用于定位可用司机的 Dispatcher。
现在我们来看一下交互方式,我们先来看看如何定义 API。
服务 API 是服务与客户端之间的契约。无论您选择何种 IPC 机制,使用接口定义语言(interface definition language,IDL)来严格定义服务 API 都是非常有必要的。有论据证明使用 API 优先法定义服务更加合理。在对需要实现的服务的 API 定义进行迭代之后,您可以通过编写接口定义并与客户端开发人员进行审阅来开始开发服务。这样设计可以增加您成功的机率,以构建出符合客户端需求的服务。
正如您将会在后面看到,定义 API 的方式取决于您使用何种 IPC 机制。如果您正在使用消息传递,那么 API 是由消息通道和消息类型组成。如果您使用的是 HTTP,那么 API 是由 URL、请求和响应格式组成。稍后我们将详细地介绍关于 IDL 方面的内容。
服务 API 总是随着时间而变化。在单体应用程序中,更改 API 和更新所有调用者通常是一件直截了当的事。但在基于微服务的应用程序中,即使 API 的所有消费者都是同一应用程序中的其他服务,要想完成这些工作也是非常困难的。通常,您无法强制所有客户端与服务升级的节奏一致。此外,您可能需要逐步部署服务的新版本,以便新旧版本的服务同时运行。因此,制定这些问题的处理策略还是很重要的。
处理 API 变更的方式取决于变更的程度。某些更改是次要或需要向后兼容以前的版本。例如,您可能会向请求或响应添加属性。此时设计客户端与服务遵守鲁棒性原则就显得很有意义了。新版本的服务要能兼容使用旧版本 API 的客户端。该服务为缺少的请求属性提供默认值,并且客户端忽略所有多余的响应属性。使用 IPC 机制和消息格式非常重要,他们可以让您轻松地演化 API。
但有时候,您必须对 API 作出大量不兼容的更改。由于您无法强制客户端立即升级,服务也必须支持较旧版本的 API 一段时间。如果您使用了基于 HTTP 的机制(如 REST),则一种方法是将版本号嵌入到 URL 中。每个服务实例可能同时处理多个版本。或者,您可以部署多个不同的实例,每个实例用于处理特定版本。
正如第二章中关于 API 网关所述,在分布式系统中始终存在局部故障的风险。由于客户端进程与服务进程是分开的,服务可能无法及时响应客户端的请求。由于故障或者维护,服务可能需要关闭。也有可能因服务过载,造成响应速度变得极慢。
例如,请回想第二章中的产品详细信息场景。我们假设推荐服务没有响应。低级的客户端实现可能会无限期地阻塞以等待响应。这不仅会导致用户体验糟糕,而且在许多应用程序中,它会消耗线程等宝贵的资源。最终导致运行时系统将线程消耗完,造成无法响应,如图 3-3 所示。
为了防止出现此类问题,在设计服务时必须考虑处理局部故障。以下是一个由 Netflix 给出的好办法。处理局部故障的策略包括:
Netflix Hystrix 是一个实现上述和其他模式的开源库。如果您正在使用 JVM,那么您一定要考虑使用 Hystrix。如果您在非 JVM 环境中运行,则应使用相等作用的库。
有多种 IPC 技术可供选择。服务可以使用基于同步请求/响应的通信机制,比如基于 HTTP 的 REST 或 Thrift。或者,可以使用异步、基于消息的通信机制,如 AMQP 或 STOMP。
还有各种不同的消息格式。服务可以使用可读的、基于文本的格式,如 JSON 或 XML。或者,可以使用如 Avro 或 Protocol Buffers 等二进制格式(更加高效)。稍后我们将讨论同步 IPC 机制,但在此之前让我们先来讨论一下异步 IPC 机制。
当使用消息传递时,进程通过异步交换消息进行通信。客户端通过发送消息向服务发出请求。如果服务需要回复,则通过向客户端发送一条单独的消息来实现。由于通信是异步的,因此客户端不会阻塞等待回复。相反,客户端被假定不会立即收到回复。
一条消息由头部(如发件人之类的元数据)和消息体组成。消息通过通道进行交换。任何数量的生产者都可以向通道发送消息。类似地,任何数量的消费者都可以从通道接收消息。有两种通道类型,分别是点对点(point‑to‑point)与发布订阅(publish‑subscribe):
图 3-4 展示了打车应用程序如何使用发布订阅通道。
Trip Management 服务通过向发布订阅通道写入 Trip Created 消息来通知已订阅的服务,如 Dispatcher。Dispatcher 找到可用的司机并通过向发布订阅通道写入 Driver Proposed 消息来通知其他服务。
有许多消息系统可供选择,您应该选择一个支持多种编程语言的。
一些消息系统支持标准协议,如 AMQP 和 STOMP。其他消息系统有专有的文档化协议。
有大量的开源消息系统可供选择,包括 RabbitMQ、Apache Kafka、Apache ActiveMQ 和 NSQ。从高层而言,他们都支持某种形式的消息和通道。他们都力求做到可靠、高性能和可扩展。然而,每个代理的消息传递模型细节上都存在着很大差异。
使用消息传递有很多优点:
然而,消息传递也存在一些缺点:
现在我们已经了解了使用基于消息的 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 提供了一套架构约束,当应用作为整体时,其强调组件交互的可扩展性、接口的通用性、组件的独立部署以及中间组件,以减少交互延迟、实施安全性和封装传统系统。” — Roy Fielding,《架构风格与基于网络的软件架构设计》
图 3-5 展示了打车应用程序可能使用 REST 的方式之一。
乘客的智能手机通过向 Trip Management 服务的 /trips
资源发出一个 POST 请求来请求旅程。该服务通过向 Passenger Management 服务发送一个获取乘客信息的 GET 请求来处理该请求。在验证乘客有权限创建旅程后,Trip Management 服务将创建旅程,并向智能手机返回 201 响应。
许多开发人员声称其基于 HTTP 的 API 就是 RESTful。然而,正如 Fielding 在这篇博文中所描述的那样,并不是都是这样。
Leonard Richardson 定义了一个非常有用的 REST 成熟度模型,包括以下层次:
使用基于 HTTP 的协议有很多好处:
使用 HTTP 也存在一些缺点:
开发人员社区最近重新发现了 RESTful API 接口定义语言的价值。有几个可以选择,包括 RAML 和 Swagger。一些 IDL(如 Swagger)允许您定义请求和响应消息的格式。其他如 RAML,需要您使用一个单独的规范,如 JSON 模式。除了用于描述 API 之外,IDL 通常还具有可从接口定义生成客户端 stub 和服务器 skeleton 的工具。
Apache Thrift 是 REST 的一个有趣的替代方案。它是一个用于编写跨语言 RPC 客户端和服务器框架。Thrift 提供了一个 C 风格的 IDL 来定义您的 API。您可以使用 Thrift 编译器生成客户端 stub 和服务器端 skeleton。编译器可以生成各种语言的代码,包括 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 模式(schema)定义。随着时间的推移,开发人员社区已经意识到 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 机制:异步消息传递和同步请求/响应。为了进行通信,一个服务必须能够找到另一个服务。在第四章中,我们将介绍微服务架构中服务发现问题。
by Floyd Smith
NGINX 使您能够实现各种伸缩和镜像操作,使您的应用程序更加灵敏和高度可用。您为伸缩和镜像所作的选择会影响到您如何进行进程间通信,这是本章的主题。
我们在 NGINX 方面建议您在实现基于微服务的应用程序时考虑使用四层架构。Forrester 在这方面有详细的报告,您可以从 NGINX 上免费下载。这些层代表客户端(包括台式机或笔记本电脑、移动、可穿戴或 IoT 客户端)、交付、聚合(包括数据存储)和服务,其中包括应用功能和特定服务,而不是共享数据存储。
四层架构比以前的三层架构更加灵活,具有可扩展、响应灵敏、移动友好,并且内在支持基于微服务的应用程序开发和交付等优点。像 Netflix 和 Uber 这样的行业引领者能够通过使用这种架构来实现用户所需的性能水平。
NGINX 本质上非常适合四层架构,从客户端层的媒体流,到交付层的负载均衡与缓存、聚合层的高性能和安全的基于 API 的通信的工具,以及服务层中支持灵活管理的短暂服务实例。
同样的灵活性使得 NGINX 可以实现强大的伸缩和镜像模式,以处理流量变化,防止安全攻击,此外还提供可用的故障配置切换,从而实现高可用。
在更为复杂的架构中,包括服务实例实例化和需求不断的服务发现,解耦的进程间通信往往更受青睐。异步和一对多通信方式可能比高耦合的通信方式更加灵活,它们最终提供更高的性能和可靠性。
from:https://github.com/DocsHome/microservices/blob/master/3-inter-process-communication.md