3.跨进程通信@微服务的设计和实现

本文探讨了微服务架构下服务间的通信机制,包括同步请求/响应及异步消息传递方式。介绍了不同交互模式,如一对一和一对多,同步和异步,并讨论了如何定义API以及API的演化策略。

3. 跨进程通信(3 Inter-Process Communication)

这是本书关于用微服务架构构建应用程序的第三章。 第一张介绍了微服务的架构模式,将微服务架构模式和单体架构模式对比,并讨论了使用微服务架构模式的优点和缺点。第2章描述了一个应用的客户端如何通过众所周知的中介API网关(API Gateway), 同微服务通信。在本章,我们将看看系统内的服务如何同其他服务之间进行通信。第4章将深入探讨服务发现相关的问题。

介绍(Introduction)

在一个单体应用中,组件之间的调用时通过编程语言层次的方法或函数调用实现的。相比之下,基于微服务的应用时运行在多台机器上的分布式系统。每个微服务实例通常来说就是一个进程。

因此,如图3-1所示,服务必须使用跨进程通信(IPC:inter-process communication)机制进行交互。

稍后我们将看一下特定的IPC技术,但首先让我们探讨一下各种设计问题。
在这里插入图片描述

交互样式(Interaction Styles)

当为服务选择一个跨线程通信机制时,首先思考服务之间如何交互是很有用的。有各种客户端和服务段之间的交互方式。可以从两个维度对他们进行划分。第一个维度是,这个交互是否是一对一的或者是一对多的:

  • 一对一: 每个客户端的请求只被一个服务实例处理。
  • 一对多:每个请求被多个服务实例处理。

第二个维度是看交互是同步的(synchronous)还是异步的(asynchronous):

  • 同步: 客户端期待一个来自服务的实时响应,在他等待的过程中可能会阻塞。
  • 异步: 客户端在等待响应的时候并不会阻塞,并且,如果有响应的话,也没有必要立即发送。

下面的表格显示了各种各样的交互方式:

在这里插入图片描述

有以下几种一对一交互。同步(请求/响应)和异步(通知和请求/异步响应):

  • Request/Response 一个client发送一个请求到一个服务,然后等待一个响应。client期待一个实时的方式响应。在一个基于线程的应用中,这个发送请求的线程在等待的过程中甚至会阻塞。
  • Notification(又称为:one-way request): 客户端向服务发送请求,但不期望或未发送应答。
  • Request/async response : 客户端发送一个请求到一个服务,响应是异步的,客户端在等待的过程中并不会阻塞,并且基于响应可能不会短时间内到达的假设设计的。

这儿有下面几种一对多的交互,他们都是异步的:

  • Publish/subscribe : 一个客户端发送一个通知消息(notification message),这个消息被一个活多个感兴趣的服务所消费。
  • Publish/async resonses: 一个客户端发送一个请求消(request message),然后等待一段时间,等待来自于感兴趣的服务的响应。

没一个服务通常使用这些交互方式的组合。对于一些服务,一个简单的跨进程通信机制是足够的。其他服务可能需要使用跨通信机制的联合。

图3-2显示了一个出租车租赁应用程序中,当用户请求出行时,服务可能的交互方式。

在这里插入图片描述

这些服务使用了 notifications, request/resonse, 和 publish/subscribe 的联合。例如,乘客的手机发送一个通知到出行管理服务,请求接送自己。出行管理服务验证乘客的账户是否是激活状态,通过使用 request/response 的方式调用乘客管理服务。出行管理服务然后创建一个出行,然后使用 public/subscribe 的方式通知其他服务,这些服务包括调度台(Dispatcher), 调度台用于定位一个可用的司机。

现在我们来看一下交互方式,让我们看一下如何定义APIs.

定义API(Defining APIs)

一个服务的API是服务和它的客户端之间的契约。无论你选择哪一种IPC机制,使用一些接口定义语言(interface definition language)精确地定义一个服务的API是很重要的。使用API线性的方法来定义服务甚至是一个好的理由。你开始开发一个服务,写接口的定义,然后和客户端的开发人员审查定义的接口。只有在经过基础API定义的迭代之后,你才实现这个服务。以这种方式设计,增加了你构建出符合客户端需求的服务的机会。

