微服务之间的通信

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

微服务之间的通信

交互方式

为服务选择IPC机制时,首先考虑服务如何交互是必要的。 客户端⇔服务交互方式多种多样。 它们可以沿两个维度进行分类。 第一个维度是互动是一对一还是一对多:

  • 一对一–每个客户端请求仅由一个服务实例处理。
  • 一对多–每个请求由多个服务实例处理。

第二个维度是交互是同步还是异步:

  • 同步–客户端期望服务及时响应,甚至在等待时可能会阻塞。
  • 异步-客户端在等待响应时不会阻塞,并且响应(如果有的话)不一定会立即发送。

下表显示了各种交互样式。

One-to-One One-to-Many
Synchronous Request/response  — 
Asynchronous Notification Publish/subscribe
Request/async response Publish/async responses

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

  • Request/response–客户端向服务发出请求并等待响应。客户希望响应能够及时到达。在基于线程的应用程序中,发出请求的线程甚至可能在等待时阻塞。
  • Notification (也称为单向one‑way 请求)–客户端向服务发送请求,但不希望或未发送答复。
  • Request/async response–客户端将请求发送到服务,该服务以异步方式答复。客户端在等待时不会阻塞,并假设响应可能不会在一段时间内到达。

一对多互动有以下几种:

  • Publish/subscribe–客户端发布通知消息,该消息由零个或更多感兴趣的服务使用。
  • Publish/async responses –客户端发布请求消息,然后等待一定时间以等待感兴趣的服务的响应。

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

微服务之间的通信

服务使用notifications, request/response, 和publish/subscribe的组合。 例如,乘客的智能手机向行程管理服务发送通知以请求接机。 行程管理服务通过使用请求/响应调用旅客服务来验证旅客的帐户是否处于活动状态。 然后,行程管理服务创建行程,并使用发布/订阅来通知其他服务,包括分派器,后者查找可用的驱动程序。

现在我们已经研究了交互样式,让我们看一下如何定义API。

定义 API

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

正如您将在本文后面看到的那样,API定义的性质取决于您所使用的IPC机制。 如果使用messaging消息服务,则API包含消息通道和消息类型。 如果使用的是HTTP,则API由URL以及请求和响应格式组成。

调用API

服务的API始终会随着时间而变化。在整体应用程序中,更改API和更新所有调用程序通常很简单。在基于微服务的应用程序中,即使API的所有使用者都是同一应用程序中的其他服务,也要困难得多。通常,您无法强制所有客户端与服务同步升级。另外,您可能会逐步部署服务的新版本,以便服务的旧版本和新版本将同时运行。制定处理这些问题的策略很重要。

处理API更改的方式取决于更改的大小。某些更改是次要的,并且与以前的版本向后兼容。例如,您可以将属性添加到请求或响应。设计客户和服务以使其遵循健壮性原则是有意义的。使用较旧API的客户端应继续使用新版本的服务。该服务为缺少的请求属性提供默认值,客户端忽略任何额外的响应属性。重要的是要使用IPC机制和消息传递格式,使您能够轻松地开发API。

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

处理部分失败

如有关API gateway的文章中我们提到了,在分布式系统中,存在着永远存在的部分故障风险。 由于客户和服务是独立的流程,因此服务可能无法及时响应客户的请求。 服务可能由于故障或维护而关闭。 否则服务可能过载,并且对请求的响应非常缓慢。

例如,考虑该文章中的“产品详细信息”方案。 假设推荐服务没有响应。 客户端的实现可能会无限期地阻塞等待响应。 这不仅会导致不良的用户体验,而且在许多应用程序中会消耗宝贵的资源,例如线程。 最终,运行时将耗尽线程并变得无响应,如下图所示。

微服务之间的通信

为避免此问题,必须设计服务以处理部分故障。

Netflix所描述的是一种很好的遵循方法。处理部分故障的策略包括:

  • 网络超时–永远不会无限阻塞,并且在等待响应时始终使用超时。使用超时可确保资源不会无限期地被占用。
  • 限制未完成请求的数量–限制客户端可以使用特定服务的未完成请求的数量。如果已达到限制,则发出其他请求可能毫无意义,并且这些尝试必须立即失败。
  • 断路器模式–跟踪成功和失败请求的数量。如果错误率超过配置的阈值,请使断路器跳闸,以便进一步尝试立即失败。如果大量请求失败,则表明该服务不可用,并且发送请求毫无意义。超时后,客户端应重试,如果成功,则合上断路器。
  • 提供回退–当请求失败时执行回退逻辑。例如,返回缓存的数据或默认值,例如空的建议集。

Netflix Hystrix是实现这些模式和其他模式的开源库。如果您使用的是JVM,则绝对应该考虑使用Hystrix。而且,如果您在非JVM环境中运行,则应使用等效的库。

IPC Technologies

