【CHRIS RICHARDSON 微服务系列】微服务架构中的进程间通讯-3

 

编者的话 |本文来自 Nginx 官方博客,是微服务系列文章的第三篇,在第一篇文章中介绍了微服务架构模式,与单体模式进行了比较,而且讨论了使用微服务架构的优缺点。第二篇描述了采用微服务架构的应用客户端之间如何采用 API 网关方式进行通讯。在这篇文章中,咱们将讨论系统服务之间是如何实现通讯的。html

做者介绍:Chris Richardson,是世界著名的软件大师,经典技术著做《POJOS IN ACTION》一书的做者,也是 cloudfoundry.com 最初的创始人,Chris Richardson 与 Martin Fowler、Sam Newman、Adrian Cockcroft 等并称为世界十大软件架构师。nginx

Chris Richardson 微服务系列全 7 篇:web

1. 微服务架构概念解析编程

2. 构建微服务架构:使用 API Gateway浏览器

3. 深刻微服务架构的进程间通讯(本篇文章)缓存

4. 服务发现的可行方案以及实践案例安全

5. 微服务的事件驱动数据管理服务器

6. 选择微服务部署策略网络

7. 将单体应用改造为微服务(本篇文章)架构

简介

在单体应用中,各模块之间的调用是经过编程语言级别的方法或者函数来实现的。而基于微服务的分布式应用是运行在多台机器上的;通常来讲,每一个服务实例都是一个进程。

所以,以下图所示,服务之间的交互必须经过进程间通讯(IPC)来实现。

后面咱们将会详细介绍 IPC 技术,如今咱们先来看下设计相关的问题。

交付模式

当为某个服务选择 IPC 时,首先须要考虑服务之间的交互问题。客户端和服务器之间有不少的交互模式,咱们能够从两个维度进行归类。

第一个维度是一对一仍是一对多:

  • 一对一:每一个客户端请求有一个服务实例来响应。
  • 一对多:每一个客户端请求有多个服务实例来响应。

第二个维度是这些交互式是同步仍是异步:

  • 同步模式:客户端请求须要服务端即时响应,甚至可能因为等待而阻塞。
  • 异步模式:客户端请求不会阻塞进程,服务端的响应能够是非即时的。

下表显示了不一样交互模式:

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

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

  • 请求/响应:一个客户端向服务器端发起请求,等待响应,客户端指望此响应即时到达。在一个基于线程的应用中,等待过程可能形成线程阻塞。
  • 通知(也就是常说的单向请求):一个客户端请求发送到服务端,可是并不指望服务端响应。
  • 请求/异步响应:客户端发送请求到服务端,服务端异步响应请求。客户端不会阻塞,并且被设计成默认响应不会马上到达。

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

  • 发布/ 订阅模式:客户端发布通知消息,被零个或者多个感兴趣的服务消费。
  • 发布/异步响应模式:客户端发布请求消息,而后等待从感兴趣服务发回的响应。

每一个服务都是以上这些模式的组合。对某些服务,一个 IPC 机制就足够了;而对另一些服务则须要多种 IPC 机制组合。下图展现了在用户叫车时,打车应用内的服务是如何交互的。

上图中的服务通讯使用了通知、请求/响应、发布/订阅等方式。例如,乘客在移动端向“行程管理”服务发送通知,请求一次接送服务。“行程管理”服务经过使用请求/响应来唤醒“乘客服务”来验证乘客帐号有效,继而建立这次行程,并利用发布/订阅来通知其它服务,其中包括定位可用司机的调度服务。

如今咱们了解了交互模式,接下来咱们一块儿来看看如何定义 API。

定义API

API 是服务端和客户端之间的契约。不管选择了何种 IPC 机制,重点是使用某种交互定义语言(IDL)来准肯定义服务的 API。对于如何使用 API 优先的方式来定义服务,已经有了一些很好的讨论。你在开发服务以前,要定义服务接口并与客户端开发者共同讨论,后续只须要迭代 API 定义。这样的设计可以大幅提高服务的可用度。

