微服务架构基于多个服务构建应用,这些服务必须经常协作才能处理各种外部请求。因为服务实例通常是在多台机器上运行的进程,所以它们必须使用进程间通信进行交互。因此,进程间通信技术在微服务架构中比单体架构中扮演着更重要的角色。本文将探讨各种进程间通信机制,并讨论如何进行权衡。注意,需要牢记“没有银弹”这个大原则。
选择合适的进程间通信机制是一个重要的架构决策。它会影响应用程序可用性。更重要的是,进程间通信甚至与实务管理相互影响。一个理想的微服务架构应该是在内部由松耦合的若干服务组成,这些服务使用异步消息相互通信。REST等同步协议主要用于服务与外部其他应用的通信。
微服务间通信概述
微服务架构以进程为一等公民,也就是说每个微服务实例都是一个独立的进程。微服务间通信就是进程间通信。有很多进程间通信技术可供选择。服务间可以使用基于同步请求/响应的通信机制,如HTTP REST或gRPC。另外,也可以使用异步的基于消息的通信机制,常见的消息通信协议有:AMRP或STOMP等。本文将从服务的交互方式、API定义、消息的格式等方面来介绍微服务间通信。
交互方式
在为服务选择进程间通信机制之前,首先应考虑服务与客户端的交互方式。考虑交互方式将有助于专注于需求,并避免陷入特定进程间通信技术的细节。交互方式的选择会影响应用的可用性。此外,交互方式还可以帮助选择更合适的集成测试策略。
有多种客户端与服务的交互方式。这里从两个维度介绍。第一个维度关注的是映射关系。具体可以分为两类:一对一和一对多。
(1) 一对一:每个客户端请求由一个服务实例来处理。
(2) 一对多:每个客户端请求由多个服务实例来处理。
第二个维度关注的是同步和异步。
(1) 同步模式:客户端请求需要服务器实时响应,客户端等待响应时可能导致阻塞。
(2) 异步模式:客户端请求不会阻塞进程,服务器的响应可以是非实时的。
一对一的交互方式有以下几种类型:
(1) 请求/响应:一个客户端向服务器发起请求,等待响应;客户端期望服务器很快发送响应。在一个基于线程的应用中,等待可能造成线程阻塞。
(2) 请求/异步响应:客户端发送请求到服务器,服务器异步响应请求。客户端在等待响应时不会阻塞线程,因为服务器的响应不会马上返回。
(3) 通知(单向通知):客户端的请求发送到服务器,但并不期望服务器做出任何响应。
一对多的交互方式有以下几种类型:
(1) 发布/订阅: 客户端发送通知消息,被零个或多个感兴趣的服务订阅。
(2) 发布/异步响应:客户端发送请求消息,然后等待从感兴趣的服务发回的响应。
每个服务通常使用的都是以上这些交互方式的组合,而不是单独使用一个交互方式。
API定义
API或接口是软件开发的中心。一个应用由一到多个模块构成(对微服务架构来说,这里的模块就是指组件,也即微服务),每个模块都有接口,这些接口定义了该模块支持的操作。一个设计良好的接口会在暴露有用功能的同时隐藏实现细节。因此,接口实现的细节可以被修改,而接口保持不变,这样就不会对客户端产生影响。也就是,接口一旦公布出去,在后续的维护过程中,必须保证接口的向下兼容。对于无法兼容的能力,则必须新开接口。
对微服务架构来说,服务的API是服务与其客户端之间的契约(contract)。这里的API可能包含向客户端提供的方法或服务发布的事件。方法具备名称、参数和返回类型。事件具有一个类型和一组字段,可发布到消息通道。
对于微服务架构,其挑战在于:服务和它的客户端并不会一起编译。如果使用不兼容的API部署新版本的服务,虽然在编译阶段不会出现错误,但是会出现运行时故障。
无论选择哪种进程间通信机制,使用某种接口定义语言(Interface Define Language, IDL)精确定义服务的API都很重要。在日常的开发中,首先后端开发人员编写接口定义,然后与设计人员、客户端开发人员一起评审该接口定义文档。只有确定API定义后,后端开发人员和前端开发人员才真正启动编程工作。这种预先设计有助于帮助构建满足客户端需求的接口。这种模式也称为"API设计优先模式"。在前后端分离的阶段,这种方式可以大大弱化前端对后端的依赖,实现一定程度的并行开发,提供工作效率。
如何定义API取决于使用的进程间通信机制。如使用消息通信机制,则API由消息通道、消息类型和消息格式组成。在基于Kafka的消息通信中,Protocol Buffers已成为IDL的推荐方式。如使用HTTP,则API由URL、HTTP动词以及请求和响应格式组成。在基于Java Web的开发中,Swagger的yaml已成为IDL的推荐方式。
消息的格式
进程间通信的本质是交换消息。消息通常包括数据,因此一个重要的设计策略是数据的格式。消息格式的选择会对进程间通信的效率、API的演化等方面产生影响。如对于类似HTTP的通信,需要选择消息的格式。有些通信方式,则指定了消息格式,如gRPC。对于消息格式,应尽量做到语言无关,这样才能在异构系统间进行使用。消息的格式可以分为两大类: 文本和二进制。
基于文本的消息格式
基于文本的消息格式主要有两种:JSON和XML。目前,JSON已经成为文本消息格式的标准。文本消息的特点是,它们的可读性很高,同时也是自描述的。这样的格式允许消息的接收方只挑选它们感兴趣的值,而忽略掉其他。因此,对消息结构的修改可以做到很好的向后兼容。
使用基于文本格式消息的弊端是消息往往过于冗长,消息的每一次传递都必须反复包含除了值之外的属性名称,这样会造成额外的开销。另外一个弊端是解析文本引入的额外开销。对于JSON消息来说,要消息发送方要进行序列化,在消息接收方要进行反序列化。因此,对效率和性能敏感的场景下,需要考虑基于二进制的消息格式。
基于二进制的消息格式
常见的基于二进制的消息格式有:Protocol Buffers和Avro。这两种格式都提供了一个强类型定义的IDL,用于定义消息的格式。编译器会自动根据这些格式生成序列化和反序列化的代码。因此开发者不得不采用API优先的模式来进行服务设计。笔者在使用的开发过程中,仅使用过Protocol Buffers,对两个的对比和选择,还请读者自行学习。
基于同步调用通信
当使用基于同步调用通信机制时,客户端向服务器发送请求,服务器处理该请求并发回响应,客户端阻塞并等待响应。基于同步的调用通信工作原理图如下:
在上图中,客户端中的业务逻辑调用代理接口,这个接口由代理适配器类实现。代理向服务器发请求。该请求由服务器适配器类处理看,该类通过接口调用服务的业务逻辑。然后将响应返回客户端代理,该代理将结果返回给客户端的业务逻辑。细心的同学可以发现,整个调用过程和HTTP的request-response模型一致。
这里,客户端的代理接口通常封装底层通信协议。有许多协议可以选择,主流的有REST和gRPC。
REST
如今开发者非常喜欢使用RESTful风格开发API。REST是一种使用HTTP协议的进程间通信机制,REST之父Roy Fielding曾说过:
REST 提供了一系列架构约束,当作为整体使用时,它强调组件交互的可扩展性、接口的通用性、组件的独立部署,
以及那些能减少交互延迟的中间件,它强化了安全性,也能封装遗留系统。
REST中的一个关键概念是资源,它通常表示单个业务对象,如订单,购物车等。REST使用HTTP动词(GET、POST、PUT、DELETE等)引用这些资源。该资源通常采用JSON对象的消息格式。RESTful风格已经成功Web API开发的事实标准,后面会写文章专门介绍下。
gRPC
使用REST的一个挑战是,由于HTTP仅提供有限数量的动词,所以无法使用REST承载不支持的场景,如设计支持多个更新的API。一种有效的替代方案是使用gRPC。gRPC是一种基于二进制消息的协议,可以使用基于Protocol Buffer的IDL定义gRPC API,这是Google公司用于序列化结构化数据的一套语言中立机制。
gRPC API可由一个或多个服务和请求/响应消息定义组成。除了支持简单的请求/响应RPC之外,gRPC还支持流式RPC。服务器可以使用消息流回复客户端。客户端也可以向服务器发送消息流。
基于异步调用通信
当使用基于异步调用通信机制时,消息通过消息通道进行交换。基于消息通道的异步调用通信工作原理图如下:
在上图中,消息发送方中的业务逻辑调用发送端接口,该接口封装底层通信机制。发送端由消息发送适配器类实现。该消息发送适配器通过消息通道向接收器发送消息。消息通道是消息传递基础设施的抽象。消息处理程序调用接收方业务逻辑实现的接收端接口来处理消息。
有两种类型的消息通道:点对点(Point to Point, P2P)和发布-订阅(Publish and Subscribe, Pub-Sub)。
(1) 点对点通道。生产者向正在从通道中读取的一个的消费者传递消息。服务使用点对点通道来一对一交互方式。
(2) 发布-订阅通道。消息发送方将一条消息发送给所有订阅的接收方。服务使用发布-订阅通道实现一对多交互方式。
使用消息机制实现交互方式
消息机制的一个有价值的特性是它足够灵活,可以支持所有的交互方式。一些交互方式通过消息机制直接实现,其他的则必须在消息机制之上实现。
实现请求/响应和请求/异步响应
当客户端和服务器使用请求/响应和请求/异步响应进行交互时,客户端会发送请求,服务器会返回响应。两者交互方式之间的区别在于,对于请求/响应来说,客户端期望服务器立即响应,而对于请求/异步响应,则没有这样的期望。消息机制本质上是异步的,因此只提供异步响应。但客户端可以阻塞,直到收到回复。
客户端和服务器通过交换一对消息来实现请求/异步响应方式的交互。原理如下图所示:
在上图中,客户端发送命令式消息,该消息指定要执行的操作和参数,这些内容通过服务器的点对点消息通道传递。服务器处理请求并将包含结果的消息发送到客户端的点对点通道。最后,客户端就可接收包含结果的消息。
实现单向通知
使用异步消息实现单向通道非常简单。客户端将消息发送到服务器所拥有的点对点通道。服务端订阅该通道并处理该消息,但服务器不会返回响应。
实现发布/订阅
消息机制内置了对发布/订阅方式的支持。发送方将生产消息并将其发布到由多个接收方读取的发布/订阅通道。接收方从该通道中消费消息。
实现发布/异步响应
发布/异步响应交互方式是一种更高级的交互方式,这种交互方式把发布/订阅和请求/响应这两种方式的元素组合在一起实现。客户端发送一条消息,在消息的头部中指定回复通道,这个通道同时也是一个发布-订阅通道。服务器将包含相关性ID的回复消息写入回复通道。客户端通过使用相关性ID来收集响应,以此将回复消息与请求进行匹配。
通信异常处理
分布式系统中,当一个服务向另一个服务发送同步请求时,永远都面临着局部故障的风险。因为客户端和服务器都是独立的进程,服务器可能无法在有限的时间对客户端的请求作出响应。服务器可能因为故障或维护的原因暂停。或者,服务器也可能因为过载而对请求的响应变得极其缓慢。除了服务器自身原因,网络原因也是一个不容忽略的因素,具体可参考笔者之前的文章。
对于同步请求,客户端因等待响应被阻塞,这可能带来的风险是在其他客户端或使用服务的第三方应用之间传导,并导致服务中断。
要通过合理地设计服务来防止在整个应用中故障的传导和扩散,这是至关重要的。每当一个服务同步调用另一个服务时,它应该使用限流、超时、熔断等技术来保护自己。
限流
限流是指限制客户端向服务器发出请求的数量。具体来说就是服务器设置一个请求上限,如果请求达到上限,后续更多的请求则会立即失败。
超时
超时是指限制服务器响应的时间。具体来说就是给服务器设置一个响应超时上限,如果服务器在规定的时间内没有响应,也会立即失败。
熔断模式
熔断模式是指监控客户端发送请求的成功和失败的数量,如果失败的比例超过了阈值,就启动熔断器,让后续的请求立即失败。在经过一定时间后,继续接收客户端请求,如果调用成功,则解除熔断器。
参考
微服务设计 Sam Newman 著, 崔力强 等 译
微服务架构设计模式 Chris Richardson 著, 陈斌 等 译
https://itdks.su.bcebos.com/307ea09e7ef34bbfa58b845bf0f3d74b.pdf 微服务架构开发及平台演进