第三篇:进程间通信

本文出自Inter-Process Communication in a Microservices Architecture,作者 Chris Richardson, 写于2015年5月19日


这是该系列的第三篇文章,第一篇文章介绍了微服务架构模式,以及与单体架构模式的比较,并讨论了使用微服务的利弊。第二篇文章阐述了应用如何通过API代理网关与微服务通信。本文我们将会了解一下系统中的服务之间如何进行通信。下一篇文章将会探讨与之密切相关的服务发现机制。

一、介绍

在单体应用中,组件之间的交互通过通过语言级别的方法或者直接函数调完成。相对而言,基于微服务的应用是运行在多个机器上的分布式系统。每个服务实例都是一个单独的进程。结果就是服务必须通过进程间通信机制(IPC)来进行交互,如图3-1所示:

这里写图片描述

图3-1 微服务使用进程间通信进行交互

稍后我们将会看一些特定的IPC技术,但是首先我们要考虑一下各种设计上的问题。

二、交互风格

当选择IPC机制时,首先考虑一下服务之间如何交互,有多种客户端—服务的交互风格。这些风格可以在两个维度上划分:

  • 第一个维度是交互是一对一还是一对多
    • 一对一:每个客户端的请求只被一个服务实例处理;
    • 一对多:每个请求被多个服务实例处理;
  • 第二个维度是交互是同步的还是异步的
    • 同步:客户端希望服务立即响应,甚至当等待响应时会阻塞;
    • 异步:当客户端等待响应时不会阻塞,如果有响应的话,也不一定要立刻返回;

下表显示了多种交互风格:

类型 一对一 一对多
同步 请求/响应(Request/Response) ——
异步 通知(Notification) 订阅/发布(Publish/subscribe)
异步 请求/异步响应(Request/async response) 发布/异步响应(Publish/async response)

对于一对一的交互模式:

  • 同步的请求与响应:客户端发送请求给服务然后等待响应。客户端期待响应立即返回。基于线程的应用中,发送该请求的线程甚至在等待的时候会阻塞;
  • 异步通知:客户端给服务发送请求但是不期待响应返回;
  • 请求与异步响应:客户端给服务发送请求,服务异步响应。客户端在等待的时候不会阻塞,甚至响应可以在一段时间后才到达;

对于一对多的交互模式都是异步的

  • 发布/订阅:客户端发布通知消息,这个消息可以被0个或者多个有兴趣的服务消费;
  • 发布/异步响应:客户端发布请求消息,在一段时间内等待有兴趣的服务的响应;

服务通常结合使用这些交互模式。对于一些服务,单个IPC机制已经足够。其他服务可能要结合使用IPC机制。

图3-2 显示在打车应用中,客户下单时服务之间是如何交互的。

这里写图片描述

图3-2 在服务交互中使用多种IPC机制

这些服务将异步通知、请求/响应和发布订阅结合在一起使用。例如,乘客的手机给出行管理服务发送通知来下单。出行管理服务通过调用乘客管理服务来验证乘客信息是否有效。订单管理服务接着创建一个订单,并且使用发布/订阅来通知其他的服务,比如调度中心,来定位可用的司机。

既然我们已经了解了交互模式,继续了解一下如何定义API。

三、定义API

服务的API是服务和客户端之间交互的契约。不管你选择哪种IPC机制,使用一些接口定义语言(interface definition language ,IDL)来精确地定义服务的API是十分重要的。甚至有建议说使用API优先的方法来定义API,通过确定接口定义,并在与客户端开发者共同审核之后,再进行服务的开发工作。只有在不断地迭代API的定义之后,才能实现该服务。预先设计使得构建的服务更能满足客户端的需求。

正如在本文后面将会看见的,API定义的特点依赖于你使用的IPC机制。如果你使用消息,那么API包括消息通道和消息类型。如果你使用HTTPAPI包括URL和请求与响应的格式。后面我们会讨论一些IDL的细节问题。

四、升级API

服务API总是随着时间不断变化的。单体应用中通常直接更改API并更新所有的调用者。在基于微服务的应用中这是极其困难的,即使你的API的消费者是相同应用中的其他服务。一般情况下,你是不能强制要求所有客户都升级与该服务的绑定。但是你可以通过不断部署新版本服务,这样新旧版本的服务就会同时运行,处理这种问题的的策略是很重要的。

如何处理API的变化也依赖于其大小。一些变化很小,可以向后与之前的版本兼容。例如,增加一些请求和响应的属性。设计客户端和服务器时遵从健壮性原则是很有意义的。使用旧的API客户端可以继续在新版本的服务下工作。服务可以给移除的属性提供默认值,客户端可以忽略新增的响应属性。使用容易升级APIIPC机制和消息格式是很重要的。

