六边形架构:使用事件驱动的无服务器实现松耦合 - Ellerby


当我们使用领域驱动设计构建事件驱动的无服务器架构时,我们最终会得到一组服务,这些服务清楚地按业务功能划分,并通过事件通道(例如 Amazon EventBridge)进行异步通信。这些架构带来了许多优点:松散耦合、独立可部署性、可测试性和降低复杂性等等。
然而,无论我们对领域的建模多么优雅,总有一些跨领域的基础设施依赖(例如事件通道本身)是无法消除的。
我们如何处理它们决定了我们是否得到一个松散耦合的乌托邦,或者一个杂乱无章的半独立服务。当你付出了 90% 的努力后,不要让这成为最后的绊脚石。
在本文中,我们将看到明确定义的服务端口如何解决最后 10% 的基础设施依赖关系,并带来松散耦合、独立部署/测试和提高开发速度。
 
跨领域的基础设施依赖关系
我们之前已经讨论过如何将 Amazon EventBridge 与强大的领域驱动设计相结合来创建此类架构(请参阅EventBridge Storming)。当我们以这种方式构建服务(或“微服务”)集合时,仍然存在一些跨领域的全局依赖关系。这些依赖项妨碍了独立开发服务,使集成测试变得困难,并且可以在我们的基础设施定义中创建复杂的代码。此外,当这种依赖关系不明确时,服务的部署顺序充其量可能成为一个谜,最坏的情况是一组竞争条件。
为了使这一点更加具体,让我们假设我们进行了一个EventBridge Storming 会话,通过将系统的所有业务事件收集到有界上下文中来了解电子商务网站的示例问题域。通过本次会议,我们将了解业务领域、属于它的事件以及如何将它们组合在一起。
但是出现了问题:跨领域的基础设施依赖关系
例如:

  • 我们可以在财务服务中实现这种复杂依赖关系的定义,并在人力资源服务中引用它。当它是 2 个服务时很好,但是 10 个服务都需要这个资源呢。
  • 人力资源服务的独立开发现在不再可能。公司的任何新开发人员都必须先部署自己的财务服务版本,然后才能在 HR 服务上进行开发——或者他们依赖于临时版本,可能无法直观地理解原因。
  • 如果你不能独立测试,你就不应该独立部署。人力资源服务的独立可测试性现在是不可能的;同样,我们需要部署一个独立的财务服务,或者冒着共享暂存资源不受控制的环境的风险——即使这些都是按使用付费和快速部署的无服务器服务。

 
六边形架构
端口和适配器,或六边形架构,是一种尝试使依赖项可互换的方法。可互换的方面允许简化开发和测试。此外,明确的端口规范可确保明确定义依赖关系。组件将它们的连接列为端口,并带有它们期望的协议的 API 规范。然后一个或多个适配器可以实现该协议。
回到我们的财务服务示例。此服务依赖于 EventBridge 总线和 Cognito 用户池。但是这些依赖项应该放在哪里呢?
如果他们使用一项服务,例如财务服务,他们将与它一起部署——但这是正确的吗?不,仅仅因为财务服务发生了变化,并不意味着应该部署 EventBridge 或 Cognito 用户池。
六边形架构的端口设计已经减轻了一些可能会产生的相互依赖,但这并不意味着我们应该随意选择一个主机服务。相反,我们应该将这些视为服务,非常简单的服务,因为我们的云提供商通过事件总线和用户身份验证为我们提供了抽象。
创建一个“全局”或“中央”或“基础设施”中心并将任何横切依赖项转储到其中是很诱人的,但是要避免这种诱惑。
正确遵循六边形架构的设计是:
将“EventBridge 总线和 Cognito 用户池”视为基础架构,非业务领域部分。
开发人员在构建他们微服务时,包括上述财务服务时,需要通过手动或使用脚本部署 EventBus 和 UserPool。
这些脚本可以作为 CI/CD 管道的专用部分,或者甚至更好地作为基础设施即代码模板中的条件块(如果开发,如果测试,如果生产)。或轻松集成到 sls-test-tools 等工具进行集成测试
 
你不能避免所有的基础设施依赖——但你可以选择如何处理它们
强大的领域驱动设计和事件驱动的面向服务的架构在帮助确保可管理和松散耦合的架构方面大有帮助。但是,会出现一些跨领域的基础设施依赖关系并模糊服务边界。如果这些不明确和孤立,它们会对系统设计、耦合、认知负载和部署顺序产生巨大影响。
采用具有明确服务端口的六边形架构方法,我们可以将这些明确化,并从部署、测试和开发中消除跨服务依赖关系。
我们应该使用 DDD 来避免依赖,但不要隐藏我们拥有的依赖。如果存在真正的基础设施依赖(例如事件总线、用户身份),我们应该为其设计——确保它不会打开通往复杂分布式单体应用的闸门。此外,不要将这些隐藏在任意主机服务中,而是创建小型、简单和有目的的服务来包含这些依赖项的配置、部署和测试。