[译]我喜欢的微服务间通信模式

avatar

我喜欢的微服务间通信模式

微服务是有趣的,它可以建立可伸缩的、高效的应用程序架构。因此,所有主流平台都在使用它。如果没有微服务,就不会产生 Netfli、Facebook、Instagram 这些网络巨头。

然而,微服务系统设计的第一步是把业务逻辑分解为一些小模块,并将它们以分布式系统的形式进行部署。接着,你需要了解使它们互相通信的最佳方法。对了,微服务不仅是面向外部,或换句话说,是对客户端提供服务的,在同样的架构下,它有时自身也是客户端,可以访问其他的服务。

那么,如何使两个服务互相通信?有一种简单的办法,就是使用相同的 API,并把它设置为公共的接口。例如,如果公共接口是某个 REST HTTP API,那么其他服务无论如何应当通过它来实现互相通信。

这种办法很有效,我们来研究一下,有没有更好的方法。

HTTP API

我们从基础知识入手,毕竟这是非常有效的。从本质上来说,一个 HTTP API 的功能就是来回发送信息,就像你所使用的浏览器或类似于 Postman 那样的桌面客户端。

它使用了客户端-服务器模式,这意味着通信只能由客户端发起。它也是一种同步通信方式,一旦客户端发起通信,只有服务器进行应答,通信才会结束。

Classic Client-Server microservice communication

这种方法很流行,它就是我们浏览网页的工作原理。你可以把 HTTP 看作互联网的骨干,因此所有的编程语言都提供了 HTTP 相关的功能模块,令它成为一种流行的方法。

但这种模式并不是完美的,让我们来分析一下。

优点

  • 易于实现 HTTP 协议实现简单,由于所有的主流编程语言都提供原生的 HTTP 支持,开发者几乎不需要关注其内部的实现机制。它复杂的内部实现是被隐藏的,以类库的形式抽象出来,供程序员使用。
  • 合乎标准 如果你在 HTTP 协议的顶层加入类似于 REST 的架构(恰当的实现它),那么,你就创建了一个标准的 API,它有助于任何客户端快速了解如何跟你的业务逻辑通信。
  • 跟具体技术无关 由于 HTTP 的作用相当于客户端和服务器之间的数据传输管道,客户端/服务器使用什么技术都是无关紧要的。你可以使用 Node.js 编写服务端,同时使用 Java 或 C# 编写客户端(或其他服务)。只要它们遵循同样的 HTTP 协议,就可以互相通信。

缺点

  • 这样的通道令业务逻辑存在延迟 HTTP 是可靠的协议,但是,由于它是整个协议的一部分,所以就有若干个步骤确保数据正确传输。然而,这种协议也造成了通信的延迟(额外的步骤也意味着额外的时间)。考虑这样一种场景:3 个或更多的微服务需要互相传递数据,直到最后一个传递完成为止。换句话说,就是让 A 向 B 传输数据,再把数据发送给 C,然后才开始发回响应。除了每个服务内部的执行时间,由于 3 条通信管道的建立都需要时间,包括它在内,造成延迟。
  • 超时设置 虽然在大多数场景下,你可以设置超时的时间,但如果服务器占用太长时间, HTTP 协议默认此时关闭连接。“太长时间”是多长时间?这取决于你使用的系统配置和服务,无论如何,这存在一些问题,因此对你的业务逻辑造成了额外的限制:它需要快速执行,否则就无法实现。
  • 故障不容易解决 实现服务器的无故障并非不可能,但这需要额外的基础设施。默认情况下,客户端-服务器模式不会在服务器发生故障时通知客户端。所以当客户端知道服务器发生故障时往往已经迟了。像我说的那样,可以采取措施减少相关困扰,比如,使用负载均衡或 API 网关,但需要在客户机-服务器通信模式之外进行一些额外工作,才能实现。

所以,当你的业务逻辑是快速可靠的,而且使用它的客户端有多个的情况下,HTTP API 是理想的解决方案。当多个团队使用不同的客户端工作时,可以使用各自熟悉的通信通道,这是标准化的工作流程,所以它很有用。

如果需要多个服务互相通信,或服务内的业务逻辑包含耗时操作,就不应使用 HTTP API。

异步消息

这种模式就是在消息的发送者和接收者之间建立一个消息代理。

