微服务(microservices)微服务架构

构建微服务之:微服务架构中的进程间通信

2017-01-01  本文已影响2082人  nonumber1989

原文链接:Building Microservices: Inter-Process Communication in a Microservices Architecture

  1. 微服务介绍
  2. 构建微服务之使用API网关
  3. 构建微服务之:微服务架构中的进程间通信(本文)
  4. 微服务中的服务发现
  5. 微服务之事件驱动的数据管理
  6. 选择一种微服务部署策略
  7. 重构单体应用到微服务

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

介绍

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

Paste_Image.png

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

交互风格

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

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

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

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

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

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

每个服务通常会使用多种交互风格的组合:对一些服务来讲,简单的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给出了一些处理局部故障比较好的方法:

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。统一的来看,他们都支持某种形式的消息和频道,都致力于高可靠,高性能和高扩展性,但是每个消息中介在实现细节上还是有很大的不同:
使用消息系统有很多优点:

当然消息系统也有缺点:

我们已经讨论了基于消息的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组成了下面几个级别:

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

使用HTTP的缺点:

开发者论坛最近又重新发掘了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机制:异步消息和同步的请求/响应。该系列的下一篇文章,将会讲解微服务架构中的服务发现问题。

上一篇 下一篇

猜你喜欢

热点阅读