然而,有时你必须对API做一些核心的、不兼容的改变。因为你不能强迫客户端立即升级,服务在一段时间内必须支持老版本的API,如果你正在使用基于HTTP机制的通信,比如REST一个方法是在URL中嵌入一个版本号。每个服务实例可以同时处理多个版本的API。你也可以将部署将这些服务部署到不同的实例中,每个实例处理特定版本的API

五、处理局部故障

正如第二篇文章中提到的,分布式系统中始终存在局部故障的风险。因为客户端和服务分别在独立的进程中,对于客户端的请求,服务可能不能做到及时地响应。服务可能因为故障和维护而宕机,或者因为超负荷而对请求的响应极其缓慢。

例如,第二篇文章中的产品信息场景中。假设推荐服务没有响应,一个不成熟的客户端可能会无限期地等待响应。这种结果不仅会带来极差的用户体验,在很多的应用中也会耗费珍贵的资源,比如线程。最终会耗费完所有的线程但依然无响应,如图3-3所示:

这里写图片描述

图3-3 由于无响应的服务造成线程阻塞

为了避免这种问题的发生,当设计服务的时候必须考虑局部故障。可以遵循由Netflix提出的有效方法,处理局部故障的策略包括:

  • 网络超时:永远不要陷入无限地阻塞中,当等待响应的时候总是使用超时机制。使用超时能够确保资源永远不会被无限占用;
  • 限制未完成请求的数量:设置客户端与服务之间未完成请求的上限。如果达到上限,再发送格外的请求没有意义,这些尝试需要立即失败;
  • 断路器模式:追踪成功和失败请求的数量。如果失败率超过了预定的阈值,触发断路器以便之后的请求尝试立即失败。如果大量请求都失败,表明该服务不可用,继续发送请求是毫无意义的。在一段超时的时间后,客户端应该再次尝试,如果成功,关闭断路器;
  • 提供备选方案:当请求失败的时候,执行备选方案,例如,返回缓存值或者默认值,比如推荐的空集;

Netflix Hystrix 是一个已经实现了上述策略以及其他模式的开源库。如果你使用基于JVM的平台,可以考虑使用它,如果运行在一个非JVM的环境中,应该考虑使用类似功能的库。

六、IPC技术

有很多不同的IPC技术以供选择。服务可以使用基于同步的请求与响应通信机制,比如基于HTTPREST或者Thrift。也可以使用基于消息的异步通信机制,比如AMQP或者STOMP等等。

也有很多的消息格式以供选择。服务可以使用人类可读的、基于文本的格式,比如JSON或者XML。另外也可以使用二进制格式(更有效率),比如Avro或者Protocol Buffers。之后我们会了解同步的IPC机制,但是首先讨论一下异步的IPC机制。

6.1 基于消息的异步通信

当使用消息时,进程之间通过异步交换消息进行通信,客户端通过发送消息来请求服务。如果服务要回复的话,也会返回给客户端一个单独的消息。因为通信是异步的,客户端不会阻塞来等待响应。代替的,客户端在开发时会假设响应不会迅速返回。

一个消息包含消息头(比如发送人等元信息)和消息体。消息通过通道进行交换。任何数量的生产者都可以发送消息到一个通道中。类似的,任何数量的消费者都可以从一个通道接收消息。有两种类型的通道:

  • 点对点:该通道只会将消息投送给等待从通道中读取消息的消费者中的一个。服务使用点对点的通道对应上文中描述的点对点的交互模式;
  • 发布-订阅:该通道投递消息给所有关联的消费者。服务使用发布-订阅通道对应上述中提到的一对多的交互模式;

图3-4 显示了打车应用可能使用的发布-订阅模式:

这里写图片描述

图3-4 在打车应用中使用发布-订阅通道

出行管理服务通过将出行创建消息写入发布-订阅通道来通知感兴趣的服务新的出行,比如调度中心。调度中心发现可用的司机时,通过发送司机通知消息到发布-订阅通道来通知其他的服务,。

在很多可选的消息系统中,应该选一个支持多种编程语言的消息系统。一些消息系统支持标准的AMQPSTOMP协议。其他的消息系统也有一些专有但是已经文档化的协议。

大量开源的消息系统以供选择,包括RabbitMQApache KafkaApache ActiveMQNSQ。从高层次上看,它们全都支持一些消息和通道的形式。它们都努力做到可靠、高性能、可扩展。然而,每个代理的消息模型的细节方面仍有重大的差异。

