0103-微服务架构中的进程间通信

一、概述

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

  

二、交互样式

2.1、两个维度进行分类

  在为服务选择IPC机制时,首先考虑服务如何交互很有用。有各种各样的客户端⇔服务交互样式。它们可以按照两个维度进行分类。

  第一个纬度是交互是一对一还是一对多:

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

    一对多 :每个请求由多个服务实例处理。

  第二个纬度面是交互是同步的还是异步的:

    同步:客户希望服务能够及时响应,甚至可能在等待时阻止

    异步:客户端在等待响应时不会阻塞,并且响应(如果有)不一定立即发送。

2.2、交互样式组合

    

  一对一一对多
同步 请求/响应  — 
异步 通知 发布/订阅
请求/异步响应 发布/异步响应

2.2.1、一对一的交互模式有以下几种方式:

  请求/响应 - 客户端向服务发出请求并等待响应。客户期望及时到达响应。在基于线程的应用程序中,发出请求的线程甚至可能在等待时阻塞。

  通知(又名单向请求) - 客户端向服务器发送请求,但预期或未发送回复。

  请求/异步响应 - 客户端将请求发送给异步回复的服务。客户在等待时不会阻塞,并且假定响应可能不会到达一段时间。

2.2.2、一对多交互:

  发布/订阅 - 客户发布通知消息,该消息由零个或多个感兴趣的服务消耗。

  发布/异步响应 - 客户端发布请求消息,然后等待一段时间,以便从感兴趣的服务响应。

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

     

  这些服务使用通知,请求/响应和发布/订阅的组合。 例如,乘客的智能手机向Trip Management服务发送通知以请求取件。 旅行管理服务通过使用请求/响应来调用乘客服务来验证乘客的账户是否有效。 旅程管理服务然后创建旅程,并使用发布/订阅通知其他服务,包括查找可用驱动程序的调度程序。

  现在我们已经研究了交互风格,让我们来看看如何定义API。 

三、API

3.1、定义API

  服务的API是服务与客户端之间的契约。 无论您选择何种IPC机制,使用某种接口定义语言(IDL)精确定义服务的API都很重要。 使用API优先方法来定义服务甚至有很好的理由。 您通过编写接口定义开始开发服务,并与客户端开发人员一起审查。 只有在API定义迭代后才能实现该服务。 事先做好这项设计可以增加建立满足客户需求的服务的机会。
   正如你将在本文后面看到的那样,API定义的性质取决于你正在使用的IPC机制。 如果您使用消息传递,则API由消息通道和消息类型组成。 如果您使用HTTP,则API由URL和请求和响应格式组成。 稍后我们将更详细地介绍一些IDL。

3.2、不断演变的API

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

  如何处理API更改取决于更改的大小。一些更改是轻微的,并向后兼容以前的版本。例如,您可能会将属性添加到请求或响应中。设计客户和服务以便他们遵守健壮性原则是有道理的。使用旧API的客户端应继续使用新版本的服务。该服务为缺少的请求属性提供默认值,并且客户端忽略任何额外的响应属性。使用IPC机制和消息传递格式很重要,这些机制和消息传递格式可以使您轻松地发展您的API。

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

3.3、处理部分故障

  正如前面关于API网关的文章所述,在分布式系统中,存在部分故障的风险。 由于客户和服务是独立的流程,因此服务可能无法及时响应客户的请求。 服务可能因故障或维护而关闭。 或者服务可能超载并且对请求响应速度极慢。

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

     

  为了防止出现这个问题,设计服务来处理部分故障至关重要。

  Netflix描述的方法很好。处理部分故障的策略包括:

    网络超时 - 永远不要无限期地阻塞,并且在等待响应时总是使用超时。使用超时确保资源不会无限期地捆绑在一起。

    限制未完成请求的数量 - 强加客户端可以使用特定服务的未完成请求数量的上限。如果已达到限制,则发出额外请求可能毫无意义,并且这些尝试需要立即失败。

    断路器模式 - 跟踪成功和失败请求的数量。如果错误率超过配置的阈值,则跳闸断路器,以便进一步尝试立即失败。如果大量请求失败,则表明该服务不可用,并且发送请求毫无意义。超时后,客户应再试一次,如果成功,请关闭断路器。

    提供回退 - 请求失败时执行回退逻辑。例如,返回缓存数据或默认值,如空集建议。

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

四、IPC技术

4.1、IPC技术  

  有许多不同的IPC技术可供选择。

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

