8.4 KiB
3、进程间通信
本书的第三章主要是关于使用微服务架构构建应用程序。第一章介绍了微服务架构模式,将其与单体架构模式进行对比,并讨论了使用微服务的优点和缺点。第二章描述了应用程序客户端通过扮演中间人角色的 API 网关与微服务器进行通信。在本章中,我们来了解一下系统中的服务是如何相互通信的。第四章将详细探讨服务发现方面的内容。
3.1、简介
在单体应用程序中,组件可通过语言级方法或者函数相互调用。相比之下,基于微服务的应用程序是一个运行在多台机器上的分布式系统。通常,每个服务实例是一个进程。
因此,如图 3-1 所示,服务必须使用进程间通信(IPC)机制进行交互。
稍后我们将了解到多种 IPC 技术,但在此之前,我们先来探讨一下涉及到的各种设计问题。
3.2、交互方式
当为服务选择一种 IPC 机制时,首先需要考虑服务如何交互。有很多种客户端←→服务端交互方式。它们可以分为两个类。第一类是交互是一对一还是一对多:
- 一对一 - 每个客户端请求都由一个服务实例处理。
- 一对多 - 每个请求由多个服务实例处理。
第二类是交互是同步还是异步:
- 同步 - 客户端要求服务及时响应,在等待过程中可能会发生阻塞。
- 异步 - 客户端在等待响应时不会发生阻塞,但响应(如果有)不一定立即返回。
下表展示了各种交互方式。
| 一对一 | 一对多 | ||
|---|---|---|---|
| 同步 | 请求/相应 | —— | |
| 异步 | 通知 | 发布/订阅 | |
| 异步 | 请求/异步响应 | 发布/异步响应 | |
| 表 3-1、进程间通信方式 |
一对一交互分为以下列举的类型,包括同步(请求/响应)和异步(通知与请求/异步响应):
- 请求/响应 - 客户端向服务发出请求并等待响应。客户端要求响应及时到达。在基于线程的应用程序中,发出请求的线程可能在等待时发生阻塞。
- 通知(a.k.a. 单向请求) - 客户端向服务发送请求,但不要求响应。
- 请求/异步响应 - 客户端向服务发送请求,服务异步响应。客户端在等待时不发生阻止,适用于假设响应可能不会立即到达的场景。
一对多交互可分为以下列举的类型,它们都是异步的:
- 发布/订阅 - 客户端发布通知消息,由零个或多个感兴趣的服务消费。
- 发布/异步响应 - 客户端发布请求消息,然后等待一定时间来接收消费者的响应。
通常,每个服务都组合着使用这些交互方式。对于一些服务,单一的 IPC 机制就足够了,但其他服务可能需要组合多个 IPC 机制。
图 3-2 显示了当用户请求打车时,出租车应用程序中的服务可能会发生交互。
服务使用了通知、请求/响应和发布/订阅组合。例如,乘客的智能手机向 Trip Management 微服务发送通知以请求一辆车。Trip Management 服务通过使用请求/响应来调用 Passenger Management 服务以验证乘客的帐户是可用。之后,Trip Management 服务创建路线,并使用发布/订阅通知其他服务,包括用于定位可用司机的调度程序。
现在我们来看一下交互方式,我们先来看看如何定义 API。
3.3、定义 API
服务 API 是服务与客户端之间的契约。无论您选择何种 IPC 机制,使用接口定义语言(interface definition language,IDL)严格定义服务 API 都是非常有必要的。有论据证明使用 API 优先(API‑first)法定义服务更加合适。在对您需要实现的服务的 API 定义进行迭代之后,您可以通过编写接口定义并与客户端开发人员进行审阅来开始开发服务。这样设计可以增加您构建出符合客户端需求的服务的几率。
正如您将会在后面看到,API 定义的方式取决于您使用的是哪种 IPC 机制。如果您正在使用消息服务,则 API 由消息通道和消息类型组成。如果您使用 HTTP,则 API 由 URL、请求和响应格式组成。稍后我们详细地介绍关于 IDL 方面的内容。
3.4、演化 API
服务的 API 总是随着时间而变化。在单体应用程序中,更改 API 和更新所有调用者通常都是直截了当的。但在基于微服务的应用程序中,即使您的 API 的所有消费者都是同一应用程序中的其他服务,要想完成这些工作也是非常困难的。通常,您无法强制所有客户端与服务升级的节奏一致。此外,您可能会逐步部署新版本的服务,以便新旧版本的服务同时运行。制定这些问题的处理策略是很重要的。
处理 API 变更的方式取决于变更的程度。某些更改是次要,需要向后兼容以前的版本。例如,您可能会向请求或响应添加属性。设计客户与服务时遵守鲁棒性原则是很有意义的。使用较旧 API 的客户端应继续使用新版本的服务。该服务为缺少的请求属性提供默认值,并且客户端忽略任何多余的响应属性。使用 IPC 机制和消息格式非常重要,可以让您轻松地演化 API。
但有时候,您必须对 API 作出重大不兼容的更改。由于您无法强制客户端立即升级,服务也必须支持较旧版本的 API 一段时间。如果您使用了基于 HTTP 的机制(如 REST),则一种方法是将版本号嵌入 URL 中。每个服务实例可能同时处理多个版本。或者,您可以部署每个用于处理特定版本的不同实例。
3.5、处理部分故障
正如第二章中关于 API 网关所述,在分布式系统中存在着部分故障风险。由于客户端进程和服务进程是分开的,服务可能无法及时响应客户端的请求。由于故障或者维护,服务可能会关闭。也有可能因服务过载,响应速度变得极慢。
例如,请考虑第二章中的产品详细信息场景。我们假设 Recommendation Service 没有反应。客户端天真的实现可能会无限期地阻塞以等待响应。这不仅会导致用户体验糟糕,而且在许多应用程序中,它将消耗诸如线程等宝贵资源。以致最终,运行时将线程用完,造成无法响应,如图 3-3 所示。
为了防止这个问题出现,您必须设计您的服务来处理部分故障。以下是一个由 Netflix 给出的一个好方法。处理部分故障的策略包括:
-
网络超时 - 在等待响应时,不要无限期地阻塞,始终使用超时方案。使用超时方案确保资源不被无限地消耗。
-
限制未完成的请求数量 - 对客户端拥有特定服务的未完成请求的数量设置上限。如果达到了上限,则发出的额外请求可能是毫无意义的,这些尝试需要立即失败。
-
断路器模式 - 追踪成功和失败请求的数量。如果错误率超过配置的阈值,则断开断路器,以便后续的尝试能立即失败。如果大量请求失败,则表明服务不可用,发送请求将是无意义的。发生超时后,客户端应重新尝试,如果成功,则关闭断路器。
-
提供回退 - 请求失败时执行回退逻辑。例如,返回缓存数据或者默认值,比如一组空的推荐。
Netflix Hystrix 是一个实现上述和其他模式的开源库。如果您正在使用 JVM,那么您一定要考虑使用 Hystrix。如果您在非 JVM 环境中运行,则应使用相等作用的库。
待续……