使用消息通信的优点:

  • 将客户端与服务解耦:客户端只需要给正确的通道发送一个消息即完成了请求。客户端完全不会意识到服务的存在。它也不需要使用发现机制来决定服务实例的位置
  • 消息缓存在同步的请求-响应协议中,比如HTTP,客户端和服务在交互的过程中都必须可用。比较而言,消息代理组件将写入通道的消息放入队列中直到消费者能处理它们。这意味着,即使订单管理系统运行的很慢甚至压根不可用,在线商店仍然能够从消费者接收订单,订单消息只管进入队列即可;
  • 客户-服务交互更加灵活:消息支持上述所有的交互模式;
  • 明确的进程间通信:基于RPC的通信机制尝试让调用远程服务和本地的服务相同。但是因为物理的限制和可能的局部故障,事实上,它们是完全不同的。消息机制使得这些差异十分明显,所以开发者不会蒙骗而进入一种虚假的安全状态;

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

  • 额外的复杂的操作:消息系统是另外一个必须要安装、配置和操作的组件。消息代理组件必须高可用,否则微服务系统的可用性就会被影响
  • 实现基于请求和响应交互的复杂性:请求-响应风格的交互要求实现一些工作。每个请求的消息必须包括应答通道标识符和关联标识符。服务将响应写入对应的应答通道中,响应中也必须包含关联标识符。客户端使用关联标识符来匹配对应的响应。通常情况下直接使用支持请求-响应交互的IPC机制会更加容易;

我们已经了解了基于消息通信的IPC机制,继续了解一些基于请求和响应的IPC机制。

6.2 同步的请求/响应

当使用基于同步的请求-响应IPC机制的时候,客户端发送一个请求到客户端。服务处理完请求并返回响应。

在许多的客户端中,发送请求的线程会阻塞直到响应返回。其他的客户端也可能使用异步的、事件驱动的客户端代码,这些代码可能被Futures或者Rx Observables封装。然而,不像使用消息机制,客户端假定响应会立即到达。

也有大量的协议以供选择。两种比较受欢迎的协议是RESTThrift。首先看一下REST吧!

6.2.1 REST

当今开发符合RESTful架构风格的API是十分流行的。REST是一个使用HTTPIPC机制。

REST中的一个核心概念是资源,它可以简单的表示为一个商业对象,比如消费者或者产品,或者许多的商业对象的集合。REST使用HTTP的谓词来操作资源,这些资源由URL指定。例如,GET请求返回一个资源的表示,可能以XML文档或者JSON对象的形式。POST请求创造了新的资源,PUT请求更新了资源。

引用REST的创造者Roy Fielding

REST提供了一系列的架构约束,总的来说,当应用REST的时候,注重组件交互的可扩展性,接口的一般性,组件和中间件的独立部署来减少交互的延迟,实施安全性,并要封装遗留系统。

—— Roy Fielding, Architectural Styles and the Design of Network-based Software Architectures

图3-5显示打车应用使用REST的一种方式:

这里写图片描述

图3-5 打车应用使用RESTful交互

乘客手机给出行管理服务的/trips资源发出POST请求。这个服务通过发送GET请求给乘客管理服务来请求乘客信息。在认证完乘客身份后来创建一个出行订单,出行管理服务创建出行订单并返回201响应给手机。

许多开发者声称他们的基于HTTPAPIRESTful风格的。然而,正如Fielding 在这篇文章中描述的一样,并不是他们所有的API都是REST风格的。

Leonard Richardson 定义了非常有用的REST成熟模型, 包含以下的层次:

  • level 0:客户端API通过发送POST请求给与它对应的唯一的URL endpoint来调用服务。每个请求指定了要执行的动作,动作的目标(例如,商业对象),和任何参数;

  • level 1API支持资源的思想。为了在资源上执行动作,客户端发送指定操作动作和任何参数的POST请求;

  • level 2API使用HTTP谓词来执行动作:GET用来获取;POST用来创建;PUT用来更新。请求查询参数和请求体,如果有的话,指定动作的参数。这个使得服务可以利用web的基础设施,比如对GET请求的缓存;

  • level 3API的设计基于严格的命名原则,HATEOAS (Hypertext As The Engine Of Application State )。基本思想是GET请求返回的资源表达包括在该资源上允许执行的动作。例如,客户端通过使用获取订单的GET请求返回的订单表示中的链接取消订单;

    HATEOAS 的优点之一包括不再必须硬编码URL到客户端。另外一个好处是资源的状态包括允许动作的链接,客户端不必猜测在资源的当前状态下,可以执行哪些动作;