就像你在本文章后面看到的那样,API定义的性质取决于您使用的IPC机制。如果你使用的是消息,API包含的是消息通道和消息类型。如果你使用的是HTTP, API包含的是URLs和请求和响应的格式。稍后我们将更加详细地描述一些接口定义语言。

逐渐发展的APIs(Evolving APIs)

一个服务的API总是随着时间变化的。在一个单体应用中,通常是直接修改API, 并更新所有的调用者。在基于微服务的应用中,情况将困难得多,即使你API的所有消费者都是同一个应用中的其他服务。你通常不能够强制所有的客户端步伐一致地升级服务。同样,你将会迭代开发新的服务版本。因此老的服务版本和新的服务版本将会同时运行。找到一种处理这些问题的策略很重要。

如何处理API的变更依赖于变更的大小。一些改动是小量的,并且是向后兼容之前的版本。你可能,例如,在请求或响应中添加属性。设计客户和服务以使它们遵守健壮性原则是有意义的。客户端继续使用老的API在新版本的服务上工作。这些服务为缺少属性的请求提供了默认值,并且客户端忽略任何响应里面多余的属性。使用跨进程通信非常重要,一种消息格式允许你能够轻松的迭代你的APIs。

有时,然而,你必须在API上做出大量不兼容的更改。由于你不能强制客户端立即升级。一个服务必须在一段时间内支持老的版本API。如果你使用一个基于HTTP通信的机制,例如REST。一种方法是在URL中嵌入版本号。每个服务实例可能会同时处理多个版本。或者,你可以部署不同的实例,每个处理一个特定的版本。

处理局部失败(Handling Partial Failure)

就像在章节2中提到的API网关,在分布式系统中,始终存在着部分失败的风险。由于客户端和服务是分别处理的,一个服务可能不能够以及时响应的方式响应一个客户端的请求。一个服务可能会因为失败或者维护的原因关掉。或者服务可能超载,对请求的响应非常慢。

考虑一下, 例如,第2章中产品详情的场景,我们假设推荐服务不可用了。客户端一个天真的实现可能是无期限第等待一个响应。这不仅会导致很差的用户体验,同样,在许多应用中,他可能消耗珍贵的资源,比如线程。最终运行时将耗尽线程,变得无响应,如图3-3所示。

在这里插入图片描述

为了防止这个问题,至关重要的是,你设计你的服务来处理部分失败。一个好的方法就是按照Netflix描述的方法。处理部分失败的方法包括:

  • 网络超时: 在等待一个响应的时候永远不要做无限制的阻塞,而是一直使用超时等待。使用超时等待确保资源永远不会被无限期地占用。
  • 限制外部请求的数量:对客户端可以使用特定服务的未完成请求数量施加上限。如果已经达到了限制,那么再提出额外的请求可能就没有意义了,这些尝试需要立即失败。
  • 断路器模式:跟踪请求成功和失败的数量。如果错误率超过了配置的阈值,断路器跳闸,所以后面的尝试将会立即失败。如果大量请求失败,这表明该服务不可用,发送请求是无意义的。超时后,客户端应再次尝试,如果成功,关闭断路器。
  • 提供备份:当一个请求失败的时候进行回调逻辑。例如,返回缓存的数据或者一个默认值,例如一个空的推荐集合。

Netflix Hystrix 是一个开源的代码库,这个库实现了这些和其他模式。如果你使用JVM, 你绝对应该考虑使用Hystrix。并且,如果你运行在一个非JVM环境,你应该使用一个等价的库。

跨进程通信技术(IPC Technologies)

有很多不同的IPC技术供选择。服务可以使用基于同步的请求/响应通信机制(synchronous request/response-based communication),例如基于 HTTP的REST或Thrift。 另外,他们可以使用异步,基于消息通信的机制,比如 AMQP 或 STOMP。

还有各种不同的消息格式。服务可以使用人类可读的,基于文本格式的,比如JSON或XML。另外,他们可以使用二进制格式(二进制格式会更高效),例如 Avro 或 Protocol Buffers, 后面我们将看一下同步的跨进程通信机制(synchronous IPC mechanisms), 但首先让我们讨论一下异步的跨进程通信机制。

