编者:本系列共7篇文章,现已全部发布:
- 微服务简介
- 构建微服务:使用API网关
- 构建微服务:微服务架构中的进程间通信(本文)
- 微服务架构中的服务发现
- 面向微服务的事件驱动型数据管理
- 选择微服务部署策略
- 将单体应用重构为微服务
还可以下载电子书,其中收录了全套文章以及使用NGINX Plus实施微服务的相关信息:《微服务:从设计到部署》。此外,请查看新的微服务解决方案页面。
本文是使用微服务架构构建应用系列文章的第3篇。第1篇文章介绍了微服务架构模式,将其与单体架构模式进行了比较,并探讨了使用微服务的优势和不足。第2篇文章介绍了应用客户端如何通过名为API网关的中介与微服务通信。本文将探讨系统内的服务如何相互通信。第4篇文章探讨了与此密切相关的服务发现问题。
简介
在单体应用中,组件通过语言级的方法调用或函数调用相互调用。相比之下,基于微服务的应用是运行在多台计算机上的分布式系统。每个服务实例通常都是一个进程。因此,如下图所示,服务必须使用进程间通信(IPC)机制进行交互。

稍后我们将介绍具体的IPC技术,但首先来探讨各种设计问题。
交互方式
为服务选择IPC机制时,建议先考虑服务之间的交互方式。客户端⇔服务之间有多种交互方式,可按两个维度进行分类。第一个维度是交互属于一对一还是一对多:
- 一对一:每个客户端请求仅由一个服务实例处理。
- 一对多:每个请求由多个服务实例处理。
第二个维度是交互采用同步方式还是异步方式:
- 同步:客户端期望服务及时响应,甚至可能在等待期间阻塞。
- 异步:客户端在等待响应时不会阻塞,而且即使有响应,也不一定会立即发送。
下表列出了各种交互方式。
| 一对一 | 一对多 | |
|---|---|---|
| 同步 | 请求/响应 | — |
| 异步 | 通知 | 发布/订阅 |
| 请求/异步响应 | 发布/异步响应 |
一对一交互包括以下几种类型:
- 请求/响应:客户端向服务发出请求并等待响应。客户端期望及时收到响应。在基于线程的应用程序中,发出请求的线程甚至可能在等待期间阻塞。
- 通知(也称为单向请求):客户端向服务发送请求,但不期望收到回复,服务也不会发送回复。
- 请求/异步响应:客户端向服务发送请求,服务以异步方式响应。客户端在等待期间不会阻塞,其设计假定响应可能需要一段时间才能到达。
一对多交互包括以下几种类型:
- 发布/订阅:客户端发布一条通知消息,由零个或多个感兴趣的服务使用。
- 发布/异步响应:客户端发布请求消息,然后等待一段时间,以接收相关服务的响应。
每项服务通常会结合使用这些交互方式。对于某些服务,使用一种进程间通信(IPC)机制即可;其他服务则可能需要结合使用多种IPC机制。下图展示了用户请求行程时,网约车应用中的各项服务可能如何交互。

这些服务结合使用通知、请求/响应和发布/订阅模式。例如,乘客的智能手机向Trip Management服务发送通知,请求接载。Trip Management服务通过请求/响应模式调用Passenger Service,以验证乘客的账户是否处于有效状态。随后,Trip Management服务创建行程,并通过发布/订阅模式通知包括Dispatcher在内的其他服务,由Dispatcher查找可接单的司机。
了解了交互方式后,接下来看看如何定义API。
定义API
服务的API是服务与其客户端之间的契约。无论选择哪种进程间通信(IPC)机制,都应使用某种接口定义语言(IDL)来准确定义服务的API。采用API优先方法来定义服务也有充分的理由。开发服务时,首先编写接口定义,并与客户端开发人员共同评审。经过多轮迭代完善API定义后,再实现服务。预先完成这项设计,有助于构建更符合客户端需求的服务。
正如本文后面所述,API定义的具体形式取决于所使用的IPC机制。如果使用消息传递机制,API由消息通道和消息类型组成。如果使用HTTP,API则由URL以及请求和响应格式组成。后文将更详细地介绍几种IDL。
API 演进
服务的API必然会随时间推移而变化。在单体应用中,更改API并更新所有调用方通常比较简单。在基于微服务的应用中,这要困难得多,即使API的所有使用方都是同一应用中的其他服务。通常无法强制所有客户端与服务同步升级。此外,您可能会逐步部署服务的新版本,因此服务的新旧版本会同时运行。制定应对这些问题的策略非常重要。
如何处理API变更取决于变更的规模。有些变更较小,并且向后兼容先前版本。例如,可以向请求或响应添加属性。客户端和服务的设计应遵循健壮性原则。使用旧版API的客户端应能继续与新版服务配合使用。服务会为请求中缺少的属性提供默认值,而客户端会忽略响应中多出的属性。应使用便于API演进的进程间通信(IPC)机制和消息格式。
但有时,必须对API进行不兼容的重大更改。由于无法强制客户端立即升级,服务必须在一段时间内支持旧版API。如果使用REST等基于HTTP的机制,一种方法是将版本号嵌入URL。每个服务实例可以同时处理多个版本。也可以部署不同的实例,每个实例分别处理特定版本。
处理部分故障
以该文章中的产品详情场景为例。假设推荐服务无响应。采用简单实现的客户端可能会无限期阻塞,持续等待响应。这不仅会导致糟糕的用户体验,还会在许多应用中占用线程等宝贵资源。最终,运行时会耗尽线程并停止响应,如下图所示。