在本文后半部分你将会看到,API 定义实质上依赖于选定的 IPC 机制。若是使用消息机制,API 则由消息频道(channel)和消息类型构成;若是选择使用 HTTP 机制,API 则由 URL 和请求、响应格式构成。后面将会详细描述 IDL。

不断进化的API

服务的 API 会随着时间而不断变化。在单体应用中,常常会直接修改 API 并更新全部的调用者。可是在基于微服务的应用中,即便全部的 API 的使用者都在同一应用中,这种作法也困难重重,一般不能强制让全部客户端都与服务保持同步更新。此外,你可能会增量部署服务的新版本,这时旧版本会与新版本同时运行。了解这些问题的处理策略相当重要。

对 API 变化的处理方式与变化的大小有关。有的变化很小,而且能够兼容以前的版本;好比给请求或响应增长属性。在设计客户端和服务时,颇有必要遵循健壮性原则。服务更新版本后,使用旧版 API 的客户端应该继续使用。服务为缺失的请求属性提供默认值,客户端则忽略任何额外的响应。使用 IPC 机制和消息格式可以让你轻松改进 API。

然而有时候,API 须要进行大规模改动,而且不兼容旧版本。鉴于不能强制让全部客户端当即升级,支持旧版 API 的服务还要再运行一段时间。若是你使用的是诸如 REST 这样的基于 HTTP 机制的 IPC,一种方法就是将版本号嵌入到 URL 中,每一个服务实例能够同时处理多个版本。另外一种方法是部署不一样实例,每一个实例处理一个版本的请求。

处理局部失败

在上一篇关于 API 网关的文章中,咱们了解到,分布式系统广泛存在局部失败的问题。因为客户端和服务端是独立的进程,服务端可能没法及时响应客户端请求。服务端可能会由于故障或者维护而暂时不可用。服务端也可能会因为过载,致使对请求的响应极其缓慢。

以上篇文章中说起的产品页为例,假设推荐服务没法响应,客户端可能会因为无限期等待响应而阻塞。这不只会致使不好的用户体验,而且在不少应用中还会占用以前的资源,好比线程;最终,以下图所示,运行时耗尽线程资源,没法响应。

为了预防这种问题,设计服务时候必需要考虑部分失败的问题。

Netfilix 提供了一个比较好的解决方案,具体的应对措施包括:

  • 网络超时:在等待响应时,不设置无限期阻塞,而是采用超时策略。使用超时策略能够确保资源不被无限期占用。
  • 限制请求的次数:能够为客户端对某特定服务的请求设置一个访问上限。若是请求已达上限,就要马上终止请求服务。
  • 断路器模式(Circuit Breaker Pattern):记录成功和失败请求的数量。若是失效率超过一个阈值,触发断路器使得后续的请求马上失败。若是大量的请求失败,就多是这个服务不可用,再发请求也无心义。在一个失效期后,客户端能够再试,若是成功,关闭此断路器。
  • 提供回滚:当一个请求失败后能够进行回滚逻辑。例如,返回缓存数据或者一个系统默认值。Netflix Hystrix 是一个实现相关模式的开源库。若是使用 JVM,推荐使用Hystrix。而若是使用非 JVM 环境,你可使用相似功能的库。

IPC技术

如今有不少不一样的 IPC 技术。服务间通讯可使用同步的请求/响应模式,好比基于 HTTP 的 REST 或者 Thrift。另外,也能够选择异步的、基于消息的通讯模式,好比 AMQP 或者 STOMP。此外,还能够选择 JSON 或者 XML 这种可读的、基于文本的消息格式。固然,也还有效率更高的二进制格式,好比 Avro 和 Protocol Buffer。在讨论同步的 IPC 机制以前,咱们先了解异步的 IPC 机制。

基于消息的异步通讯

使用消息模式的时候,进程之间经过异步交换消息消息的方式通讯。客户端经过向服务端发送消息提交请求,若是服务端须要回复,则会发送另外一条独立的消息给客户端。因为异步通讯,客户端不会由于等待而阻塞,相反会认为响应不会被当即收到。