异步的,基于消息的通信(Asynchronous, Message-Based Communication)

在使用消息传递时,流程通过异步交换消息进行通信。客户端通过发送消息向服务发出请求。如果期望服务进行应答,它将通过向客户机发送单独的消息来完成应答。由于通信是异步的,客户端不会因为等待应答而阻塞。相反,客户在写消息时假设不会立即收到回复。

一个消息包含headers(元数据,比如生产者)和一个message body. messages 通过 channels 进行交换。任意数量的生产者(producers)可以发送消息到channel。类似的,任意数量的消费者(consumers)可以从channel接收 messages. 有两种类型的 channels, 点对点(point-to-point)和发布-订阅(publish-subscribe):

  • 一个点到点通道(point-to-point channel)将消息准确地传递给从通道读取的消费者之一。服务使用点对点通道来实现前面描述的一对一交互样式。
  • 一个发布订阅通道(publish-subscribe channel)发布-订阅通道将每个消息传递给所有绑定的的消费者(consumers)。对于上面描述的一对多交互样式,服务使用发布-订阅通道(publish-subscribe channels)。

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

在这里插入图片描述

行程管理服务通过将行程创建的消息写入发布-订阅通道,通知感兴趣的服务(如Dispatcher)有新的出行。Dispatcher发现一个可用的司机,并通过向发布-订阅通道编写司机已派遣的消息来通知其他服务。

有很多消息系统可以选择。你应当选择一个支持各种各样编程语言的消息系统。

一些消息系统支持类似 AMQP 和 STOMP 的标准协议。其他消息传递系统有专有的但有文档记录的协议。

有大量的开源的消息系统供选择,包括 RabbitMQ, Apache Kafka, Apache ActiveMQ, 和 NSQ. 在更高层面上,他们都支持一些格式的消息(messsages)和通道(channels)。他们都努力做到可靠。高性能和可伸缩的。然而,每个代理(broker)的消息传递模型的细节有很大的不同。

使用消息有很多优点:

  • 将客户端和服务解耦: 一个客户端只需要发送一条消息到合适的通道。客户端完全不知道服务实例,它不需要使用发现机制来确定服务实例的位置。
  • 消息缓存:对于同步请求/响应协议,比如HTTP,客户端和服务在交换期间都必须是可用的。相反,消息代理将写入通道的消息排队,直到使用者能够处理它们。例如,这意味着在线商店可以接受来自客户的订单,即使订单履行系统很慢或不可用。订单消息只是排到队列中。
  • 灵活的客户端和服务端之间的交互:消息支持前面描述的所有交互方式。
  • 显式的跨进程通信: 基于rpc的机制试图使调用远程服务看起来与调用本地服务一样。然而,由于物理定律和部分失败的可能性,它们实际上是非常不同的。消息传递使得这些差异非常明显,因此开发人员不会被安全的错觉所迷惑。

然而,使用消息也有一些缺点:

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

现在我们已经了解了如何使用基于消息的IPC,接下来让我们研究一下基于请求/响应的IPC。

同步的,请求/响应 跨进程通信(Synchronous,Request/Response IPC)

当使用一个同步的,基于请求/响应的 IPC 机制,一个客户端发送一个请求到一个服务。这个服务处理这个请求,然后发送回一个响应。

在许多客户机中,发出请求的线程在等待响应时阻塞。其他客户端可能使用异步的、事件驱动的客户端代码,这些代码可能由Futures或Rx Observables封装。然而,与使用消息传递不同的是,客户端假设响应将以及时的方式到达。

有许多协议可供选择。两种流行的协议是REST和Thrift。让我们首先看看REST。

REST

如今使用 RESTful 风格开发API是一件很流行的事。REST 是一种 IPC 机制,几乎总是使用HTTP。

REST 中一个核心的概念是资源(resource), 通常表示一个业务对象,比如一个客户或产品,或者一组类似的业务对象。REST使用HTTP动词(verbs)来操作资源,使用URL引用。例如,一个 GET 请求返回表示的资源(resource)。可能是以XML文档或者JSON对象的格式。一个 POST 请求创建一个新的资源(resource),一个PUT请求更新一个资源(resource)。

