Chris Richardson 微服务系列 第三篇 构建微服务之:微服务架构中的进程间通信

 
 

这是使用微服务架构构建应用系列的第三篇文章。第一篇文章介绍了微服务架构模式并讨论了使用微服务的优势和劣势 ;第二篇文章介绍了应用的客户端如何通过API网关作为中介实现服务间的通信;在这篇文章中我们将看一看同一系统间的服务如何通信;第四篇文章主要介绍服务发现的问题。

介绍

在传统单体应用中,模块间使用编程语言级别的方法或功能彼此调用。然而微服务架构应用本质上是运行在多台机器上的分布式系统,每个服务都是一个进程!因此,下图为我们展示,微服务必须使用进程间通信(IPC)的机制实现交互:

Paste_Image.png

稍后,我们将看具体的 IPC 技术实现,但首先让我们探讨不同方案设计中的问题。

交互风格

当我们为服务选择一种IPC机制的时候,我们首先要考虑服务间如何交互,技术上存在多种 client⇔service 交互风格:它们可以按照两大维度分类:第一维度是服务间交互是一对一还是一对多;

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

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

  • 同步:客户端期望来自服务端的及时响应,甚至可能阻塞并等待。
  • 异步:客户端等待响应时不会阻塞,对异步来讲,及时响应并不是必须的。

下列表格展示了两种方式的不同

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

有下面几种一对一的交互方式:

  • 请求/响应模式: 客户端向服务端发送请求并等待响应,并期望服务端可以及时的返回响应。在一个基于线程的应用中,发出请求的线程可能在等待时阻塞线程的执行。
  • 通知(也就是单向请求):客户端往服务端发送请求,但并不等待响应返回
  • 请求/异步响应:客户端往一个异步返回响应的服务发送请求。客户端等待式并不会阻塞线程,因为设计时就假设请求不会立即返回(js回调)

有下面几种一对多的交互方式:

  • 发布/订阅模式:客户端发布一个通知消息,消息将会被0或多个感兴趣的服务消费
  • 发布/异步响应:客户端发布一个请求消息,并在一定时间内等待消费消息的服务响应。

每个服务通常会使用多种交互风格的组合:对一些服务来讲,简单的IPC机制可能已经足够了,但另外一些服务可能需要几种IPC机制的组合。下图展示了在taxi-hailing应用中,当用户请求行程时,服务是如何交互的:

Paste_Image.png

这个服务使用了通知、请求/响应、发布/订阅风格的组合。比如,乘客使用智能手机向行程管理服务发送一个接送需求的通知,行程管理服务将使用请求/响应模式调用乘客服务来验证乘客账号是否为活动状态,然后行程管理服务创建行程并使用发布/订阅方式来通知诸如分发器(用来定位空闲司机)等服务。

我们已经讨论了交互风格,那么再来看下如何定义API。

定义API

服务API是服务与客户之间的契约。抛开选择哪种IPC机制的选择,使用一些接口定义语言interface definition language (IDL)准确定义服务API是很重要的!.当然,最好考虑使用API优先的方式来定义服务,通过先写接口定义语言来开始开发,并与客户端开发者(服务消费者)一起review你的设计,先对API定义进行迭代,再去实现这些服务。这样做设计的话将会使你构建更加符合客户需求的服务!

后续文章你将会发现,服务定义和你选择哪种IPC机制息息相关,如果你是要消息机制,API就由消息频道和消息类型组成;如果你使用http,API就是由URLs以及request/response格式组成。稍后我们将会讨论更多关于接口定义语言的细节。

API进化

服务API将会不可避免的随着时间进化,在传统单体应用中,我们可以很直接的去修改服务并更新所有服务的调用者(refactor)。但是在基于微服务架构的应用中,哪怕服务API的其他消费者都是在一个应用中,去更新所有服务也是相当困难的。你通常不能强制让所有的客户端升级来保持和服务端升级维持步调一致,而且,你还可能会增量部署新服务使得新老服务同时运行,寻找一种处理此种情况的策略是很重要的。

你是如何根据更改的大小来处理服务API的变化的呢?一些变化很小,通常可以与之前版本做到向后兼容,比如,你为请求或相应添加了一个属性;对此,设计服务时考虑服务和客户消费者的鲁棒性原则是很有必要的:使用就版本服务API的客户端可以在新版本服务API下正常工作,服务端为客户端缺失的属性提供默认值,客户端自动忽略额外添加的响应属性。最后强调,注意使用IPC机制和定义消息格式使你的API可以简单方便的进化!