为避免此问题,必须将服务设计为能够处理部分故障。
一种值得采用的方法是Netflix所述的方法。处理部分故障的策略包括:
- 网络超时:切勿无限期阻塞,等待响应时应始终设置超时。使用超时机制可确保资源不会被无限期占用。
- 限制未完成请求数:对客户端向特定服务发出的未完成请求数设置上限。如果已达到上限,继续发出请求可能没有意义,这些请求尝试需要立即失败。
- 断路器模式:跟踪成功和失败的请求数量。如果错误率超过配置的阈值,则触发断路器,使后续尝试立即失败。如果大量请求失败,则表明服务不可用,继续发送请求已无意义。超时期限过后,客户端应重试;如果成功,则关闭断路器。
- 提供回退机制:请求失败时执行回退逻辑。例如,返回缓存数据或默认值,如空的推荐结果集。
Netflix Hystrix是一个实现这些模式及其他模式的开源库。如果使用JVM,强烈建议考虑使用Hystrix。如果在非JVM环境中运行,则应使用功能相当的库。
IPC技术
有多种不同的IPC技术可供选择。服务可以使用基于同步请求和响应的通信机制,例如基于HTTP的REST或Thrift;也可以使用基于异步消息的通信机制,例如AMQP或STOMP。消息格式同样多种多样。服务可以使用JSON或XML等人类可读的文本格式,也可以使用Avro或Protocol Buffers等效率更高的二进制格式。稍后我们将介绍同步IPC机制,但首先来讨论异步IPC机制。
异步消息通信
使用消息传递时,进程通过异步交换消息进行通信。客户端通过向服务发送消息来发出请求。如果服务需要回复,则会另行向客户端发送一条消息。由于通信是异步的,客户端不会因等待回复而阻塞。相反,客户端的设计基于不会立即收到回复这一假设。
一条消息由消息头(发送方等元数据)和消息正文组成。消息通过通道进行交换。任意数量的生产者都可以向一个通道发送消息。同样,任意数量的消费者都可以从一个通道接收消息。通道分为两种:点对点和发布-订阅。点对点通道会将消息仅传递给正在从该通道读取消息的其中一个消费者。服务使用点对点通道实现前文所述的一对一交互方式。发布-订阅通道会将每条消息传递给所有已连接的消费者。服务使用发布-订阅通道实现上文所述的一对多交互方式。
下图展示了出租车叫车应用可能如何使用发布-订阅通道。

