# 3、进程间通信
本书的第三章主要是关于使用微服务架构构建应用程序。第一章介绍了[微服务架构模式](http://microservices.io/patterns/microservices.html)，将其与单体架构模式进行对比，并讨论了使用微服务的优点和缺点。第二章描述了应用程序客户端通过扮演中间人角色的 [API 网关](http://microservices.io/patterns/apigateway.html)与微服务器进行通信。在本章中，我们来了解一下系统中的服务是如何相互通信的。第四章将详细探讨服务发现方面的内容。

## 3.1、简介
在单体应用程序中，组件可通过语言级方法或者函数相互调用。相比之下，基于微服务的应用程序是一个运行在多台机器上的分布式系统。通常，每个服务实例是一个进程。

因此，如图 3-1 所示，服务必须使用进程间通信（IPC）机制进行交互。

稍后我们将了解到多种 IPC 技术，但在此之前，我们先来探讨一下涉及到的各种设计问题。

![使用进程间通信交互的微服务](https://github.com/oopsguy/microservices-from-design-to-deployment-chinese/blob/master/resources/3-1.png)

## 3.2、交互方式
当为服务选择一种 IPC 机制时，首先需要考虑服务如何交互。有很多种客户端←→服务端交互方式。它们可以分为两个类。第一类是交互是一对一还是一对多：

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

第二类是交互是同步还是异步：

- **同步** - 客户端要求服务及时响应，在等待过程中可能会发生阻塞。
- **异步** - 客户端在等待响应时不会发生阻塞，但响应（如果有）不一定立即返回。

下表展示了各种交互方式。

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

表 3-1、进程间通信方式

一对一交互分为以下列举的类型，包括同步（请求/响应）和异步（通知与请求/异步响应）：

- **请求/响应** - 客户端向服务发出请求并等待响应。客户端要求响应及时到达。在基于线程的应用程序中，发出请求的线程可能在等待时发生阻塞。
- **通知（a.k.a. 单向请求）** - 客户端向服务发送请求，但不要求响应。
- **请求/异步响应** - 客户端向服务发送请求，服务异步响应。客户端在等待时不发生阻止，适用于假设响应可能不会立即到达的场景。

一对多交互可分为以下列举的类型，它们都是异步的：

- **发布/订阅** - 客户端发布通知消息，由零个或多个感兴趣的服务消费。
- **发布/异步响应** - 客户端发布请求消息，然后等待一定时间来接收消费者的响应。

通常，每个服务都组合着使用这些交互方式。对于一些服务，单一的 IPC 机制就足够了，但其他服务可能需要组合多个 IPC 机制。

图 3-2 显示了当用户请求打车时，出租车应用程序中的服务可能会发生交互。

![使用了多重 IPC 机制的服务交互](https://github.com/oopsguy/microservices-from-design-to-deployment-chinese/blob/master/resources/3-2.png)

服务使用了通知、请求/响应和发布/订阅组合。例如，乘客的智能手机向 Trip Management 微服务发送通知以请求一辆车。Trip Management 服务通过使用请求/响应来调用 Passenger Management 服务以验证乘客的帐户是可用。之后，Trip Management 服务创建路线，并使用发布/订阅通知其他服务，包括用于定位可用司机的调度程序。

现在我们来看一下交互方式，我们先来看看如何定义 API。

## 3.3、定义 API
服务 API 是服务与客户端之间的契约。无论您选择何种 IPC 机制，使用接口定义语言（interface definition language，IDL）严格定义服务 API 都是非常有必要的。有论据证明使用 [API 优先（API‑first）法](https://www.programmableweb.com/news/how-to-design-great-apis-api-first-design-and-raml/how-to/2015/07/10)定义服务更加合适。在对您需要实现的服务的 API 定义进行迭代之后，您可以通过编写接口定义并与客户端开发人员进行审阅来开始开发服务。这样设计可以增加您构建出符合客户端需求的服务的几率。

正如您将会在后面看到，API 定义的方式取决于您使用的是哪种 IPC 机制。如果您正在使用消息服务，则 API 由消息通道和消息类型组成。如果您使用 HTTP，则 API 由 URL、请求和响应格式组成。稍后我们详细地介绍关于 IDL 方面的内容。

## 3.4、演化 API
服务的 API 总是随着时间而变化。在单体应用程序中，更改 API 和更新所有调用者通常都是直截了当的。但在基于微服务的应用程序中，即使您的 API 的所有消费者都是同一应用程序中的其他服务，要想完成这些工作也是非常困难的。通常，您无法强制所有客户端与服务升级的节奏一致。此外，您可能会[逐步部署新版本的服务](http://techblog.netflix.com/2013/08/deploying-netflix-api.html)，以便新旧版本的服务同时运行。制定这些问题的处理策略是很重要的。

处理 API 变更的方式取决于变更的程度。某些更改是次要，需要向后兼容以前的版本。例如，您可能会向请求或响应添加属性。设计客户与服务时遵守[鲁棒性原则](https://en.wikipedia.org/wiki/Robustness_principle)是很有意义的。使用较旧 API 的客户端应继续使用新版本的服务。该服务为缺少的请求属性提供默认值，并且客户端忽略任何多余的响应属性。使用 IPC 机制和消息格式非常重要，可以让您轻松地演化 API。

但有时候，您必须对 API 作出重大不兼容的更改。由于您无法强制客户端立即升级，服务也必须支持较旧版本的 API 一段时间。如果您使用了基于 HTTP 的机制（如 REST），则一种方法是将版本号嵌入 URL 中。每个服务实例可能同时处理多个版本。或者，您可以部署每个用于处理特定版本的不同实例。

## 3.5、处理局部故障
正如第二章中关于 API 网关所述，在分布式系统中存在着局部故障风险。由于客户端进程和服务进程是分开的，服务可能无法及时响应客户端的请求。由于故障或者维护，服务可能会关闭。也有可能因服务过载，响应速度变得极慢。

例如，请考虑第二章中的产品详细信息场景。我们假设 Recommendation Service 没有反应。客户端天真的实现可能会无限期地阻塞以等待响应。这不仅会导致用户体验糟糕，而且在许多应用程序中，它将消耗诸如线程等宝贵资源。以致最终，运行时将线程用完，造成无法响应，如图 3-3 所示。

![由无响应服务引起的线程阻塞](https://github.com/oopsguy/microservices-from-design-to-deployment-chinese/blob/master/resources/3-3.png)

为了防止这个问题出现，您必须设计您的服务来处理局部故障。以下是一个由 [Netflix 给出的好方法](http://techblog.netflix.com/2012/02/fault-tolerance-in-high-volume.html)。处理局部故障的策略包括：

- **网络超时** - 在等待响应时，不要无限期地阻塞，始终使用超时方案。使用超时方案确保资源不被无限地消耗。

- **限制未完成的请求数量** - 对客户端拥有特定服务的未完成请求的数量设置上限。如果达到了上限，则发出的额外请求可能是毫无意义的，这些尝试需要立即失败。

- **[断路器模式](http://martinfowler.com/bliki/CircuitBreaker.html)** - 追踪成功和失败请求的数量。如果错误率超过配置的阈值，则断开断路器，以便后续的尝试能立即失败。如果大量请求失败，则表明服务不可用，发送请求将是无意义的。发生超时后，客户端应重新尝试，如果成功，则关闭断路器。

- **提供回退** - 请求失败时执行回退逻辑。例如，返回缓存数据或者默认值，比如一组空的推荐。

[Netflix Hystrix](https://github.com/Netflix/Hystrix) 是一个实现上述和其他模式的开源库。如果您正在使用 JVM，那么您一定要考虑使用 Hystrix。如果您在非 JVM 环境中运行，则应使用相等作用的库。

## 3.6、IPC 技术
有很多不同的 IPC 技术可供选择。服务可以使用基于同步请求/响应的通信机制，比如基于 HTTP 的 REST 或 Thrift。或者，可以使用异步、基于消息的通信机制，如 AMQP 或 STOMP。

还有各种不同的消息格式。服务可以使用人类可读的、基于文本的格式，如 JSON 或 XML。或者，可以使用如 Avro 或 Protocol Buffers 等二进制格式（更加有效）。稍后我们将讨论同步 IPC 机制，但首先让我们来讨论一下异步 IPC 机制。

## 3.7、异步、基于消息的通信
当使用消息传递时，进程通过异步交换消息进行通信。客户端通过发送消息向服务发出请求。如果服务需要回复，则通过向客户端发送一条单独的消息来实现。由于通信是异步的，因此客户端不会阻塞等待回复。相反，客户端被假定不会立即收到回复。

一条[消息](http://www.enterpriseintegrationpatterns.com/patterns/messaging/Message.html)由头部（如发件人之类的元数据）和消息体组成。消息通过[通道](http://www.enterpriseintegrationpatterns.com/patterns/messaging/MessageChannel.html)进行交换。任何数量的生产者都可以向通道发送消息。类似地，任何数量的消费者都可以从通道接收消息。有两种通道，[点对点](http://www.enterpriseintegrationpatterns.com/patterns/messaging/PointToPointChannel.html)（point‑to‑point）和[发布订阅](http://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html)（publish‑subscribe）：

- **点对点通道**发送一条消息给一个切确的、正在从通道读取消息的消费者。服务使用点对点通道，就是上述的一对一交互方式。
- **发布订阅通道**将每条消息传递给所有订阅的消费者。服务使用发布订阅通道，就是上述的一对多交互方式。

图 3-4 展示了打车应用程序如何使用发布订阅通道。

![使用了发布-订阅通道的打车应用]()

Trip Management 服务通过向发布订阅通道写入 Trip Created 消息来通知已订阅的服务，如 Dispatcher。 Dispatcher 找到可用的司机并通过向发布订阅通道写入 Driver Proposed 消息来通知其他服务。

有许多信息系统可供选择。您应该选择一个支持多种编程语言的。

一些消息系统支持标准协议，如 AMQP 和 STOMP。其他消息系统有专有的但为文档化的协议。

有大量的开源消息系统可供选择，包括 [RabbitMQ](http://www.rabbitmq.com/)、[Apache Kafka](http://kafka.apache.org/)、[Apache ActiveMQ](http://activemq.apache.org/) 和 [NSQ](https://github.com/bitly/nsq)。在高层上，他们都支持某种形式的消息和通道。他们都力求做到可靠、高性能和可扩展。然而，每个代理的消息传递模型细节上都存在着很大差异。

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

- **将客户端与服务分离** - 客户端通过向相应的通道发送一条消息来简单地发出一个请求。服务实例对客户端而言是透明的。客户端不需要使用发现机制来确定服务实例的位置。
- **消息缓冲** - 使用如 HTTP 的同步请求/响应协议，客户端和服务在交换期间必须可用。相比之下，消息代理会将消息写入通道入队，直到消费者处理它们。这意味着，例如，即使订单执行系统出现缓慢或不可用的情况，在线商店还是可以接受客户的订单。订单消息只需要简单地排队。
- **灵活的客户端-服务交互** - 消息传递支持前面提到的所有交互方式。
- **毫无隐瞒的进程间通信** - 基于 RPC 的机制试图使调用远程服务看起来与调用本地服务相同。然而，由于物理因素和局部故障的可能性，他们实际上是完全不同的。消息传递使这些差异变得非常明显，所以开发人员不会被这些虚假的安全感所欺骗。

然而，消息传递也存在一些缺点：

- **额外的复杂操作** - 消息传递系统是一个需要安装、配置和操作的系统组件。消息代理程序必须高度可用，否则系统的可靠性将受到影响。
- **实施基于请求/响应的交互的复杂性** - 请求/响应式交互需要做些工作来实现。每个请求消息必须包含应答通道标识符和相关标识符。该服务将包含相关 ID 的响应消息写入应答信道。客户端使用相关 ID 将响应与请求相匹配。通常使用直接支持请求/响应的 IPC 机制更加容易。

现在我们已经了解了使用基于消息的 IPC，让我们来看看请求/响应的 IPC。

## 3.8、同步的请求/响应 IPC
当使用基于同步、基于请求/响应的 IPC 机制时，客户端向服务器发送请求。该服务处理该请求并返回响应。

在许多客户端中，请求的线程在等待响应时被阻塞。其他客户端可能会使用异步、事件驱动的客户端代码，这些代码可能是由 [Futures](http://docs.scala-lang.org/overviews/core/futures.html) 或 [Rx Observables](http://reactivex.io/documentation/observable.html) 封装的。然而，与使用消息传递不同，客户端假定响应能及时到达。

有许多协议可供选择。有两种流行协议分别是 REST 和 Thrift。我们先来看一下 REST。

### 3.8.1、REST
如今，开发 [RESTful](https://en.wikipedia.org/wiki/Representational_state_transfer) 风格的 API 是很流行的。REST 是一种使用了 HTTP （几乎总是）的 IPC 机制。

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

引用 REST 的创建者 Roy Fielding：

> “REST 提供了一套架构约束，当作为整体应用时，其强调组件交互的可扩展性、接口的通用性、组件的独立部署以及中间组件，以减少交互延迟、实施安全性和封装传统系统。” - Roy Fielding，[《架构风格与基于网络的软件架构设计》](http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm)

图 3-5 显示了打车应用程序可能使用 REST 的方式之一。

![使用了 RESTful 交互的打车应用]()

**待续……**