当然,有时候我们不得不对API做一些较大的,不再兼容的变化,而我们这时候又不可能强制每个客户端升级,因此我们的服务就要继续支持运行一段时间的老版本API。如果使用http,我们可以在URL里嵌入服务版本,每个服务实例可能同时处理多个版本的服务,当然,你也可以选择为每个服务版本部署单独的服务实例。

处理局部故障

就像前面关于API网关文章提到的那样:在分布式系统中总会有无时无刻的局部故障的风险。由于客户端和服务在不同的进程中,服务可能由于挂掉或者维护原因而不能及时响应客户端的请求,或者服务由于过载原因导致响应缓慢。

比如,让我们考虑之前文章提到的Product details场景,假设推荐服务没有响应了,一个简单的客户端实现可能无期限的等待服务响应并阻塞,这样不仅导致糟糕的用户体验,在很多应用中还会消耗比如线程这样宝贵的资源,最终就像下图展示的那样,运行时将会用尽所有线程使得服务不再响应任何请求:

Paste_Image.png

为解决此类问题,设计上处理局部故障是很有必要的。

Netflix给出了一些处理局部故障比较好的方法:

  • 网络超时:等待响应时不要一直阻塞,而是使用超时,超时能够保证资源不会一直被占用
  • 限制未完成请求的数量:针对一个请求某服务的客户端,需要设置其未处理请求数量的上限,一旦超过限制就不再处理任何请求,这样就做到快速失败。
  • 断路器模式:跟踪成功和失败请求的数量,如果比率超过了设置的阀值,打开断路器使得后续请求快速失败。如果大量请求失败,就建议服务为不可以状态并决绝处理新请求,过一段时间之后,客户端可以再次重试,一旦成功,关闭断路器。
  • 提供fallback机制:请求失败时提供fallback,比如返回缓存值或者为失败的推荐服务返回默认空集合作为默认值。 Netflix Hystrix是一个实现了这些模式的开源工具包,如果你使用JVM那么一定要考虑使用它!如果你的服务不是运行在JVM中,那也要考虑有等效的实现来处理此类问题。

IPC 技术

我们有不同的IPC技术可供选择:服务可以使用基于请求/响应的同步通信模式,比如基于Http的REST或者Thrift,当然,也可以使用异步基于消息的通信模式,比如AMQP、STOMP。这些通信模式有不同的消息格式,服务可以使用基于文本格式、方便阅读的JSON 或者 XML格式,也可以使用效率更高的二进制格式(比如Avro或Protocol Buffers)。稍后我们将讨论同步IPC机制,现在我们先讨论下异步的IPC机制:

异步,基于消息的通信

使用消息时,进程间通过异步交换消息来通信。一个客户端通过发送消息的方式请求服务,如果期望服务有响应,也是服务通过向客户端发送另外的消息来实现。由于通信是异步的,客户端不会为了响应等待并阻塞,相反的,客户端编程时就是以服务不会立即返回响应来处理的。

一条消息包含消息头(元数据和发送者)和消息体,消息通过频道进行交换,任意数量的消费者都可以往频道中发消息,任意数量的消费者也可以消费频道中的消息。有point‑to‑pointpublish‑subscribe两种频道:point‑to‑point模式下,频道的消息只会被交付到某一个消费者,这种模式用于前面提到的一对一的交互;publish‑subscribe 模式下,频道的消息将会交付到所有感兴趣的消费者,使用于前面提到的一对多交互风格。

下图展示了taxi-hailing 应用可能是一publish-subscribe模式:

Paste_Image.png

行程管理服务通过向publish-subscribe频道写入trip create消息的方式通知比如分发器这样感兴趣的服务,分发器查找空闲司机并通过向publish-subscribe频道写入Driver Proposed消息通知其他服务。