Trip Management服务通过向发布-订阅通道写入Trip Created消息,将新Trip通知给Dispatcher等相关服务。Dispatcher找到可用司机后,通过向发布-订阅通道写入Driver Proposed消息来通知其他服务。
有许多消息传递系统可供选择。应选择支持多种编程语言的系统。一些消息传递系统支持AMQP和STOMP等标准协议,另一些则使用专有但有文档说明的协议。可供选择的开源消息传递系统有很多,包括RabbitMQ、Apache Kafka、Apache ActiveMQ和NSQ。总体而言,它们都支持某种形式的消息和通道,并致力于实现可靠性、高性能和可扩展性。不过,各消息代理的消息传递模型在具体细节上存在显著差异。
使用消息传递有许多优势:
- 将客户端与服务解耦:客户端只需向相应的通道发送消息即可发出请求。客户端完全无需感知服务实例,也不需要使用服务发现机制来确定服务实例的位置。
- 消息缓冲:使用HTTP等同步请求/响应协议时,客户端和服务在整个交互期间都必须可用。相比之下,消息代理会将写入通道的消息排入队列,直至消费者能够处理这些消息。例如,即使订单履行系统响应缓慢或不可用,在线商店仍可接受客户订单,订单消息只需进入队列等待处理。
- 灵活的客户端与服务交互:消息传递支持前文所述的所有交互方式。
- 显式进程间通信:基于RPC的机制试图让远程服务调用看起来与本地服务调用相同。然而,由于物理定律的限制以及发生部分故障的可能性,两者实际上存在很大差异。消息传递会明确体现这些差异,避免让开发人员产生错误的安全感。
不过,使用消息传递也存在一些缺点:
- 额外的运维复杂性:消息传递系统是另一个必须安装、配置和运行的系统组件。消息代理必须具备高可用性,否则会影响系统可靠性。
- 实现基于请求/响应的交互较为复杂:实现请求/响应式交互需要完成一些额外工作。每条请求消息都必须包含回复通道标识符和关联标识符。服务将包含关联ID的响应消息写入回复通道。客户端使用关联ID将响应与请求进行匹配。使用直接支持请求/响应的IPC机制通常更容易。
我们已经了解了如何使用基于消息传递的IPC,接下来看看基于请求/响应的IPC。
同步请求/响应式IPC
使用基于同步请求/响应的进程间通信(IPC)机制时,客户端向服务发送请求。服务处理该请求并返回响应。在许多客户端中,发出请求的线程会在等待响应期间阻塞。其他客户端可能会使用异步、事件驱动的客户端代码,并可能通过Future或Rx Observable进行封装。但与使用消息传递机制不同,客户端会假定响应能够及时到达。有多种协议可供选择,其中REST和Thrift是两种常用协议。下面先来了解REST。
REST
如今,采用RESTful风格开发API已成为一种趋势。REST是一种(几乎总是)使用HTTP的进程间通信(IPC)机制。REST的一个关键概念是资源,通常表示Customer或Product等业务对象,也可以表示业务对象集合。REST使用HTTP动词来操作通过URL引用的资源。例如,GET请求会返回资源的表示形式,可能是XML文档或JSON对象。POST请求会创建新资源,而PUT请求会更新资源。引用REST创始人Roy Fielding的话:
“REST提供了一组架构约束。整体应用这些约束时,重点在于组件交互的可扩展性、接口的通用性、组件的独立部署,以及通过中间组件降低交互延迟、实施安全控制并封装遗留系统。”
下图展示了叫车应用使用REST的一种可能方式。