引用 REST 的发明者, Roy Fielding的话:

REST 提供了一组架构约束,当整体应用时,强调组件交互的可扩展性、接口的通用性、组件的独立部署和中间组件,以减少交互延迟,加强安全性。并封装遗留的系统.
-Roy Fielding, Architectual Styles and the Design of Network-based Software Architectures

图3-5显示了打车应用可能使用的REST方法中的一种。

在这里插入图片描述

乘客的手机通过发送一个POST请求到出行管理服务的 /trips 资源上。 这个服务通过发送一个 GET 请求到乘客管理服务,请求关于乘客的信息。在验证乘客被授权创建行程后,行程管理服务创建行程,并向智能手机返回201响应。

许多开发者宣传他们基于HTTP的APIs是RESTful风格的。然而,就像Fielding 在博客中描述的那样,并不是所有的都是。

Leonard Richardson(没有关系)为REST定义了一个非常有用的成熟的模型,这个模型包含了下面几层:

  • Level 0 : 0级API的客户端通过向服务的唯一URL端点发出HTTP POST请求来调用服务。每个请求都指定要执行的操作、操作的目标(例如,业务对象)和任何参数。
  • Level 1 : 1级API支持资源的概念。要在资源上执行操作,客户端会发出一个POST请求,指定要执行的操作和任何参数
  • Level 2 : 2级API使用HTTP动词来执行操作:GET来检索,POST来创建,PUT来更新。请求查询参数和正文(如果有的话)指定操作的参数。这使得服务能够利用web基础设施,如用于GET请求的缓存。
  • Level 3: 3级API的设计基于一个可怕的命名原则,HATEOAS(超文本作为应用程序状态的引擎)。的基本思想是通过GET请求返回的资源表示包含链接,资源上执行允许的操作例如,客户端可以取消一个订单使用链接的顺序表示响应发送GET请求来检索返回订单。

HATEOAS的好处之一是不再需要将url硬连接到客户端代码中。另一个好处是,由于资源的表示包含允许操作的链接,客户端不必猜测在资源的当前状态下可以执行什么操作。

使用基于HTTP的协议有很多好处:

  • HTTP即简单又熟悉
  • 你可以在浏览器中使用一个扩展插件比如 Postman 来测试一个HTTP的 API, 或者使用 curl 命令行(假设使用了 JSON 或者一些其他文本格式)
  • 它直接支持请求/响应-风格的通信
  • HTTP 堆防火墙友好
  • 它并不要求中间代理,这简化了系统的架构。

使用HTTP有一些缺点:

  • HTTP只支持请求/响应风格的交互。你可以使用HTTP发送通知,但是服务端必须一直发送一个HTTP响应。
  • 因为client和 service 直接通信(没有任何中间件做消息缓冲),它们必须在交换期间运行。
  • client 必须知道每个服务实例的位置,就像第二章描述的API网关,客户端必须使用服务发现机制来定位服务实例。

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

Thrift

Apache Thrift 是 REST 的一个有趣的替代,它是一个用于编写客户端可服务端跨语言的RPC框架。Thrift 提供了一种C风格的接口定义语言(IDL)来定义您的APIs,你使用Thrift 编译期来生成客户端的存根(subs)和服务端的骨架(skeletons)。编译期生成各种各样语言(包括 C++, Java, Python, PHP, Ruby, Erlang, 和 Node.js)类型的代码。

一个Thrift接口包含一个或多个服务。一个服务的定义和Java接口相似。它是一组强类型的方法。

Thrift方法可以定义成返回一个值,或者,如果定义成单向的方法,可以不返回值。有返回值的方法实现请求/响应风格的交互。客户端等待一个响应,然后可能会抛出一个异常。单向方法对应于通知样式的交互;服务端并不会发送一个响应(response)。

Thrift支持各种类型的消息格式:JSON, binary, 和 compact binary。 Binary 比 JSON 更高效,因为它比JSON解码更快。并且,顾名思意义,紧凑的二进制(compact binary)是一种空间高效的格式。当然,JSON是,对人类和浏览器友好的。Thrift还为您提供了传输协议的选择,包括raw TCP 和 HTTP。Raw TCP 比 HTTP更加的高效。然而,HTTP是防火墙友好的,浏览器友好的,人类友好的。