消息由数据头(例如发送方这样的元数据)和消息正文构成。消息经过渠道发送,任何数量的生产者均可以发送消息到渠道,一样,任何数量的消费者均可以从渠道中接受数据。频道有两类,包括点对点渠道和发布/订阅渠道。点对点渠道会把消息准确的发送到从渠道读取消息的用户,服务端使用点对点来实现以前提到的一对一交互模式;而发布/订阅则把消息投送到全部从渠道读取数据的用户,服务端使用发布/订阅渠道来实现上面提到的一对多交互模式。

下图展现了打车软件如何使用发布/订阅:

经过向发布/订阅渠道写入一条建立行程的消息,行程管理服务会通知调度服务有新的行程请求。调度服务发现可用的司机后会向发布/订阅渠道写入一条推荐司机的消息,并通知其它服务。

有多种消息系统可供选择,最好选择支持多编程语言的。有的消息系统支持 AMQP 和 STOMP 这样的标准协议,有的则支持专利协议。也有大量的开源消息系统可用,譬如 RabbitMQ、Apache Kafka、Apache ActiveMQ 和 NSQ。宏观上,它们都支持一些消息和渠道格式,而且努力提高可靠性、高性能和可扩展性。然而,细节上,它们的消息模型却截然不同。

使用消息机制有不少优势:

  • 解耦客户端和服务端:客户端只须要将消息发送到正确的渠道。客户端彻底不须要了解具体的服务实例,更不须要一个发现机制来肯定服务实例的位置。
  • 消息缓冲:在 HTTP 这样的同步请求/响应协议中,全部的客户端和服务端必须在交互期间保持可用。而在消息模式中,消息中间人将全部写入渠道的消息按照队列方式管理,直到被消费者处理。也就是说,在线商店能够接受客户订单,即便下单系统很慢或者不可用,只要保持下单消息进入队列就行了。
  • 客户端-服务端的灵活交互:消息机制支持以上说的全部交互模式。
  • 清晰的进程间通讯:基于 RPC 的通讯机制试图让唤醒远程服务端像调用本地服务同样,然而,囿于物理定律和可能的局部失败,这两者大不相同。消息机制能让这些差别直观明确,开发者不会产生安全错觉。

然而,消息机制也有本身的缺点:

  • 额外的操做复杂性:消息系统须要单独安装、配置和部署。消息broker(代理)必须高可用,不然系统可靠性将会受到影响。
  • 实现基于请求/响应交互模式的复杂性:请求/响应交互模式须要完成额外的工做。每一个请求消息必须包含一个回复渠道 ID 和相关 ID。服务端发送一个包含相关 ID 的响应消息到渠道中,使用相关 ID 来将响应对应到发出请求的客户端。这种状况下,使用一个直接支持请求/响应的 IPC 机制会更容易些。

如今咱们已经了解了基于消息的 IPC,接下来咱们来看看基于请求/响应模式的 IPC。

基于请求/响应的同步 IPC

使用同步的、基于请求/响应的 IPC 机制的时候,客户端向服务端发送请求,服务端处理请求并返回响应。一些客户端会因为等待服务端响应而被阻塞,而另一些客户端可能使用异步的、基于事件驱动的客户端代码,这些代码可能经过 Future 或者 Rx Observable 封装。然而,与使用消息机制不一样,客户端须要响应及时返回。这个模式中有不少可选的协议,但最多见的两个协议是 REST 和 Thrift。首先咱们来了解 REST。

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 一般都有工具从接口定义中生成客户端存根和服务端框架。

ThriftApache 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 对于防火墙、浏览器和使用者来讲更友好。

消息格式

了解 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 消费者须要了解模式来解析消息。

Martin Kleppmann 的博客文章 对 Thrift、Protocol Buffers 和 Avor 进行了详细的比较。

总结

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

文章转载自:http://blog.daocloud.io/microservices-3/

查看英文原文

相关文章
相关标签/搜索