乘客的智能手机向Trip Management服务的POST资源/trips发送请求,以申请行程。该服务向Passenger Management服务发送GET请求,获取乘客信息并处理该请求。确认乘客有权创建行程后,Trip Management服务会创建行程,并向智能手机返回201响应。
许多开发者声称其基于HTTP的API符合REST风格。然而,正如Fielding在这篇博客文章中所述,并非所有这些API都真正符合REST风格。Leonard Richardson(与作者并无亲属关系)定义了一个非常实用的REST成熟度模型,其中包括以下级别。
- 级别0:级别0 API的客户端通过向其唯一的URL端点发出HTTP
POST请求来调用服务。每个请求都会指定要执行的操作、操作目标(例如业务对象)以及所有相关参数。 - 级别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等浏览器扩展在浏览器中测试HTTP API,也可以在命令行中使用
curl进行测试(前提是使用JSON或其他文本格式)。 - 它直接支持请求/响应式通信。
- 当然,HTTP对防火墙很友好。
- 无需中间代理,从而简化了系统架构。
使用HTTP存在一些缺点:
- 它仅直接支持请求/响应式交互。可以使用HTTP发送通知,但服务器必须始终发送HTTP响应。
- 由于客户端与服务直接通信(没有中间方缓冲消息),因此在整个交互期间,两者都必须保持运行。
- 客户端必须知道每个服务实例的位置(即URL)。正如上一篇有关API网关的文章所述,这在现代应用中并非易事。客户端必须使用服务发现机制来查找服务实例。
开发者社区最近重新认识到接口定义语言对于RESTful API的价值。目前有多种选择,包括RAML和Swagger。某些IDL(如Swagger)可用于定义请求和响应消息的格式。另一些IDL(如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,可以自行选择消息格式。Thrift等其他进程间通信(IPC)机制可能只支持少数几种消息格式,甚至可能只支持一种。无论哪种情况,使用跨语言消息格式都很重要。即使目前只使用一种语言编写微服务,将来也很可能会使用其他语言。
消息格式主要分为两类:文本格式和二进制格式。基于文本的格式包括JSON和XML。这些格式的优势在于,它们不仅便于人们阅读,还具有自描述性。在JSON中,对象的属性由一组名称-值对表示。同样,在XML中,属性由命名元素和值表示。因此,消息使用方可以提取所需的值,并忽略其余内容。这样一来,对消息格式进行小幅更改时,通常很容易保持向后兼容。
XML文档的结构由XML模式指定。随着时间的推移,开发者社区逐渐认识到,JSON也需要类似的机制。一种选择是使用JSON Schema,既可单独使用,也可作为Swagger等接口定义语言(IDL)的一部分。
使用基于文本的消息格式有一个缺点:消息往往较为冗长,XML尤其如此。由于消息具有自描述性,每条消息除了包含属性值,还包含属性名称。另一个缺点是解析文本会产生额外开销。因此,可以考虑使用二进制格式。
有多种二进制格式可供选择。如果使用Thrift RPC,可以采用二进制Thrift。如果可以自行选择消息格式,常用选项包括Protocol Buffers和Apache Avro。这两种格式都提供类型化接口定义语言(IDL),用于定义消息结构。不过,两者有一个区别:Protocol Buffers使用带标记的字段,而Avro使用方需要了解模式才能解析消息。因此,与Avro相比,Protocol Buffers更便于API演进。这篇博客文章对Thrift、Protocol Buffers和Avro进行了深入比较。
总结
微服务必须使用进程间通信机制进行通信。在设计服务间的通信方式时,需要考虑多个问题:服务如何交互、如何为每项服务定义API、如何演进API,以及如何处理局部故障。微服务可以使用两类进程间通信机制:异步消息传递和同步请求/响应。本系列的下一篇文章将探讨微服务架构中的服务发现问题。
编者:本系列共7篇文章,现已全部发布:
- 微服务简介
- 构建微服务:使用API网关
- 构建微服务:微服务架构中的进程间通信(本文)
- 微服务架构中的服务发现
- 面向微服务的事件驱动型数据管理
- 选择微服务部署策略
- 将单体应用重构为微服务
还可以下载电子书,其中收录了全套文章,以及使用NGINX Plus实施微服务的相关信息:《微服务:从设计到部署》。
客座博主Chris Richardson是早期网站CloudFoundry.com的创始人。该网站是面向Amazon EC2的早期Java平台即服务(PaaS)。目前,他为各组织提供咨询服务,帮助改进应用开发和部署方式。他还定期在http://microservices.io上撰写有关微服务的博客文章。
相关博客文章
NGINX Plus R26 正式发布
NGINX Plus R26通过缓存JSON Web密钥集加快JWT验证,并强化TLS握手,从而提升安全性。NGINX JavaScript的新功能包括增强对异步函数的支持,以及实现WebCrypto API。
使用NGINX App Protect DoS保护gRPC应用,抵御严重的DoS攻击
gRPC具备开发人员设计和部署现代应用所需的速度与灵活性,但其开放性也使其极易受到第7层DoS攻击。NGINX App Protect DoS可保护gRPC应用免受此类破坏性攻击。
Ingress Controller 选型指南(第 2 部分):风险与面向未来的规划
了解选择错误的Ingress控制器可能带来的风险,以及如何从关键方面着手,让您的选择更好地适应未来需求。
NGINX Plus R19 正式发布
NGINX Plus R19扩展了监控功能,不仅提供更多指标,还新增了指标分析方式;增加试运行模式,用于测试限速效果;增强键值存储,以支持IP地址范围;并使限速更加动态。
NGINX现已正式加入F5
NGINX现已正式成为F5的一部分。Gus Robertson现任F5 NGINX业务部门负责人。他阐述了两家公司在人才、文化和愿景方面融合的价值,以及双方将如何携手增强NGINX的开源产品和商业产品。