有很多不同的IPC技术可供选择。服务可以使用基于请求/响应的同步通信机制,例如基于HTTP的REST或Thrift。或者,他们可以使用基于消息的异步通信机制,例如AMQP或STOMP。还有各种不同的消息格式。服务可以使用人类可读的,基于文本的格式,例如JSON或XML。或者,他们可以使用二进制格式(效率更高),例如Avro或Protocol Buffers。稍后,我们将讨论同步IPC机制,但首先让我们讨论异步IPC机制。

基于消息的异步通信

使用消息传递时,进程通过异步交换消息进行通信。客户端通过向其发送消息来向服务发出请求。如果期望该服务进行答复,则通过将单独的消息发送回客户端来进行答复。由于通信是异步的,因此客户端不会阻止等待答复。而是编写客户端,假定不会立即收到答复。

一条消息由标头(例如发送方之类的元数据)和一条消息主体组成。消息通过通道交换。任何数量的生产者都可以将消息发送到一个频道。同样,任何数量的使用者都可以从频道接收消息。有两种渠道,点对点和发布-订阅。点对点通道将消息传递给正从该通道读取的消费者中的一个。服务使用点对点渠道进行前面所述的一对一交互样式。发布订阅通道将每个消息传递给所有附加的使用者。服务将发布-订阅通道用于上述一对多交互样式。

下图显示了计程车应用程序如何使用发布-订阅通道。

微服务之间的通信

行程管理服务通过将“行程创建”消息写入发布-订阅频道来通知感兴趣的服务(例如,调度程序)有关新行程。分派器通过将“驱动程序建议”消息写入发布-订阅通道来找到可用的驱动程序并通知其他服务。

有许多消息传递系统可供选择。您应该选择一种支持多种编程语言的语言。一些消息传递系统支持标准协议,例如AMQP和STOMP。其他消息传递系统具有专有但已记录的协议。有很多开源消息传递系统可供选择,包括RabbitMQ,Apache Kafka,Apache ActiveMQ和NSQ。在较高级别上,它们都支持某种形式的消息和通道。他们都努力做到可靠,高性能和可扩展。但是,每个经纪人的消息传递模型的详细信息存在很大差异。

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

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

但是,使用消息传递有一些缺点:

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

现在,我们已经研究了使用基于消息的IPC,下面我们来研究基于请求/响应的IPC。

Synchronous, Request/Response IPC 请求/响应的同步IPC

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

REST
如今,以RESTful风格开发API已成为一种时尚。 REST是(几乎始终)使用HTTP的IPC机制。 REST中的一个关键概念是一种资源,通常代表一个业务对象,例如客户或产品,或业务对象的集合。 REST使用HTTP谓词来操纵资源,这些谓词是使用URL引用的。例如,GET请求返回资源的表示形式,该形式可以是XML文档或JSON对象的形式。 POST请求创建一个新资源,而PUT请求更新一个资源。

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

微服务之间的通信

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

许多开发人员声称他们基于HTTP的API是RESTful的。但是,正如Fielding在此博客文章中所描述的那样,实际上并非全部。 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 API。
  • 它直接支持请求/响应样式的通信。
  • HTTP当然是防火墙友好的。
  • 它不需要中间代理,从而简化了系统的体系结构。

使用HTTP有一些缺点:

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

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

Message Formats消息格式

现在,我们已经研究了HTTP和Thrift,现在让我们检查消息格式的问题。如果您使用的是消息传递系统或REST,则可以选择消息格式。其他IPC机制(例如Thrift)可能仅支持少量消息格式,也许仅支持一种。无论哪种情况,都必须使用跨语言消息格式。即使您今天用一种语言编写微服务,将来也可能会使用其他语言。

消息格式主要有两种:文本和二进制。基于文本的格式的示例包括JSON和XML。这些格式的优点是它们不仅易于阅读,而且是自描述的。在JSON中,对象的属性由名称/值对的集合表示。同样,在XML中,属性由命名的元素和值表示。这使消息的使用者可以选择其感兴趣的值,而忽略其余值。因此,对消息格式的微小更改可以轻松地向后兼容。

XML文档的结构由XML模式指定。随着时间的流逝,开发人员社区逐渐意识到JSON也需要类似的机制。一种选择是使用JSON Schema,它既可以独立使用,也可以作为IDL(例如Swagger)的一部分使用。

使用基于文本的消息格式的缺点是消息往往很冗长,尤其是XML。因为消息是自描述的,所以每条消息除其值外还包含属性的名称。另一个缺点是解析文本的开销。因此,您可能要考虑使用二进制格式。

总结

微服务必须使用进程间通信机制进行通信。 在设计服务如何通信时,您需要考虑各种问题:服务如何交互,如何为每个服务指定API,如何扩展API以及如何处理部分故障。 微服务可以使用两种IPC机制:异步消息传递和同步请求/响应。