比较服务间通信的技术 - ardalis


在分布式软件应用程序中,不同的服务或进程或应用程序经常需要相互通信。微服务和容器以及云原生应用程序的现代架构趋势都增加了应用程序将越来越多地部署为相关服务的集合而不是单个单体的可能性。这些应用程序可以通过多种不同的方式相互通信,每种选择都会带来一定的好处以及后果和权衡。让我们考虑选项并根据其相对性能、可扩展性、应用程序隔离或独立性以及复杂性来评估每个选项。另外需要CAP定理
 
案例场景:在评估下面显示的每种技术和模式时,请考虑用户尝试从 Web 应用程序 (A) 购买某些产品。Web 应用程序依赖于单独的系统 (B) 来获取产品目录信息,包括每个产品的最新定价。在结帐过程中,它需要查询产品目录以获取所购买商品的最新价格。暂时忽略这是否是电子商务系统的最佳设计,而只是考虑如何进行这种通信。
 
共享数据
传统上,许多公司将拥有一个数据库,应用程序都将连接到该数据库。数据库价格昂贵且任务关键,因此只要拥有其中一个,就可以更轻松地聘请专家来保护和优化它。今天,数据存储是可以轻松部署为任何单个应用程序或服务的一部分的商品,并且众所周知,使用数据库作为进程间通信的主要机制会对服务/应用程序的独立性产生很多负面影响。
在电子商务示例中,订单处理服务 (A) 和产品目录 (B) 将它们的数据保存在同一个数据库中。这意味着服务 A 可以简单地查询适当的表来获取完成客户订单所需的价格数据。

  • 性能

单个数据库可以为大量请求提供足够的性能,尤其是读取。但是,当表变大且未正确索引时,或者应用大量更新时,关系数据库的性能会受到影响。
  • 可扩展性

云提供允许单个数据库扩展到大规模,尽管这并非没有大量成本。
  • 应用隔离

使用共享的可变全局状态作为应用程序之间的集成方式的最大问题是它们都与共享状态提供程序(数据库)紧密耦合。任何时候数据库关闭,所有应用程序都会关闭,并且对数据库的任何更改都可能导致依赖它的任意数量的应用程序关闭。
  • 复杂

由于大多数 Web 应用程序至少需要在外部数据存储中存储一些状态,因此利用共享数据存储通常不会显着增加应用程序的复杂性。事实上,每个依赖于共享数据库的应用程序都可以像它是该数据库的唯一用户一样构建,但需要注意的是,它不能在不破坏其他应用程序的情况下对数据库进行任何破坏性更改。
 

直接 API 调用
当您需要其他服务提供的东西时,有时最简单的方法就是询问。在这种情况下,订单处理服务 (A) 可以对产品目录服务 (B) 进行同步 API 调用。这要求服务 A 知道服务 B,并且这两个服务同时可用。然而,这是一种相当简单的方法,不需要任何额外的服务或复杂性,如消息队列或总线。

  • 性能

直接 API 调用的性能在很大程度上取决于处理和完成调用的速度。对于冗长的请求,调用的同步特性会损害上游服务的性能(即服务 A 的响应时间至少与服务 B 的响应时间一样长)。
  • 可扩展性

由于服务 A 和 B 都可以横向扩展,因此该技术可以根据需要很好地扩展。
  • 应用隔离

在此方法的基本版本中,服务 A 依赖于服务 B。服务 B 签名的任何重大更改都需要更新服务 A。此外,如果服务 B 的网络位置发生变化,则必须更新服务 A。这些问题可以通过使用集中配置和/或网关模式来缓解。不能轻易缓解的是两个服务之间的时间依赖性。如果 B 不可用,A 也将不可用。
  • 复杂

这种解决方案往往比共享数据库更复杂,因为涉及多个应用程序进程。调试和测试可能会更加困难,并且应用程序失败的方式也更多。但是,总的来说,这是一种相对简单的方法。
 