有多种消息系统供我们选择,当然我们尽量选择一个支持多种编程语言的来使用。一些消息系统支持标准的协议比如 AMQP和STOMP,另一些消息系统有专有但是文档化的协议,大量的开源消息系统可供我们挑选,包括RabbitMQ、Apache Kafka、Apache ActiveMQ和NSQ。统一的来看,他们都支持某种形式的消息和频道,都致力于高可靠,高性能和高扩展性,但是每个消息中介在实现细节上还是有很大的不同: 使用消息系统有很多优点:

  • 客户端与服务端解耦: 客户端只需要向合适的频道发送消息就实现简单的请求,客户端完全感知不到服务实例的存在,因此不需要再去使用一套服务发现机制去决定服务实例的位置。
  • 缓存消息:在同步的请求/响应协议,比如HTTP下,客户端和服务端在交互的阶段必须保证双方都可用,然而,消息中介会把消息写入队列直到消息被消费者处理位置,这意味着,尽管 在订单履行系统响应缓慢甚至不可用情况下,在线商城仍然可以接受来自客户的订单,只需要先把订单消息简单的入队即可。
  • 灵活的客户-服务端交换风格,消息支持前面提到的所有交互风格。
  • 显示的进程间通信:基于 RPC的通信机制试图使调用远程服务等同于调用本地服务。然而,由于物理定律和局部故障的可能性,事实上他们相当不同。消息使这些差异非常明显,因此开发人员不被虚假的安全感所迷惑。

当然消息系统也有缺点:

  • 额外的运维复杂度:消息系统毕竟也是额外的系统组件,也要求安装、配置、运维等操作,有必要保证消息系统的高可用,否则会影响整个系统的稳定性。
  • 实现请求/响应交互的复杂度:要实现请求/响应的交互风格还是要做些额外工作的:每条请求消息必要要包含回复频道的标志符以及关联标志符 ,服务回写包含关联ID的消息到回复频道,客户端使用关联ID去匹配请求对应的响应。当然,如果使用直接支持请求/响应的基于IPC机制的方式,将会特别简单。

我们已经讨论了基于消息的IPC,再看检验下基于请求/响应的IPC吧:

同步,基于请求/响应的IPC

当使用同步,基于请求/响应的IPC机制的时候,客户端向服务端发送请求,服务端处理请求并返回响应,很多客户端,发出请求的线程会在等待响应过程中阻塞,另外有一些客户端也会使用异步、事件驱动的代码,比如封装好的Futures 或 Rx Observables。然而,和使用消息不一样,客户端假设请求会立即返回。有几种方案供我们选择,比较流行就是REST和 Thrift,我们先看下REST:

REST

限制使用REST风格暴露API很流行,REST基本就是使用HTTP的IPC 机制,REST的关键理念是资源,也就是通常代表诸如用户或产品的某个或一组业务对象,REST使用HTTP verbs维护URL指向的资源,比如 GET返回某资源的表示,可能是XML也可能是JSON对象, POST会创建新资源,PUT更新资源··· 引用自Roy Fielding,提出REST的大牛:

“REST provides a set of architectural constraints that, when applied as a whole, emphasizes scalability of component interactions, generality of interfaces, independent deployment of components, and intermediary components to reduce interaction latency, enforce security, and encapsulate legacy systems.” —Fielding, Architectural Styles and the Design of Network-based Software Architectures

下图展示了taxi-hailing 应用使用REST的场景:

Paste_Image.png

乘客向行程服务/trips发送POST请求,行程服务通过向乘客管理服务发送GET请求获取乘客信息,在验证完乘客授权之后,创建行程,行程服务创建行程后返回201响应给手机.

很多用了HTTP暴露服务API的开发就说自己是REST,其实按照 Fielding 在blog post描述的规定,他们根本不是REST。 Leonard Richardson (no relation)定义了非常有用的 maturity model for REST组成了下面几个级别:

  • Level 0 :客户端使用HTTP POST调用服务固定的URL,每次请求指定动作和参数
  • Level 1:支持资源的概念,请求通过POST,并且要制定要做的动作和参数
  • Level 2:充分使用 HTTP verbs 执行动作GET获取资源 POST创建资源PUT更新资源,还是要请求参数和请求体,还可以指定请求的参数,使得服务充分使用web基础架构的功能,比如缓存请求等
  • Level 3: API定义按照HATEOAS (Hypertext As The Engine Of Application State) 原则。基本的定义就是GET请求返回表示资源的body中包含一些对资源允许动作的链接。比如,客户端可以使用get订单返回的订单body中的一个超链接取消一个订单。HATEOAS 优点之一是:客户端不用在代码中硬编码URL了,另外,由于返回的body中包含允许对资源所作动作的超链接,客户端就不需要再猜测当前资源状态下他可以做哪些操作了。

