回想去年您在分布式系统中工作的时候,你可以考虑使用其他的东西比RESTful HTTP服务调用的组件之间的通信的本系统中的方法?
在微服务的世界中,服务间通信的问题产生了两个主要的解决方案。 第一种解决方案基于RESTful HTTP调用的使用,而另一种解决方案则围绕消息队列的使用。
通常,在做出此类设计决策时,正确的决策是基于对您的需求以及两种方法所涉及的权衡取舍的牢固理解。 在这篇文章中,我们将通过分析一些普遍存在的误解来提高对这些折衷的理解,这些误解通常是在证明一种方法优于另一种方法时给出的。
总览
理想情况下,服务是自包含在分布式系统中的,不需要彼此依赖即可完成工作。 这是在微服务架构服务之间的许多相似之处的一个对象在面向对象的系统。 但是,正如在两个域中都常见到的那样,这种情况不可能在100%的时间内出现。 结果,服务需要一种相互通信的方式。
为了介绍对服务间通信方法的讨论,让我们首先定义一些关键概念:
- 微服务 :软件开发方法和体系结构风格都着眼于将复杂的软件系统构建为具有定义明确的接口和操作的松散耦合且高度可维护的单功能模块的集合。 我们的讨论围绕此类系统中服务之间的通信。
- REST :API的架构设计模式,围绕资源的概念展开。 对于我们的讨论,足以理解REST体系结构中的通信通常是通过HTTP协议实现的,其中发送方直接调用使用者。
- 消息队列 :服务间通信的一种异步, 分离的形式,其中数据的发送者将消息放置在消息队列中,直到接收服务对其进行处理和删除为止。
在过去的几年中,随着组织越来越多地将其单片应用程序转换为微服务,微服务的普及度有了巨大的增长。 但是,有趣的是,大多数关于微服务的介绍性文章,教程和指南都使用同步RESTful通信模式实现了服务间通信。
Google趋势图随时间推移对搜索词“微服务”的兴趣。
这并不奇怪,因为这种通信机制最简单,并且以每种编程语言的库形式提供了广泛的支持,因此最容易被客户采用。 更不用说,由于RESTful系统依赖于资源概念,因此可以用一些非常直观的方式进行记录。 但是,正如马丁·汤普森(Martin Thompson)所说,
“同步通信是分布式软件的结晶”
通常在大多数微服务实现和数据管道中都被过度使用。
尽管为消息队列中的安全性,缓存和文档提供了较少的正式支持,但与RESTful HTTP调用相比,它们具有自己的优点。 消息队列不仅使发送者与接收者脱钩,而且还使系统具有更高的容错能力。 也就是说,即使个别服务中断,消息也可以保留在队列中。 这些服务可以在它们重新联机后继续从队列中读取消息。
归根结底,通常有充分的理由在分布式系统中同时使用这两种机制。 但是,在这篇文章中,我想集中讨论一些错误原因,这些原因通常是使用一种通信机制而不是另一种使用的。
消息队列会增加额外费用
“用于持久保存消息的消息队列基础结构的成本大大增加了系统成本。”
首先,HTTP通信通常会在没有消息队列的情况下通过负载平衡器进行路由,尽管它可能不那么重要,但无论如何都会产生一些成本。
其次,消息队列通常在比HTTP 轻得多的协议上运行,从而提高了基础架构的效率并降低了由此产生的成本。 当两个服务之间的通信量很大时,由于HTTP无法保持连接打开,因此为这两个服务之间发送的所有请求创建TCP连接和协商SSL / TLS的开销非常大。
另一方面,诸如高级消息队列协议 (AMQP)之类的协议 (许多消息队列技术基于该协议 )在开始时创建连接并将其持久化。 它们使消息在TCP连接顶部的服务之间流动,因此TCP和SSL / TLS成本只需支付一次。 该协议最大程度地减少了流经网络的字节数,非常适合需要交换大量小消息的系统。
AMQP协议的高级概述。
最后,需要弄清系统中的容错和故障的要求,以使有关成本的陈述正确。 如果您的系统对失败的传输具有较高的容忍度,则比使用消息队列要贵得多。 但是,如果您的系统对邮件传递和传输失败的容忍度较低,则必须选择以下两个选项之一:
- 开发和管理逻辑以处理服务本身内部服务之间的失败通信。
- 使用消息队列
选项1通常涉及使用其他基础结构组件,例如数据库/缓存系统。 此外,开发弹性和健壮的服务间数据传输机制所涉及的复杂性和复杂性水平将远远超过使用旨在解决该问题的现成工具的成本。 在这种情况下,选择不使用消息队列的组织将承担更多的费用,因此在这种情况下,此声明将不成立。
消息队列引入单点故障(SPOF)
消息队列为您的架构引入了全新的基础架构。 由于全权负责启用服务之间的通信,因此此类组件引入了庞大的SPOF。”
减去任何使其成为高可用性(HA)的架构问题,消息队列就是SPOF。 但是,他们当然不会在任何现有的微服务实现中引入 SPOF。 诸如负载平衡器和API网关之类的机制是SPOF,被认为是任何微服务架构不可或缺的。 它们也必须像消息队列一样被设计为可以实现任何微服务的HA。
像消息队列一样,API网关必须为HA,以避免成为SPOF。
但是,RESTful HTTP调用的一个缺点是服务必须直接从代表SPOF的发送方接收通信。 如果接收服务由于任何原因变得不可用,则该系统将丢失所有传输的数据,并且在无法继续执行该过程的意义上实质上会发生故障。
消息队列的优点在于它能够处理单个服务的停机时间。 也就是说,队列仍然可以保留由发送者服务发送给它的消息,直到接收服务再次可用并能够使用它们为止。 此外,与系统中的每个服务相反,我们只需要担心使消息队列具有高可用性。
与HTTP调用不同,消息传递是异步的
“与AMQP不同,HTTP是一种同步协议,可以阻止使用它的服务以异步方式进行通信”
HTTP当然是一个同步协议,但是术语“同步”和“异步”具有不同的含义,具体取决于它所使用的域,从而使诸如此类的过度简化变得太普遍了。 让我们从三个层面进行分析:
1-输入/输出级别 :在此级别,异步意味着对其他服务的请求在服务响应之前不会阻塞主执行线程。 这允许线程同时完成其他任务,并通过允许您处理更多请求来提高CPU的效率。
RESTFul HTTP调用可以在IO级别以同步和异步方式实现。
这种异步级别可以在RESTful调用和消息队列中实现,这使得上面的语句为false。 我在下面用Java 8中引入的CompletableFuture机制说明了一个示例,以对API进行异步HTTP调用。 这是异步的,因为请求在单独的线程中完成,并且不会阻塞主线程。
public CompletableFuture<String> get (String uri) {
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.build();
return client.sendAsync(request, BodyHandlers.ofString())
.thenApply(HttpResponse::body);
}
2-协议级别 :如前所述,HTTP是同步协议,因为客户端发出请求并等待响应。 另一方面,消息队列通常基于异步协议。 例如,RabbitMQ基于AMQP,该协议比HTTP协议的协议重量轻得多,并且可以“一劳永逸”的方式运行。 使用nucleon.amqppython库,我们可以通过以下方式编写代码以将事件发布到基于AMQP的消息队列中。
from nucleon.amqp import Connection
conn = Connection( 'amqp://localhost/' )
with conn.channel() as channel:
channel.queue_declare(queue= 'test' )
# This operation will return immediately
channel.basic_publish(
exchange= '' ,
routing_key= 'test' ,
body= 'Hello world!'
)
与HTTP调用不同,将消息发布到队列不会返回任何响应,而basic_publish方法会立即返回。 因此,上述关于异步性的陈述是正确的。
3-服务集成级别 :此级别的异步通信与微服务的设计有关,因此微服务在它们的请求/响应周期中无需与其他服务进行通信。 为什么? 因为归根结底,即使整个系统中的其他服务处于脱机状态或运行状况不佳,服务的目标还是对最终用户可用。
如果一个服务需要触发另一个服务中的某些动作,则应在请求/响应周期之外完成。 此外,如果服务依赖于另一个服务中的数据,则应使用最终的一致性跨服务复制数据。 这还具有能够将数据转换为您自己的“绑定上下文”语言的优势。
这里的重点是异步服务集成是一种体系结构设计决策,与所使用的特定通信机制无关 。 从理论上讲,消息队列可用于同步集成服务,在这些服务中,它们被设计为以有状态方式传递和等待消息队列中的消息。 当然,那将是一个非常糟糕的设计。
你同意吗?
我希望这篇博客文章能够阐明有关RESTful HTTP Calls Vs Message Queue辩论的一些常见误解。 我强烈感觉到,诸如此类的重要架构决策无法通过对所涉及技术的“表面层面”理解来做出,这就是为什么我试图在这一领域中加深我的理解。
如果您喜欢此帖子,请订阅我的邮件列表以接收将来的帖子通知,并在Twitter上分享。