写点什么

微服务通信设计模式

作者:俞凡
  • 2022 年 1 月 16 日
  • 本文字数:4899 字

    阅读完需:约 16 分钟

微服务之间的通信,需要根据业务需求和架构的实际情况选择合适的方案,基于 HTTP 的 REST API 是最常见的选择,但并不是唯一的选择,需要考虑复杂性、性能、可扩展性等方面的权衡。原文:My Favorite Interservice Communication Patterns for Microservices[1]

微服务很有意思,可以帮助我们创建可伸缩的、高效的架构,因此当前几乎所有主流平台都基于微服务架构系统。如果没有微服务,就不可能有现在的 Netflix、Facebook 或 Instagram。


然而,将业务逻辑分解为更小的单元并以分布式方式部署它们只是第一步。我们还必须理解怎样才能让服务之间更好的通信。没错,微服务不仅仅是面向外部的——或者换句话说,为外部客户服务——很多时候它们也是同一系统中其他服务的客户。


那么,如何使两个服务相互通信呢?简单的方案是继续使用呈现给外部客户的 API。例如,如果我们面向外部客户的 API 是 REST HTTP API,那么内部服务也可以通过这些 API 进行交互。


这是一个很合理的设计,但让我们看看有没有其他改进方案。


注:通信是基于商定的协议,微服务之间以及服务和客户之间的通信都是如此,始终确保协议一致的一种方法是在这些解耦的代码库之间共享描述这些协议的代码,可以是类、类型、模拟数据对象等,Bit[2]就是有助于实现这一目标的工具。


Bit 从源头独立的控制 TS/JS 模块,即使它们被部署到独立的远程主机上,也能维护它们之间的依赖关系,从而使得对某一模块的更新能够触发其所有依赖模块的持续集成。


HTTP API


HTTP API 毕竟是非常有效的设计,就让我们从这儿开始。HTTP API 本质上意味着让服务就像响应浏览器或者 Postman[3]这样的桌面客户端那样发送信息。


HTTP API 基于 CS 模式,意味着通信只能由客户端发起。这也是一种同步通信,意味着一旦通信由客户端发起,要一直等到服务端返回响应才会结束。


经典的 CS 微服务通信


因为这和我们访问互联网的方式一致,因此这种方法非常流行。HTTP 是互联网的支柱,因此所有编程语言都通过某种方式支持 HTTP 功能,从而使其成为一种非常流行的方法。


但这种方式并不完美,我们来分析一下。


优点

  • 容易实现。HTTP 协议并不难实现,而且所有主要的编程语言都已经对它提供了原生支持,开发人员几乎不需要担心其内部是如何工作的,复杂性被类库所隐藏和抽象。

  • 可以被标准化。如果在 HTTP 之上添加了 REST 之类的东西(正确实现的),其实就是创建了一个标准 API,允许任何客户端可以快速学习如何与我们的业务逻辑进行通信。

  • 技术中立。由于 HTTP 充当了客户端和服务器之间的数据传输通道,因此和两端的具体实现技术无关。可以用 Node.js 实现服务端,用 JAVA 或 C#实现客户端(或其他服务),只要遵循相同的 HTTP 协议,就能够彼此通信。


缺点

  • 额外的时延。作为 HTTP 协议的一部分,有若干个步骤确保了数据被正确发送,因此 HTTP 非常可靠。然而,该这样也给通信增加了延迟(额外的步骤意味着额外的时间)。因此,考虑这样一个场景:在最后一个微服务完成之前,3 个或更多的微服务需要在彼此之间交换数据。换句话说,需要让 A 向 B 发送数据,这样 B 才可以向 C 发送数据,然后 C 才能够发送响应。除了每个服务的处理时间外,还必须考虑在它们之间建立 3 个 HTTP 通道所增加的延迟。

  • 超时。虽然可以在大多数场景中配置超时时间,但默认情况下,如果服务器占用的时间太长,将导致客户端关闭连接。多长时间是“太长”?这取决于配置和当前的服务,但是总会有这么个时间。这为业务逻辑增加了额外的约束:需要快速执行,否则将失败。

  • 失败难以解决。解决服务器故障并不是不可能的,但是需要有额外的基础设施。默认情况下,如果服务器关闭,将不会通知客户端。客户端只有在试图访问服务器时才会意识到这一点,但已经为时已晚。有一些方法可以缓解这种情况,例如使用负载平衡器或 API 网关,但需要在 CS 通信之上进行额外的工作,以使其更可靠。