具有异步轮询的直接 API 调用
对于较长的请求,初始 API 调用可以快速完成,但会提供一个位置标头,指示在哪里检查请求的状态。对状态端点的后续调用(如果成功)最终将导致资源或结果被请求。
在这个例子中,对于任何不能快速完成的请求,服务 B 可以返回一个 202 和状态端点的位置。服务 A 可以轮询状态端点(额外的标头可能指示在再次检查状态之前等待多长时间),最终取回它期望的结果(或超时或任何数量的其他错误状态)。请注意,如果需要,此模式可以批量应用于所有 API 调用,从而产生一致的后端方法。但是,更多情况下,它会根据需要添加到性能缓慢的端点。该模式也可以短路,如果例如请求的资源已经准备好(例如在缓存中可用),则直接返回资源,而不是返回 202 并强制调用者点击状态端点。
  • 性能

涉及轮询和多个请求的解决方案几乎总是比直接调用的解决方案性能更差。因此,由于需要更多 Web 请求,以及在资源准备就绪和调用者轮询获取资源之间发生的额外浪费时间,此解决方案将导致性能损失。较短的轮询间隔将减少这种浪费时间的程度,但会导致网络和系统的整体负载增加。
  • 可扩展性

使用这种方法可以提高服务 A 和 B 的可扩展性。服务 A 可能能够处理更多请求,因为它将有更少的传出请求被阻止等待响应(尽管这对于 HTTP 客户端代码中的现代异步模式来说不是一个因素)。如果服务 B 能够将处理卸载到其他资源,并且只负责快速返回 202 响应,然后处理状态检查,那么它可以扩展以处理更多请求。涉及的实际工作可能由根本没有耦合到服务 B 的其他进程完成。
  • 应用隔离

应用程序隔离评估与直接 API 调用相比并没有真正改变。如果有的话,情况更糟,因为除了需要发出初始请求的服务 B 上的特定端点之外,服务 A 现在还依赖于状态端点和服务 B 使用的轮询模式(标头等)。
  • 复杂

这种模式通过添加额外的端点和轮询行为增加了同步 API 调用的现有复杂性。
 
异步消息
一些应用程序采用消除服务之间所有同步调用的方法,而是选择对所有内容使用消息。虽然异步消息适用于发布状态事件和发出命令,但它们更难用于查询。许多利用 CQRS 的架构将在模式的命令部分使用消息传递系统,同时将查询作为同步调用。然而,这种方法将查询作为异步命令发出,然后等待直到指示查询处理的相应事件发生或立即返回并在响应到达时通过其他方式通知客户端查询的状态。
  • 性能

与直接同步模式相比,基于消息传递的系统对于单个调用的性能通常更差。此外,如果在队列中建立了大量请求,单个请求的性能可能会发生巨大变化,因为每个请求现在都必须通过队列才能开始处理。
  • 可扩展性

基于异步消息传递的系统往往具有极高的可扩展性,因为任意数量的服务都可以独立于彼此和请求系统来获取消息并处理它们。此外,使用消息总线返回响应进一步使应用程序彼此分离,从而更容易扩展系统。
  • 应用隔离

这种方法的一个好处是服务 A 和 B 之间没有直接的依赖关系。它们不依赖于彼此的 API 定义或数据库模式(然而,它们都依赖于一种通用的消息模式)。它们不需要同时可用。服务 B 可以短时间停机,而服务 A 可以继续不受阻碍地工作。
  • 复杂

异步处理本质上比同步处理更复杂,并且要求除了每个命令之外的每个查询都异步处理会导致更加复杂。在运行时诊断问题和调试都比使用更简单的系统更困难,并且通常需要更高级的日志记录和监控功能来实现这些系统的更好可维护性。
 
来自真实来源的直接 API 更新的本地缓存
最快调用另一项服务是您不必拨打的电话一样的调用。无需在每次需要一条信息时调用另一个服务,尤其是服务经常需要的信息,数据的本地副本可以存储在缓存中。这可以是内存缓存,也可以是像 Redis 这样的持久性存储,甚至是服务用于其自身持久性需求的相同数据库。
任何时候在缓存中找不到所需的数据时,都可以使用Cache-Aside 模式从“真实来源”服务请求它。缓存条目通常会给出一个过期日期,但为了更好地提高运行时性能(并避免让客户端请求支付更新缓存的成本),下游服务可以对消费服务进行 API 调用以更新其缓存版本数据随时更改。通过这种方式,缓存可以与其源数据保持同步,而不必需要短暂的到期或频繁的更新,至少对于“主要读取”类型的数据而言。
  • 性能

