文章目录

  • 微服务架构中的进程间通信概述
    • 交互方式
    • 消息的格式
  • 基于同步远程过程调用模式的通信
    • 使用 REST
    • 使用 gRPC
    • 使用断路器模式处理局部故障

微服务架构中的进程间通信概述

交互方式

客户端与服务的交互方式可以分为两个维度,第一个维度关注的是一对一和一对多:

  • 一对一:每个客户端请求由一个服务实例来处理。
  • 一对多:每个客户端请求由多个服务实例来处理。

交互方式的第二个维度关注的是同步和异步:

  • 同步模式:客户端请求需要服务端实时响应,客户端等待响应时可能导致堵塞。
  • 异步模式:客户端请求不会阳寒进程,服务端的响应可以是非实时的。

一对一的交互方式有以下几种类型:

  • 请求 / 响应:一个客户端向服务端发起请求,等待响应;客户端期望服务端很快就会发送响应。在一个基于线程的应用中,等待过程可能造成线程阻塞,这样的方式会导致服务的紧耦合。
  • 异步请求 / 响应:客户端发送请求到服务端,服务端异步响应请求。客户端在等待响应时不会阻塞线程,因为服务端的响应不会马上就返回。
  • 单向通知:客户端的请求发送到服务端,但是并不期望服务端做出任何响应。

一对多的交互方式有以下几种类型:

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

消息的格式

进程间通信的本质是交换消息。消息的格式可以分为两大类:文本和二进制。

基于文本的消息格式

  • 第一类是 JSON 和 XML 这样的基于文本的格式。这类消息格式的好处在于,他们的可读性很高,同时也是自描述的。JSON 消息是命名属性的集合,XML 消息也是命名元素和值的集合。这样的格式允许消息的接收方只挑选他们感兴趣的值,而忽略掉其他。也因此对消息结构的修改可以做到很好的后向兼容性。
  • 弊端是消息往往过于冗长,特别是 XML;另一个弊端是解析文本引入的额外开销,尤其是在消息较大的时候。

二进制消息格式

  • 有两种不同的二进制消息可供选择,常用的包括 Protocol Buffers 和 Avro。这两种消息都提供了一个强类型定义的 IDL(接口描述文件),用于定义消息的格式。编译器会自动根据这些格式生成序列化和反序列化的代码。

基于同步远程过程调用模式的通信

如下图显示了远程过程调用的工作原理。客户端中的业务逻辑调用代理接口,这个接口由远程过程调用代理配器类实现。远程过程调用代理向服务发出请求,该请求由远程过程调用服务器适配器类处理,该类通过接口调用服务的业务逻辑,然后它将回复发送回远程过程调用代理,该代理将结果返回给客户端的业务逻辑。

代理接口通常封装底层通信协议,有许多协议可供选择,这里将介绍 REST 和 gRPC,并且将介绍如何通过正确处理局部故障来提高服务的可用性,同时解释为什么使用远程过程调用的基于微服务的应用程序必须使用服务发现机制。

使用 REST

REST 是一种使用 HTTP 协议的进程间通信机制,如今开发者非常喜欢使用 RESTful 风格来开发 API,REST 中的一个关键概念是资源,它通常表示单个业务对象,可以将其理解为一个超大的 JSON 文档,通过 URI 在文档中来定位资源,通过 HTTP 动词完成增删查改操作,例如以下 JSON 举例:

{members: [{id: 100000001,mobile: 18900000001,addresses: [{id:100000001, detail: 美创总部}],tags: [100000001,100000002],pointRecords: [{id:100000001}]},{id: 100000002,mobile: 18900000002,addresses: [{id:100000002, detail: 美创总部}],tags: [100000003],pointRecords: [{id:100000003}]}]}

URL 规范

第一步是通过 URL 在文档中对资源进行定位。URL 是统一资源定位符,用于在文档中定位资源。如果资源的单词是包含多个,采用驼峰形式,例如积分流水,采用:pointRecords。看几个常见的URL

  • https://cmms3.meicloud.com/webadmin/vip/members/100000001 定位到会员 id 为 100000001 的会员对象
  • https://cmms3.meicloud.com/webadmin/vip/members/100000001/addresses/100000002 定位到会员 100000001 下的 id 为 100000002 的会员地址对象(不推荐、推荐下面这个)
  • https://cmms3.meicloud.com/webadmin/vip/addresses/100000002 定位到 id 为 100000002 的会员地址
    从上面的例子看出,URL 包含以下几部分
  • 协议,域名,端口
  • 业务应用:上面的 webadmin 就是应用
  • 业务模块:vip 是业务模块,表示会员中心
  • 具体的资源:具体中心的资源定位,包括复数名字和唯一资源定位符(ID)

对于操作会员地址,为什么不是先定位到会员再定位到地址?

  • 主要是通过地址的唯一标识也能定位到,所以直接简化为直接通过地址 ID 定位到地址对象。

动词规范

第二步就是通过动词,对资源进行操作

标准动词

  • 增加: POST https://cmms3.meicloud.com/webadmin/vip/members
  • 删除: DELETE https://cmms3.meicloud.com/webadmin/vip/members/100000001
  • 根据 ID 查询: GET https://cmms3.meicloud.com/webadmin/vip/members/100000001
  • 修改: PATCH https://cmms3.meicloud.com/webadmin/vip/members/100000001(小程序不支持 PATCH ,改用 PUT 亦可)