因此,如果我们的业务逻辑快速可靠,并且需要被许多不同的客户端访问,HTTP API 是一个很好的解决方案。多个团队在不同的客户端上工作时,可以基于一个标准、一致的接口通信,这会非常有用。


如果多个服务需要互相交互,或者其中一些服务中的业务逻辑需要大量时间才能完成,那么就不要使用 HTTP API。


异步消息(Asynchronous Messaging)


这种模式还包括了一个在消息生产者和接收端之间的消息代理。


这绝对是我最喜欢的多服务之间通信的方式之一,尤其是当我们需要横向扩展平台的处理能力时。


微服务之间的异步通信


这种模式通常需要引入消息代理,因此会增加额外的复杂性。然而,这样做的好处远不止于抽象。


优点

  • 容易扩展。客户端和服务器之间直接通信的一个主要问题是,为了让客户端能够发送消息,服务器需要有空闲的处理能力,但这受到单个服务可以执行的并行处理量的限制。如果客户端需要发送更多的数据,那么服务就需要扩容并拥有更多的处理能力。这有时可以通过扩展服务部署的基础设施来解决,使用更好的处理器或更多的内存,但总会有上限。相反,我们可以继续使用较低规格的基础设施,并让多个副本并行工作。消息代理可以将接收到的消息分发到多个目标服务,可以根据需求,让副本接收相同的数据或不同的消息。

  • 易于添加新服务。创建新服务、订阅希望接收的消息类型、将新服务连接到工作流,都会很简单。生产者不需要知道新服务,只需要知道需要发送什么样的消息。

  • 简单的重试机制。如果消息的传递由于服务器宕机而失败,只要消息代理愿意,可以自动继续尝试,不需要编写特殊的逻辑。

  • 事件驱动。异步消息可以帮助我们创建事件驱动体系架构,这是微服务交互的最有效方式之一。与其让单个服务因为等待同步响应而被阻塞,或者更糟的是,让它不断轮询某个存储介质来等待响应,还不如编写服务代码,以便在数据准备就绪时通知它们。当需要等待响应时,服务可以处理其他事情(比如处理下一个传入的请求)。这种架构可以更快的数据处理、更有效的使用资源和提供更好的整体通信体验。


缺点

  • 调试困难。由于没有明确的数据流,只是承诺消息会被尽快处理,因此调试数据流和数据处理路径可能会成为一场噩梦。这就是为什么通常需要在接收到消息时生成一个惟一 ID,这样就可以通过日志跟踪消息在内部系统中的路径。

  • 没有明确的直接响应。考虑到此模式的异步特性,一旦从客户端接收到请求,唯一可能的响应是“OK,收到了,一旦准备好,我会让您知道”。对于无效请求,还可以发送 400 错误。问题是,客户端不能直接访问服务端的执行逻辑返回的输出,而是需要单独请求。作为一种替代方法,客户端可以订阅响应消息类型。通过这种方式,一旦响应消息到达,客户端将立即得到通知。

  • 代理成为单点故障。如果没有正确配置消息代理,它可能会成为架构的问题。虽然不必忍受自己编写的不稳定的服务,但却被迫维护一个几乎不知道如何使用的消息代理。


这绝对是一个有趣的模式,并且提供了很大的灵活性。如果生产者端需要产生大量消息,那么在生产者和消费者之间有一个类似缓冲区的结构将增加系统的稳定性。


虽然处理过程可能会很慢,但有了缓冲区后,扩展将变得容易得多。


Socket 链接(Direct socket connection)


有时候我们不必依赖古老的 HTTP 来发送和接收消息,而是可以采用一些完全不同的路径,使用一些更快的技术,比方说 socket。


为微服务通信打开 socket 通道


乍一看,基于 socket 的通信很像在 HTTP 中实现的客户端-服务器模式,然而,如果仔细看,还是有一些区别:

  • 对于初学者来说,协议要简单得多,这意味着也要快得多。当然,如果希望提供可靠的通信,需要编写更多代码来实现,不过 HTTP 所增加的额外延迟在这里已经消失了。

  • 通信可以由任何一方参与者启动,而不仅仅是客户端。一旦打开 socket 通道,它会一直保持这种状态,直到被关闭。可以把它想象成一个进行中的电话,任何人都可以开始对话,而不仅仅是打电话的人。


话虽如此,还是来看看这种方法的利弊:


缺点

  • 没有真正的标准。与 HTTP 相比,基于 socket 的通信似乎有点混乱,没有任何结构化的标准(比如 SOAP 和 REST)。因此,需要实现方来定义通信结构。反过来又使得创建和实现新客户端有点困难。但是,如果只是为了自己的服务可以相互交互,那么实际上是在实现自定义协议。

  • 容易使接收端过载。如果一个服务产生太多的消息让另一个服务无法处理,那么可能会导致第二个服务无法承受并崩溃。这就是上一个模式解决的问题。在这里,发送和接收消息之间的延迟非常小,这意味着吞吐量可以更高,但也意味着接收服务必须足够快的处理所有事情。


优点

  • 轻量级。实现基本的 socket 通信只需要很少的工作和设置。当然,这取决于使用的编程语言,但其中一些,例如带有 Socket.io[4]的 Node.js,可以通过几行代码就实现两个服务的通信。

  • 非常优化的通信流程。由于在两个服务之间有一个长时间打开的通道,因此双方都能够在消息到达时作出反应。和拉取数据库来获取新消息的方式不一样,这是一个反射性的方法(reactive approach),没有比这个更快的方式了。


基于 socket 的通信是让服务彼此通信的非常有效的方式。例如,当部署为集群时,Redis 使用这个方法来自动检测失败的节点,并将它们从集群中移除。由于通信速度快且成本低(意味着几乎没有额外的延迟,并且只需要很少的网络资源),才可以做到这一点。


如果能够控制服务之间交换的信息量,并且不介意定义自己的标准协议,那么就可以使用这种方法。


轻量级事件(Lightweight events)


此模式混合了前两种模式。一方面,它提供了一种让多个服务通过消息总线相互通信的方式,从而允许异步通信。另一方面,由于它只通过该通道发送非常轻量级的载荷,并要求调用相应服务的 REST API 将额外信息与载荷结合起来。


微服务通信中的轻量级事件和 API 的混合作用


当我们希望尽可能控制网络流量,或者当消息队列有包大小限制时,这种通信模式非常方便。在这种情况下,最好让事情尽可能简单,然后只在需要的时候要求额外的信息。


优点

  • 两全其美。因为有 80-90%的数据通过类似缓冲区的结构发送,因此这种方法提供了异步通信模式的优点,并且只需要通过效率较低但标准的、基于 API 的方法来完成一小部分网络流量。

  • 重点优化最常见的场景。如果我们知道在大多数情况下不需要使用额外的信息来填充事件,那么将其保持在最低限度将有助于优化网络流量,并将消息代理的需求保持在非常低的水平。

  • 基本的缓冲区。通过这种方法,每个事件的额外细节都是保密的,并且远离缓冲区。这反过来又消除了在需要为这些消息定义 schema 的情况下可能有的耦合。保持缓冲区的“哑(dumb)”使它更容易与其他系统交互,特别是在需要迁移或扩展的情况下(例如从 RabbitMQ 迁移到 AWS SQS)。


缺点

  • 可能会有太多 API 请求。如果不小心为不适合的用例实现此模式,那么最终将面临 API 请求的开销,而这会增加响应服务的额外延迟,更不用说服务之间发送的所有 HTTP 请求所增加的额外网络流量了。如果面临这样的场景,请考虑切换到完全基于异步的通信模型。

  • 两倍的通信接口。服务必须提供两种不同的通信方式。一方面,需要实现消息队列所需的异步模型,但另一方面,还必须具有类似于 API 的接口。考虑到两种方法使用的不同,这可能会变得难以维护。


这是一种非常有趣的混合模式,考虑到需要将两种方法混合在一起,需要花费一些精力编写代码。


这可以是一种非常好的网络优化技术,确保对于对应用例的载荷混合请求只发生大约 10 - 20%的比例,否则带来的好处将不值得为其编写额外的代码。


微服务之间通信的最佳方式是提供了我们想要的东西的方式,可以是性能、可靠性或者安全性,我们必须知道想要什么,然后基于这些信息来选择最佳模式。


没有通信模式的银弹,即使像我一样更喜欢其中一种模式,现实的说,还是必须找到适应当前用例的模式。


References:

[1] My Favorite Interservice Communication Patterns for Microservices: https://blog.bitsrc.io/my-favorite-interservice-communication-patterns-for-microservices-d746a6e1d7de

[2] bit: https://github.com/teambit/bit

[3] Postman: https://www.postman.com/

[4] Socket.io: https://socket.io/


你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。微信公众号:DeepNoMind

发布于: 刚刚阅读数: 2
用户头像

俞凡

关注

还未添加个人签名 2017.10.18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
微服务通信设计模式