通过消除在绝大多数情况下在运行时调用服务 B 的需要,这种方法的性能超过了上面列出的 API 和基于消息的通信方法(并且可能与共享数据库方法大致相同,但可能更好,因为服务 A 的数据库不太可能受到来自其他应用程序的负载的影响)。
  • 可扩展性

对上游服务消费者的直接 API 调用只能扩展到目前为止。如果更新频繁且订阅应用程序的数量很大,服务 B 很快就会花费大量时间进行服务间调用来更新其他应用程序(这些应用程序可能需要也可能不需要此类持续更新)。但是,对于数据相对稳定的相对较小的系统,这种方法非常有效。
  • 应用隔离

这种设计使用了一种依赖倒置的形式。尽管服务 A 的数据依赖于服务 B,但服务 A 对服务 B 没有编译时或运行时依赖性(除非它使用缓存辅助模式,这是可选的)。相反,服务 B 需要了解服务 A 及其用于处理数据更新的 API。它还要求服务 A 在对其数据进行更新时随时可用,否则它将需要重试逻辑以确保最终的一致性,或者系统将需要求助于缓存超时以确保丢失的更新最终得到纠正。
  • 复杂

服务 A 的复杂性低于所示的大多数其他方法。如果没有 Cache-Aside 模式,服务 A 的运行就好像它需要的所有数据都在它自己的数据存储中是本地的(尽管它应该注意只读取而不是更新这些数据)。但是,它确实需要构建和公开 API 来更新其他只读数据,当更新发生时,服务 B 将使用哪些数据。
然而,服务 B 更复杂,因为它需要执行其职责范围内的任何操作,然后在发生会影响这些服务的缓存数据的更改时处理对所有订阅服务的更新。这可能比起初看起来更困难,因为它通常不像直接的 1:1 表映射那么简单。
 
来自真实来源的更新事件的本地缓存
与必须去获取数据相比,已经拥有数据仍然会更快。使用这种方式,设计和之前的一样,只是下游服务不再调用 API 来更新上游服务,而是简单地发布事件。这种方法具有前一种方法的所有优点,但大大简化了整体架构。不必构建、定义和使用 API,服务 A 只需要处理它感兴趣的某些类型的事件,而服务 B 只需要在发生某些更改时发布事件。
  • 性能

高,和上一个一样。
  • 可扩展性

像所有基于消息的系统一样,这个系统的扩展性非常好。
  • 应用隔离

服务 A 可以在没有服务 B 的情况下运行。服务 B 可以在没有服务 A 的情况下运行。在运行时两者都不依赖于另一个;两者都只取决于传递的消息的格式和所使用的消息总线的实现细节。
  • 复杂

这种方法比仅使用共享数据库更复杂,但服务 A 能够像在那种情况下一样轻松地读取数据(可能更容易,因为它可以完全控制数据结构并且不共享数据)与任何其他服务或应用程序的架构)。但是,服务 A 需要支持消息处理程序来检测更新并将它们应用于其本地数据存储。服务 B 只需要在进行更新时发布事件,这会增加一些复杂性,但通常比处理对多个应用程序上的多个 API 端点的调用更简单,正如前面的模式所要求的那样。
 
混合方法
一致性很有价值,可以帮助降低系统的复杂性。但是,您应该为了复杂性而牺牲所需的用户体验。如果您的大部分系统都可以使用一种技术,但少数服务会从使用另一种方法中受益,那么请务必使用正确的工具来完成这项工作。您可以随意混合搭配技术,但通常最好确定给定技术最适合的一类服务或行为。也许报告需要一段时间才能从 API 和轮询技术中获益。也许命令受益于纯粹的异步消息传递方法,而查询使用更具决定性的流程。如果存在可以遵循的既定规则,则可以减少不一致,因此编写新服务的新开发人员可以轻松确定要采用的适当模式。架构决策记录 (ADR)有助于记录和传达此类决策和政策。