自定义动词

  • 如果对应的操作无法抽象成标准的 HTTP 动词,允许定义自定义动词。为了满足 URL 规范,需要维护好自定义动词表。自定义动词采用 HTTP POST 方法,在 URL 后面,通过动词的方法来定义。例如:
    – 根据条件查询: POST https://cmms3.meicloud.com/webadmin/vip/members:search
    – 批量更新: POST https://cmms3.meicloud.com/webadmin/vip/members:batchUpdate
    – 锁定操作: POST https://cmms3.meicloud.com/webadmin/vip/members/100000001:disable
  • 维护好自定义动词表,后续涉及的自定义动词都从动词表出,如果没有,才考虑新增,并维护进动词表。目前系统上用的自定义动词表有:
    – 分页查询: xxx:search,这个本来可以用 GET 方法,由于条件可能较多,就没有采用 GET 方法 +queryParmeter 来实现。
    – 批量操作: batchXXX, 例如: batchGet,batchUpdate,batchDelete,batchCreate
    – 其它操作: 例如 :login,:lock,:unlock,:disable,:enable,:approve,:stat

好处和弊端

REST 有如下好处:

  • 它非常简单,并且大家都很熟悉;
  • 可以使用浏览器扩展(比如 Postman 插件)或者 curl 之类的命令行(假设使用的是 JSON 或其他文本格式)来测试 HTTP API;
  • 直接支持请求 / 响应方式的通信;
  • HTTP 对防火墙友好;
  • 不需要中间代理,简化了系统架构。

它也存在一些弊端:

  • 它只支持请求 / 响应方式的通信;
  • 可能导致可用性降低,由于客户端和服务直接通信而没有代理来缓冲消息,因此它们必须在 REST API 调用期间都保持在线;
  • 客户端必须知道服务实例的位置(URL),客户端必须使用所谓的服务发现机制来定位服务实例;
  • 在单个请求中获取多个资源具有挑战性;
  • 有时很难将多个更新操作映射到 HTTP 动词。

使用 gRPC

  • gRPC 是一种基于二进制消息的协议,这意味着如同前面讨论二进制消息格式时所说的,不得不采用 API 优先的方法来进行服务设计。
  • gRPC API 是由一个或多个服务和请求 / 响应消息定义组成,服务定义类似于 Java 接口,是强类型方法的集合。
  • gRPC 使用 Protocol Buffers 作为消息格式,Protocol Buffers 是一种高效且紧凑的二进制格式。

使用断路器模式处理局部故障

分布式系统中,当服务试图向另一个服务发送同步请求时,永远都面临着局部故障的风险,因为客户端和服务端是独立的进程,服务端很有可能无法在有限的时间内对客户端的请求做出响应,服务端可能因为故障或维护的原因而暂停。或者,服务端也可能因为过载而对请求的响应变得极其缓慢。

客户端等待响应被阻塞,这可能带来的麻烦就是在其他客户端甚至使用服务的第三方应用之间传导,并导致服务中断。

模式·断路器:这是一个远程过程调用的代理,在连续失败次数超过指定阈值后的一段时间内,这个代理会立即拒绝其他调用。

要通过合理地设计服务来防止在整个应用程序中故障的传导和扩散,这是至关重要的。解决这个问题分为两部分:

  • 必须让远程过程调用代理有正确处理无响应服务的能力。
  • 需要决定如何从失败的远程服务中恢复。

开发可靠的远程过程调用代理

每当一个服务同步调用另一个服务时,它应该需要方法来保护自己,这种方法包括以下机制的组合。

  • 网络超时:在等待针对请求的响应时,一定不要做成无限阻塞,而是要设定一个超时。使用超时可以保证不会一直在无响应的请求上浪费资源。
  • 限制客户端向服务器发出请求的数量:把客户端能够向特定服务发起的请求设置一个上限,如果请求达到了这样的上限,很有可能发起更多的请求也无济于事,这时就应该让请求立刻失败。
  • 断路器模式:监控客户端发出请求的成功和失败数量,如果失败的比例超过一定的阈值,就启动断路器,让后续的调用立刻失效。如果大量的请求都以失败而告终,这说明被调服务不可用,这样即使发起更多的调用也是无济于事。在经过一定的时间后,客户端应该继续尝试,如果调用成功,则解除断路器。

从服务失效故障中恢复

使用诸如 Hystrix 之类的库只是解决方案的一部分,你还必须根据具体情况决定如何从无响应的远程服务中恢复你的服务。

  • 一种选择是服务只是向其客户端返回错误,例如创建Order的请求失败,唯一的选择是API Gateway将错误返回给移动客户端。
  • 在其他情况下,返回备用值(fallbackvalue,例如默认值或缓存响应)可能会有意义。

每个服务的数据对客户来说重要性可能不同,比如订单数据至关重要,如果此服务不可用,APIGateway应返回其数据的缓存版本或直接返回错误;而其他不太重要的服务不可用的话,APIGateway 应返回其数据的缓存版本或从响应中省略它。


《微服务架构设计模式》第三章 微服务的拆分策略