使用基于HTTP的协议的优点有:

  • HTTP 简单而且大家都熟悉
  • 可以用浏览器测试,配合比如Postman插件更佳,命令行curl也很方便(假设使用json或其他数据格式)
  • 直接就支持请求/响应风格的通信
  • HTTP很友好
  • 无需中介,简化架构

使用HTTP的缺点:

  • HTTP只支持请求/响应风格的交互,你可以使用HTTP请求向服务器发送通知,但是服务器一定要返回HTTP响应。
  • 客户端和服务端没有消息buffer机制,交互都是直接的,这就要求交换消息的时候双方必须同时运行。
  • 客户端必须知道每个服务实例的地址,比如URL,正如前面的API网关文章描述的那样,在现代流行的应用架构中,这已经不再是一个问题,我们可以使用服务发现机制来定位服务实例。

开发者论坛最近又重新发掘了RESTful API风格接口定义语言的价值,我们可以选择使用RAML或者Swagger等工具, Swagger允许定义请求响应的消息格式,RAML则要求你使用额外的诸如JSON Schema这样的定义.IDL除了描述API,通常还会提供根据接口定义生产客户端Stub或服务端骨架的工具。

Thrift

Apache Thrift是REST的一个很有意思的替代品,它是一个实现跨语言客户端与服务端RPC通信的框架。Thrift提供C语言风格的接口定义语言来定义API,你可以通过编译生成客户端Stub和服务端的骨架,编译器可以为 C++、Java、Python、PHP、Ruby、Erlang、Node.js等不同语言生产代码。

一个Thrift接口包含一个或多个服务,一个服务定义可以类比java的接口:都是一组强类型方法的集合。Thrift方法可以返回值也可以被定义为单向通信,如果方法需要返回值就需要实现请求/响应风格的交互,客户端等待响应的时候可能会抛出异常;单向通信就是我们前面讲到的通知风格的交互,服务端不需要返回响应。

Thrift支持不同的消息格式:JSON、binary以及compact binary。 Binary相对JSON更加高效,因为解码速度更快,compact binary比JSON空间利用率高,见名知意嘛,JSON则对人和浏览器更加的友好 ;Thrift也支持不同的通信协议选择:原生TCP或者HTTP,原生TCP相比HTTP肯定更加高效,但是HTTP对防火墙、人以及浏览器更加的友好。

消息格式

既然我们已经讨论了HTTP和Thrift,现在再来探讨下消息格式的问题吧:如果你需要消息系统或者REST风格交互,你就必须选择消息格式。其他类似Thrift的IPC机制可能只支持一小部分的消息格式,甚至只会支持一种!在某些情况下,使用一种支持跨语言的消息格式非常重要,哪怕你现在只有一种语言实现微服务,谁又能保证你以后不会使用新的语言呢?

主要有文本和二进制两种格式:文本格式包括JSON和XML等,文本格式不仅仅方便阅读,而且是自描述的,JOSN中对象属性是采用一组键值对的组合来表示的;同样,XML的属性是采用命名元素和值来表示的,这样允许消费者只挑选感兴趣的消息摒弃其他消息,因而这种方式也可以方便的做到向后兼容。

XML文档的结构是由XML schema来指定的,随着时间的流逝,开发者论坛逐步意识到JSON也需要类似的机制:一种选择是使用JSON Schema,要么单独使用,要么作为类似Swagger这种IDL的一部分使用。

文本格式消息的缺点是非常的冗长,尤其是XML格式:由于消息是自描述的,每条消息除了值之外还包含属性的名称,另一个缺点就是解析文本开销略大,这时候可以考虑下二进制格式。

二进制格式也有多种选择:如果使用Thrift,你可以选择Thrift binary,如果选择其他的消息格式,比较流行的还有Protocol Buffers和Apache Avro,两种格式都提供了IDL来定义消息的结构。区别是,Protocol Buffers使用标记字段,而Avro 消费者则需要了解Schema才能解析消息,因此使用Protocol Buffers时,API进化比Avro更容易。这篇 文章是一个对Thrift、 Protocol Buffers以及 Avro非常好的比较。

总结

微服务需要使用进程间通信的机制进行交互,当设计你的服务如何通信的时候,需要考虑多个问题:服务如何交互、如何为服务定义API、如何处理API进化、如何处理局部故障。有两种微服务可以使用的IPC机制:异步消息和同步的请求/响应。该系列的下一篇文章,将会讲解微服务架构中的服务发现问题。

作者:nonumber1989 链接:http://www.jianshu.com/p/9c03081bc0d9 來源:简书 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。