4.2、异步,基于消息的通信【基于消息传递的IPC】

  当使用消息传递时,进程通过异步交换消息进行通信。客户通过发送消息向服务发出请求。如果服务预计会回复,则通过向客户端发送单独的消息来实现。由于通信是异步的,客户端不会阻止等待回复。相反,客户端的编写假定不会立即收到答复。

  消息由头部(发送者等元数据)和消息主体组成。 通过频道交换消息。 任何数量的生产者都可以将消息发送到一个渠道。 同样,任何数量的消费者都可以接收来自频道的消息。 有两种渠道,点对点和发布 - 订阅。 点对点通道向正在从该通道读取的一个消费者传递消息。 服务使用点对点渠道来实现前面描述的一对一交互风格。 发布 - 订阅频道将每条消息传递给所有附加的消费者。 服务使用发布 - 订阅频道来获得上述的一对多互动风格。

  下图显示出租车应用程序如何使用发布 - 订阅频道。

    

  旅程管理服务通过向发布 - 订阅频道写入旅行创建消息来通知感兴趣的服务,例如调度员关于新旅程。 Dispatcher通过将驱动程序建议消息写入发布 - 订阅通道来查找可用的驱动程序并通知其他服务。

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

  使用消息传递具有许多优点:

    将客户端与服务分离 - 客户端通过简单地向相应的通道发送消息来发出请求。客户端完全不知道服务实例。它不需要使用发现机制来确定服务实例的位置。

    消息缓冲 - 使用同步请求/响应协议(例如HTTP),客户端和服务在交换期间都必须可用。相反,消息代理将写入通道的消息排队,直到消费者可以处理它们。这意味着,例如,即使订单履行系统缓慢或无法使用,在线商店也可以接受来自客户的订单。订单消息只是排队。

    灵活的客户服务交互 - 消息传递支持前面描述的所有交互风格。

    显式进程间通信 - 基于RPC的机制试图调用远程服务看起来与调用本地服务相同。但是,由于物理规律和部分失效的可能性,它们实际上完全不同。消息传递使这些差异非常明确,因此开发人员不会陷入虚假的安全感。

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

    额外的操作复杂性 - 消息传递系统是另一个必须安装,配置和运行的系统组件。 消息代理的高可用性至关重要,否则系统可靠性受到影响。

    实现基于请求/响应的交互的复杂性 - 请求/响应式交互需要一些工作来实现。 每个请求消息必须包含回复通道标识符和相关标识符。 该服务将包含关联ID的响应消息写入回复通道。 客户端使用关联ID将响应与请求进行匹配。 使用直接支持请求/响应的IPC机制通常更容易。

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

4.3、基于请求/响应的同步IPC

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

4.3.1、REST

  当前很流行开发 RESTful 风格的 API。REST 基于 HTTP 协议,其核心概念是资源典型地代表单一业务对象或者一组业务对象,业务对象包括“消费者”或“产品”。REST 使用 HTTP 协议来控制资源,通过 URL 实现。譬如,GET 请求会返回一个资源的包含信息,可能是 XML 文档或 JSON 对象格式。POST 请求会创建新资源,而 PUT 请求则会更新资源。REST 之父 Roy Fielding 曾经说过:

  “REST提供了一套体系结构约束,在整体应用时强调组件交互的可伸缩性,接口的通用性,组件的独立部署和中间组件,以减少交互延迟,强化安全性并封装遗留系统。”

    — Fielding, Architectural Styles and the Design of Network-based Software Architectures
  下图展示了打车软件如何使用 REST。

     

  乘客通过移动端向行程管理服务的 /trips 资源提交了一个 POST请求。行程管理服务收到请求之后,会发送一个 GET 请求到乘客管理服务以获取乘客信息。当确认乘客信息之后,随即创建一个行程,并向移动端返回 201 响应。

  很多开发者都表示他们基于 HTTP 的 API 是 RESTful 风格。但是,如同 Fielding 在他的博客中所说,并非所有这些 API 都是 RESTful。Leonard Richardson(注:与本文作者 Chris 无任何关系)为 REST 定义了一个成熟度模型,具体包含以下四个层次:

  • Level 0:本层级的 Web 服务只是使用 HTTP 作为传输方式,实际上只是远程方法调用(RPC)的一种具体形式。SOAP 和 XML-RPC 都属于此类。
  • Level 1:Level 1 层级的 API 引入了资源的概念。要执行对资源的操作,客户端发出指定要执行的操作和任何参数的 POST 请求。
  • Level 2:Level 2 层级的 API 使用 HTTP 语法来执行操作,譬如 GET 表示获取、POST 表示创建、PUT 表示更新。如有必要,请求参数和主体指定操作的参数。这能够让服务影响 web 基础设施服务,如缓存 GET 请求。
  • Level 3:Level 3 层级的 API 基于 HATEOAS(Hypertext As The Engine Of Application State)原则设计,基本思想是在由 GET请求返回的资源信息中包含链接,这些链接能够执行该资源允许的操作。例如,客户端通过订单资源中包含的链接取消某一订单,GET 请求被发送去获取该订单。HATEOAS 的优点包括无需在客户端代码中写入硬链接的 URL。此外,由于资源信息中包含可允许操作的链接,客户端无需猜测在资源的当前状态下执行何种操作。

  使用基于 HTTP 的协议有如下好处:

  • HTTP 非常简单并且大家都很熟悉。
  • 可以使用浏览器扩展(比如 Postman)或者 curl 之类的命令行来测试 API。
  • 内置支持请求/响应模式的通信。
  • HTTP 对防火墙友好。
  • 不需要中间代理,简化了系统架构。

  不足之处包括:

  • 只支持请求/响应模式交互。尽管可以使用 HTTP 通知,但是服务端必须一直发送 HTTP 响应。
  • 由于客户端和服务端直接通信(没有代理或者缓冲机制),在交互期间必须都保持在线。
  • 客户端必须知道每个服务实例的 URL。如前篇文章“API 网关”所述,这也是个烦人的问题。客户端必须使用服务实例发现机制。

  开发者社区最近重新认识到了 RESTful API 接口定义语言的价值,于是诞生了包括 RAML 和 Swagger 在内的服务框架。Swagger 这样的 IDL 允许定义请求和响应消息的格式,而 RAML 允许使用 JSON Schema 这种独立的规范。对于描述 API,IDL 通常都有工具从接口定义中生成客户端存根和服务端框架。

 4.3.2、Thrift

  Apache Thrift 是一个很有趣的 REST 的替代品,实现了多语言 RPC 客户端和服务端调用。Thrift 提供了一个 C 风格的 IDL 定义 API。通过 Thrift 编译器能够生成客户端存根和服务端框架。编译器可以生成多种语言的代码,包括 C++、Java、Python、PHP、Ruby, Erlang 和 Node.js。

  Thrift 接口由一个或多个服务组成,服务定义与 Java 接口类似,是一组强类型方法的集合。Thrift 能够返回(可能无效)值,也可以被定义为单向。返回值的方法能够实现交互的请求/响应模式。客户端等待响应,可能会抛出异常。单向方法与交互的通知模式相对应。服务端不会发送响应。

  Thrift 支持 JSON、二进制和压缩二进制等多种消息格式。由于解码更快,二进制比 JSON 更高效;如名称所称,压缩二进制格式可以提供更高级别的压缩效率;同时 JSON 则易读。Thrift 也能够让你选择传输协议,包括原始 TCP 和 HTTP。原始 TCP 比 HTTP 更高效,然而 HTTP 对于防火墙、浏览器和使用者来说更友好。

