今天这篇,我们主要讲解微服务架构究竟应该怎么进行服务间通信,同步通信和异步通信各有哪些问题,又应该怎么解决这些问题。
背景
微服务架构将应用程序构建为一组服务。这些服务必须经常协作才能处理各种外部请求。因为服务实例通常是在多台机器上运行的进程,所以它们必须使用进程间通信进行交互。
选择合适的通信机制是我们在进行微服务架构设计中很重要的架构决策。它会直接影响应用程序的可用性。
一个理想的微服务架构应该是在内部由松散耦合的若干服务组成,这些服务使用REST、GRPC等同步协议进行通信,或者使用异步消息队列进行通信。
同步通信机制
同步模式主要是客户端请求需要服务端实时响应,客户端等待响应时可能导致堵塞。
同步模式主流的有REST和gRPC这两种通信模式。
使用REST
REST是一种使用HTTP协议的进程间通信机制,如今的开发者也非常喜欢使用RESTful风格来开发API。
REST中的一个关键概念是资源,它通常表示单个业务对象,例如客户或产品,或业务对象的集合。
REST使用HTTP动词来操作资源,使用URL引用这些资源。例如,GET请求返回资源的表示形式,该资源通常采用XML文档或JSON对象的形式。POST请求创建新资源,PUT请求更新资源。
REST的好处和弊端
REST有如下好处:
- 它非常简单,并且大家都很熟悉。
- 可以使用浏览器扩展(比如Postman插件)或者curl之类的命令行(假设使用的是JSON或其他文本格式)来测试HTTP API。
- 直接支持请求/响应方式的通信。
- HTTP对防火墙友好。
- 不需要中间代理,简化了系统架构。
它也存在一些弊端:
- 它只支持请求/响应方式的通信。
- 可能导致可用性降低。由于客户端和服务直接通信而没有代理来缓冲消息,因此它们必须在REST API调用期间都保持在线。
- 客户端必须知道服务实例的位置(URL)。客户端必须使用所谓的服务发现机制来定位服务实例。
- 在单个请求中获取多个资源具有挑战性。
- 有时很难将多个更新操作映射到HTTP动词。
使用gRPC
gRPC API由一个或多个服务和请求/响应消息定义组成。服务定义类似于Java接口,是强类型方法的集合。
除了支持简单的请求/响应RPC之外,gRPC还支持流式RPC。服务器可以使用消息流回复客户端。客户端也可以向服务器发送消息流。
gRPC使用Protocol Buffers作为消息格式。Protocol Buffers是一种高效且紧凑的二进制格式。它是一种标记格式。
Protocol Buffers消息的每个字段都有编号,并且有一个类型代码。消息接收方可以提取所需的字段,并跳过它无法识别的字段。因此,gRPC使API能够在保持向后兼容的同时进行变更。
gRPC的好处和弊端
gRPC有几个好处:
- protobuf二进制消息,性能好/效率高(空间和时间效率都很不错)
- proto文件生成目标代码,简单易用
- 序列化反序列化直接对应程序中的数据类,不需要解析后在进行映射(XML,JSON都是这种方式)
- 支持向前兼容(新加字段采用默认值)和向后兼容(忽略新加字段),简化升级
- 支持多种语言(可以把proto文件看做IDL文件)
- Netty等一些框架集成
gRPC也有几个弊端:
- 与基于REST/JSON的API机制相比,JavaScript客户端使用基于gRPC的API需要做更多的工作。
- 旧式防火墙可能不支持HTTP/2。
- Protobuf二进制可读性差(貌似提供了Text_Fromat功能) 默认不具备动态特性(可以通过动态定义生成消息类型或者动态编译支持)
gRPC是REST的一个引人注目的替代品,但与REST一样,它是一种同步通信机制,因此它也存在局部故障的问题。
同步模式问题解决
分布式系统中,当服务试图向另一个服务发送同步请求时,永远都面临着局部故障的风险。因为客户端和服务端是独立的进程,服务端很有可能无法在有限的时间内对客户端的请求做出响应。
服务端可能因为故障或维护的原因而暂停。或者服务端也可能因为过载而对请求的响应变得极其缓慢。
客户端等待响应被阻塞,这可能带来的麻烦就是在其他客户端甚至使用服务的第三方应用之间传导,并导致服务中断。
要通过合理地设计服务来防止在整个应用程序中故障的传导和扩散。
解决这个问题分为两部分:
- 必须让远程过程调用代理有正确处理无响应服务的能力。
- 需要决定如何从失败的远程服务中恢复。
开发可靠的远程过程调用代理
网络超时:在等待针对请求的响应时,一定不要做成无限阻塞,而是要设定一个超时时间。使用超时可以保证不会一直在无响应的请求上浪费资源。
限制客户端向服务器发出请求的数量:把客户端能够向特定服务发起的请求设置一个上限,如果请求达到了这样的上限,很有可能发起更多的请求也无济于事,这时就应该让请求立刻失败。
断路器模式:监控客户端发出请求的成功和失败数量,如果失败的比例超过一定的阈值,就启动断路器,让后续的调用立刻失效。
如果大量的请求都以失败而告终,这说明被调服务不可用,这样即使发起更多的调用也是无济于事。在经过一定的时间后,客户端应该继续尝试,如果调用成功,则解除断路器。
基于异步消息模式的通信
使用消息机制时,服务之间的通信采用异步交换消息的方式完成。基于消息机制的应用程序通常使用消息代理,它充当服务之间的中介。
另一种选择是使用无代理架构,通过直接向服务发送消息来执行服务请求。服务客户端通过向服务发送消息来发出请求。
如果希望服务实例回复,服务将通过向客户端发送单独的消息的方式来实现。由于通信是异步的,因此客户端不会堵塞和等待回复。相反,客户端都假定回复不会马上就收到。
使用消息代理实现消息通道
每个消息代理都用自己与众不同的概念来实现消息通道。ActiveMQ等JMS消息代理具有队列和主题。
基于AMQP的消息代理(如RabbitMQ)具有交换和队列。Apache Kafka有主题,AWS Kinesis有流,AWS SQS有队列。一些消息代理还提供有更灵活的消息机制。
基于代理的消息的好处和弊端
使用消息有以下很多好处。
- 松耦合:客户端发起请求时只要发送给特定的通道即可,客户端完全不需要感知服务实例的情况,客户端不需要使用服务发现机制去获得服务实例的网络位置。
- 消息缓存:消息代理可以在消息被处理之前一直缓存消息。像HTTP这样的同步请求/响应协议,在交换数据时,发送方和接收方必须同时在线。然而,在使用消息机制的情况下,消息会在队列中缓存,直到它们被接收方处理。
- 灵活的通信:消息机制支持前面提到的所有交互方式。
- 明确的进程间通信:基于RPC的机制总是企图让远程服务调用跟本地调用看上去没什么区别(在客户端和服务端同时使用远程调用代理)。然而,因为物理定律(如服务器不可预计的硬件失效)和可能的局部故障,远程和本地调用还是大相径庭的。
消息机制也有如下一些弊端。
- 潜在的性能瓶颈:消息代理可能存在性能瓶颈。不过许多现代消息代理都支持高度的横向扩展。
- 潜在的单点故障:消息代理的高可用性至关重要,否则系统整体的可靠性将受到影响。一定要选择高可用的消息代理。
- 额外的操作复杂性:消息系统是一个必须独立安装、配置和运维的系统组件。