消息格式(Message Formats)

现在,我们已经研究了 HTTP 和 Thrift,让我们来研究一下消息格式的问题。如果您正在使用消息系统或 REST,您可以选择消息格式。其他 IPC 机制(如Thrift)可能只支持少量消息格式,甚至仅支持一种消息格式。

无论哪种情况,使用跨语言消息微服务格式都很重要。即使您今天用单一语言编写微服务,将来也很可能会使用其他语言。

有两种类型的消息格式:text 和 binary。 基于text格式的例子包括 JSON 和 XML。 这些格式的优点是,他们不仅仅是人类可读的,它们也是自描述的。在JSON中,一个对象的属性描述成一组键值对。类似的,在XML中,属性被表示成名称元素和值。这使得消息的消费者挑出他们感兴趣的值,然后忽略剩下的。因此,消息格式的小量修改可以容易的向后兼容。

XML文档结构通过一个XML语法描述。随着时间的推移,开发人员社区已经意识到, Json 也需要一个类似的机制。一种选择是使用 Json schema, 无论是独立还是作为 IDL 的一部分, 如swagger。

使用基于文本的消息格式的一个缺点是,消息往往冗长,尤其是 XML。由于消息是自我描述的,因此每个消息都包含属性的名称及其值。另一个缺点是分析文本的开销。因此,您可能需要考虑使用二进制格式。

有几个二进制格式可以选择。如果您使用Thrift RPC,您可以使用binary Thrift。如果您可以选择消息格式,热门选项包括Protocol Buffers和Apache Avro。这两种格式提供了一种类型的接口定义语言(IDL)用于定义你消息的结构。然而,有一点不同,Protocol Buffers 使用标记的字段,而 Avro 消费者需要了解消息格式才能解释消息。因此,在 API 的演变上,Protocol Buffers 比 Avro 更容易,此博文是Thrift、Protocol Buffers 和 Avro 的性能比较。

总结

微服务必须使用一种跨进程通信机制进行通信。当设计你的服务进行交互时,你需要考虑各种各样的问题:服务如何交互,如何为每个服务指定API,APIs 如何迭代, 如何处理局部失败。微服务可以使用两种类型的跨进程通信机制(IPC mechanisms):异步的消息(asynchronous)和同步的请求/响应(synchronous request/response)。为了通信,一个服务必须能够发现其他的服务:在第4章我们将了解在微服务交给中服务发现的问题。

Microservices in Action: NGINX and Application Architecture

NGINX 使您能够实现各种缩放和镜像选项,使您的应用程序响应更灵敏,可用性更高。您在缩放和镜像方面所做的选择会影响您进行跨进程通信的工作方式,这是本章的主题。

我们在 NGINX 建议您在实施基于微服务的应用时考虑四层架构, Forrester 有一份详细的专题报告,您可以免费从 NGINX 下载

这些层有 代表客户端(represent clients)(最新层 - 包括台式机或笔记本电脑以及移动、可穿戴或物联网客户端)、交付(delivery)、聚合(aggregation)(包括数据存储)和服务(services),这些服务包含应用功能和特定于服务,而不是共享的数据存储。

与以前的三层架构相比,四层架构更加灵活、可扩展、响应迅速、移动友好,并且本质上支持基于微服务的应用程序开发和交付。行业领导者,如 Netflix 和优步,能够达到他们的用户的需求所要求的性能,因为他们使用这种架构。

NGINX 本身就非常适合四层架构,其功能范围从媒体到媒体流式处理客户端层,为交付层加载平衡和缓存,在聚合层中用于高性能和安全的基于 API 的通信的工具,以及支持灵活管理服务层中的临时服务实例。

同样的灵活性使得实现强大的缩放和镜像模式以处理流量变化、防止安全攻击以及在接到通知后随时提供故障转移配置提供高可用性成为可能。

在这些更复杂的架构中,包括需求所需的服务实例即时化以及不断发现服务的需求,低耦合的流程间通信往往受到青睐。这里的异步和一对多通信方式可能更灵活,最终提供更高的性能和可靠性,而不是紧密耦合的通信风格。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值