4.4、消息格式

  了解 HTTP 和 Thrift 后,我们要考虑消息格式的问题。如果使用消息系统或者 REST,就需要选择消息格式。像 Thrift 这样的 IPC 机制可能只支持少量消息格式,或许只支持一种格式。无论哪种情况,使用跨语言的消息格式非常重要。即便你现在使用单一语言实现微服务,但很有可能未来需要用到其它语言。

  目前有文本和二进制这两种主要的消息格式。文本格式包括 JSON 和 XML。这种格式的优点在于不仅可读,而且是自描述的。在 JSON 中,对象的属性是名称-值对的集合。与此类似,在 XML 中,属性则表示为命名的元素和值。消费者能够从中选择感兴趣的值同时忽略其它部分。相应地,对消息格式的小幅度修改也能容易地向后兼容。

  XML 的文档结构由 XML schema 定义。随着时间发展,开发者社区意识到 JSON 也需要一个类似的机制。方法之一是使用 JSON Schema,要么独立使用,要么作为 Swagger 这类 IDL 的一部分。

  文本消息格式的一大缺点是消息会变得冗长,特别是 XML。由于消息是自描述的,所以每个消息都包含属性和值。另外一个缺点是解析文本的负担过大。所以,你可能需要考虑使用二进制格式。

  二进制的格式也有很多。如果使用的是 Thrift RPC,那可以使用二进制 Thrift。如果选择消息格式,常用的还包括 Protocol Buffers 和 Apache Avro,二者都提供类型 IDL 来定义消息结构。差异之处在于 Protocol Buffers 使用添加标记的字段(tagged fields),而 Avro 消费者需要了解模式来解析消息。

总结

微服务必须使用进程间通信机制来交互。在设计服务的通信模式时,你需要考虑几个问题:服务如何交互,每个服务如何标识 API,如何升级 API,以及如何处理局部失败。微服务架构异步消息机制和同步请求/响应机制这两类 IPC 机制可用。在下一篇文章中,我们将会讨论微服务架构中的服务发现问题。

 原文地址:

https://www.nginx.com/blog/service-discovery-in-a-microservices-architecture/

http://blog.daocloud.io/microservices-3/

posted @ 2018-04-11 14:57  bjlhx15  阅读(241)  评论(0编辑  收藏  举报
Copyright ©2011~2020 JD-李宏旭