使用基于HTTP的协议有巨大的好处:

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

使用HTTP也有很多的缺点:

  • HTTP只直接支持请求-响应的交互模式。你可以使用HTTP来通知,但是服务器总会返回给HTTP响应;
  • 因为客户端和服务直接通信(没有中间件来缓存消息),它们两个在进行交互的时候必须全部运行;
  • 客户端必须知道每个服务实例的位置(URL)。正如在第二篇文章中描述的API网关,在当今的应用中是一个很重要的问题。客户端必须使用服务发现机制来定位服务的实例;

开发者社区最近重新发现了RESTful API的接口定义语言的价值。也有一些选择,包括RAMLSwagger。一些IDL,比如Swagger,允许你定义请求和响应消息的格式。其他的,比如RAML,要求你使用单独的规范,比如JSON Schema。正如对API的描述,IDL通常情况下使用工具从接口定义中生成客户端的stub和服务端的skeleton。

6.2.2 Thrift

Apache Thrift是对REST的有意思的替代,是用来编写跨语言RPC的客户端和服务器的框架Thrift提供了C风格的IDL来定义你的API,可以使用Thrift编译器来生成客户端的stub和服务端的skeleton。编译器为多种语言生成代码,包括C++JavaPythonPHPRubyErlangNode.js

Thrift接口包括一个或者多个服务。服务的定义和Java接口的定义是类似的。它是一些强类型的方法的集合。

Thrift方法可以返回一个值(可以是void),或者,被定义为单向,没有返回值。有返回值的方法实现了请求-响应风格的交互;客户端等待响应,也可能抛出异常。单向方法对应通知的交互模式,服务器不会返回任何响应。

Thrift支持多种消息格式:JSON、二进制和紧凑二进制。二进制比JSON更有效率,因为它解码更快。而且正如名称暗示的一样,紧凑二进制在空间上更有效率。JSON,然而对人类和浏览器友好。Thrift使你可以选择传输协议,包括原始TCPHTTP。原始TCP很可能比HTTP更有效率。然而,HTTP是防火墙友好的,浏览器友好的,人类友好的。

6.3 消息格式

了解了HTTPThrift,继续考虑消息格式的问题。如果使用消息系统或者REST,你就需要选择一种消息格式。其他的IPC机制,比如Thrift仅仅支持很少的消息格式,或者甚至只有一种。在任何情况下,使用跨语言的消息格式是很重要的。即使你现在只使用一种语言开发你的微服务应用。但是未来,你很可能也会使用其他语言。

有两种主要的消息格式:文本和二进制。基于文本格式的例子包括JSONXML。这种格式的优点不仅在于它们是可读的,自我描述的。在JSON中,一个对象的属性被一系列的键值对表达。类似的,在XML中,属性通过命名元素和值来表达,这使得消息的消费者可以选出有兴趣的值,并忽略其他值,对这种格式的微小变化可以很容易的做到向后兼容。

XML文档的结构通过XML schema指定。随着时间的发展,开发者社区逐渐地意识到JSON也需要类似的机制。一个选择就是使用JSON Schema ,或者独立,或者作为IDL的一部分,比如Swagger。

使用基于文本的消息格式的缺点是消息趋向于繁琐,特别是XML。因为消息是自我描述的,每个消息除了包括属性值还要包含属性名称。另外一个缺点是解析文本的负担,你可能考虑使用二进制格式。

有几种二进制格式可供选择。如果使用Thrift RPC,可以使用二进制的Thrift。如果必须要选择消息格式,受欢迎的选项包括Protocol BuffersApache Avro 。这两种格式都提供了类型化的IDL来定义消息的结构。然而它们之间有个差别,Protocol Buffers 使用标签字段,然而Avro消费者需要知道模式来解析消息。所以,相比于Avro,对于Protocol Buffers来说升级API进化更容易。这篇文章将Thrift、Protocol Buffers和Avro进行了很好的对比。

七、总结

微服务必须通过进程间通信机制进行交互。当设计服务间如何进行交互时,需要考虑多种问题:

  • 服务如何交互;
  • 如何为每个服务指定API
  • 如何升级API
  • 如何处理局部故障;

可以使用两种IPC机制:异步消息和同步的请求-响应。为了通信,一个服务必须能够发现另外一个。在下一篇文章中,我们将会讨论微服务架构中的服务发现问题。

猜你喜欢

转载自blog.csdn.net/lmy86263/article/details/75094493
今日推荐