这确实是我最喜爱的一种实现多个服务互相通信的方法,特别是需要水平扩展平台的处理能力时。

Asynchronous communication between microservices

这种模式通常需要添加消息代理,所以就有一些额外的复杂工作要处理。不过,这样做获得的好处大于所付出的代价。

优点

  • 易于扩展 客户端与服务端直接通信的主要问题之一是,为了接收客户端发送的消息,服务器需要有空闲的处理能力。但是单个服务的可容纳的并行进程数量是有限的。如果客户端需要发送更多数据,服务器的处理能力也要相应提升。有时候会通过扩展部署服务的基础设施来满足,比如使用高性能的处理器或增大内存,但由于需要支付相应的成本,这种办法也存在局限性。相反地,你也可以继续使用低配置的机器,建立一些并行处理的副本。使用消息代理,可以将收到的消息分发到多个目标服务。因此你的那些副本可能收到相同的数据,也可能收到不同的消息,具体取决于你的需求。
  • 易于添加新服务 将新服务连接到工作流就跟创建一个新服务并订阅你需要的消息类型一样方便。发送者不需要知道它的存在,只需要知道发送的消息类型。
  • 更方便的重试机制 在消息代理允许时,如果由于服务器发生故障导致消息发送失败,不需要我们编写相关代码,消息代理就可以自动重发。
  • 事件驱动 这种支持创建事件驱动的架构是令微服务互相通信的最有效的方法。你可以编写代码,让微服务在数据即将传递时接到通知,而不需要让服务由于等待异步响应而堵塞,更不需要为了等待响应而轮询存储系统。如果能做到那样,就可以解决更多问题(类似于下一个传入请求)。这种架构令数据处理更快,资源使用更有效,通信体验更好。

缺点

  • 调试较困难 由于没有明确的数据流,没有需要处理的数据,调试数据流和测试有效负载的路径可能是一场噩梦。因此,若在收到数据时创建唯一的 ID,可以通过日志跟踪内部架构的相关路径,这是一个好办法。
  • 不存在直接响应 考虑到这种模式的异步性,当客户端发出请求时,唯一可能收到的响应是“已接收,当准备就绪,我会通知你”。服务端也有可能会验证请求模式,返回 400 错误信息(如果经验证是非法请求)。问题在于,客户端不能直接获取应用程序的业务逻辑返回的输出数据,这些数据需要另外的单独请求才能获取。另外,客户端也可以向消息代理订阅某些消息。当有响应消息时,客户端会立即收到通知。
  • 消息代理变为失效的单点 如果消息代理配置不当,它会对整体架构造成影响。此时你不得不对你几乎不了解如何使用的消息代理进行维护,而不是维护您编写的不稳定的服务。

这种模式确实很有趣,它提供了很大的灵活性。如果您希望一端产生大量消息,那么在生产者和消费者之间使用类似缓冲区的结构将增加系统的稳定性。

当然,处理过程可能比较慢,但有了缓冲区,对它进行扩展会方便得多。

使用直接的 Socket 连接

我们继续来了解一种截然不同的方式。我们还可以使用一种更快一些的技术,它的结构更加简单,不需要依赖 HTTP 协议进行发送和接收,它是套接字(Socket)。

Open channels with sockets for microservice communication

乍一看,基于 Socket 的通信跟基于 HTTP 的客户端/服务器模式很相似,但如果你仔细研究,会发现一些不同之处:

  • 对通信的发起者来说,此时协议变得更简单,所以速度变得更快。诚然,如果你需要更高的可靠性,就需要编写更多的代码,然而,这里不存在 HTTP 中的延迟现象。
  • 通信可以由任何一方发起,不是只有客户端可以发起通信。通过使用 Socket,当你打开通道,它可以保持其状态,直到你关闭它为止。把它想象为正在进行的通话,任何一方都能发起对话,不是只有打电话者可以发起对话。

结合上述内容,我们来快速了解一下这种方式的优劣:

缺点

  • 没有现成的标准 跟 HTTP 相比,基于 Socket 的通信似乎比较混乱,这是由于它没有类似于 HTTP 的 SOAP 和 REST 之类的标准。所以实际上,实现方可以定义协议的结构。这反过来又使得创建和实现新客户端变得更加困难。然而,如果你这样做的目的只是为了让你自己的服务互相通信,你其实是在实现自己定义的协议。
  • 容易令接收端负载过大 如果一个服务开始为另一个服务产生大量的信息,并令它处理这些信息,那么最终可能会导致第二个服务过载并崩溃。顺便说一下,这是前一种模式能够解决的问题。在这种模式中,发送和接收之间有微小的延迟,这意味着吞吐量可能会更高,但也需要接收端以足够快的速度处理一切需求。

优点

  • 它非常轻量级 实现基础的 Socket 通信不需要安装任何工具。这当然也取决于你使用的语言,但其中有些语言,类似于带有 Socket.io 模块的 Node.js,支持两个服务之间的通信,而且只要几行代码即可实现。
  • 支持优化的通信进程 由于你在两个服务间建立了持续开放的通道,它们都可以在消息到达时立即响应。它不像请求新消息的数据库连接池那样,数据库连接池使用的是反应式的方法,速度不会快。

基于 Socket 的通信是一种非常高效的服务间通讯方式。例如,当 Redis 以集群方式部署时,就使用了这种方法,因此它可以自动检测有缺陷的节点,并把它们移除。这得益于通信的快速和低成本(这意味着几乎不存在额外的延迟,而且对网络资源的占用很少)。

如果你能控制服务间通信的信息量,并愿意根据实际情况定制协议,可以使用这种模式。

轻量级事件

这种方式结合了头两种方式的特点。一方面,它提供了通过消息总线实现多个服务互相通信的方法,因此可以进行异步通信。另一方面,由于它只能通过通道发送轻量级信息,需要通过对相应服务的 REST 调用来为该有效负载添加额外的信息。

Lightweight events & hydration during microservice communication

当你需要尽可能控制网络流量时,或者当消息队列有包大小限制时,这种通信模式很有用。在那种情形下,最好的做法是尽可能简单化一切事物,仅在需要时请求额外信息。

优点

  • 在两方面都是最优的 使用这种方式,80–90% 的数据通过类似于 Buffer 的结构发送,因此也能带来异步通信提供的便利。并且,只需要一小部分网络流量通过低效、标准的、基于 API 的方式进行通信。
  • 关注最常见的场景的优化工作 如果你知道,大多数情况下,服务不需要为事件添加额外的信息,应该把它维持在最低水平,以便优化网络流量,把对消息代理的需求维持在较低水平。
  • 基础的缓冲区 通过使用这种模式,每个事件的额外的细节信息是保密的,并会离开缓冲区。这反过来就破坏了你需要为这些信息定义模式的情况下建立的结合体。在进行迁移或扩展时,保持缓冲区的“沉默状态”使选项的交换变得更容易(例如从 RabbitMQ 到 AWS SQS)。

缺点

  • 最终可能面临 API 请求过多的情况 如果你没有仔细研究,在一个不适合的项目中使用了这种方法,后果就是 API 请求的开销过大,这样只会导致服务的响应过程有所延迟。至于在服务之间发送的所有 HTTP 请求所增加的额外网络流量就更多了。如果你的实际情况是这样的,需要考虑改为完全基于异步通信的模型。
  • 需要两种方式的通信接口 你的服务需要提供两种不同的互相通信的方式。一方面,它们会实现消息队列所需要的异步通信模型,但另一方面,它们也需要类似于 API 的接口。由于两种方式有这么多不同之处,所以难以维护。

这是一种有趣的混合模式,需要付出一定的精力进行编码(这取决于你混合使用两种方法的需要)。

这可能是一种好的优化方法,你需要确保对于你的用例来说,消息负载需要附加额外消息的机会只占 10%-20%,否则,获得的益处不足以补偿编写代码所花费的精力。


令两个微服务通信的最好方式应当是能满足你的需求的那个。如果有性能、可靠性、安全性的需求,这些需求和相关信息就是你选择最佳模式的依据。

不存在一种“符合所有场景的模式”。即使你像我一样,酷爱某一种模式,但实事求是地说,你应当根据你的具体业务场景来选择。

话虽然这么说,但我们还是可以讨论“你最喜欢的是哪一个,为什么”的问题。你可以留言,我们一起来探讨令微服务互相通信的方法!

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