领域分区:如何在微服务和单体之间找到健康的平衡 - Ashley


深入了解适合大多数中小型公司的架构模式:领域分区。
只要工程师一直在编写代码,就一直在讨论构建一组系统的最佳方法。两种最常见的模式是单体和微服务。它们都有其优点和缺点,但是否有其他选择可以寻求两者之间的平衡?我相信是这样——域分区服务。
 
什么是域分区?
在我们深入研究域分区是什么之前,快速介绍微服务和单体的标准架构很重要。
通常,微服务架构鼓励创建相对较小的服务,每个服务都有自己的存储库,所有这些都以某种方式通过网络进行通信——例如 HTTP 或通过消息代理。在频谱的另一端,单体应用通常是一个大型服务,全部位于单个存储库中,在内存中调用系统不同部分之间的通信。
那么,域分区如何适应?从本质上讲,它们是两者之间的中间地带。您最终会得到一组中型服务,而不是大量小型服务或单个单体服务,这些服务具有不同且明确定义的部分 - 分区。
让我们通过一个示例来看看使用我们的三种架构模式是如何实现的。在进行了假设域建模练习之后,我们定义了一个新域 — 购买域。它处理四个功能领域:付款、运输、退货和促销。
我们已经有许多其他域已经实施,例如,目录和客户域。
不要太担心这些域是否有意义,它们在某种程度上是为了说明这种模式。下面的架构也是如此,它们在依赖项方面绝对没有经过深思熟虑,但只是为了说明要点。
 
单体monoliths
在单体应用中,这很容易——所有与购买域相关的代码都将进入单体应用。至少我们希望每个子域都具有命名空间,但这可能是大多数单体应用所能达到的。
代码库的不同部分之间通常没有明确的界限,这会产生问题,包括整个单体的级联故障,以及模糊的所有权和关于谁拥有什么的冗长讨论。
 
微服务
在微服务架构中,会有很多选择,但很可能你最终会得到四个新服务。您可能决定将退货和运输结合起来,可能只会产生三项新服务,但无论哪种方式,我们最终都会提供许多新服务。
服务之间需要某种形式的通信,例如在付款后,需要请求发货。
 
领域分区
通过域分区,我们最终会在两者之间取得平衡。将创建一项新服务,在该服务中将有四个分区——每个子域一个分区。下面有更多技术层面的实现细节,所以我不会在这里介绍。
架构中的其他服务将通过 HTTP 或消息代理以类似于微服务架构的方式与购买服务进行通信,而分区本身通常通过内存调用进行通信。没有什么可以阻止通过消息代理进行通信,但是,如果有充分的理由这样做(例如,减少直接耦合,更快地处理 API 调用)
另一个重要方面是公开单个 API,通常通过 API 网关。这并不意味着每个分区不能有自己的内部 API,有自己的控制器,但外部服务会将它们视为该域的单个、统一的 API。

 
域分区带来什么价值?
我在上面提到了这种架构的一些好处,但值得更详细地讨论它们。

  • 架构复杂性

如果您曾经使用过微服务,您可能知道试图了解所有部分如何组合在一起的痛苦。当出现问题时,这尤其成问题:问题或错误究竟在哪里?
当您进入异步工作流时,问题会加剧,尤其是在可观察性较差的情况下。例如,什么服务消耗事件 X?
此外,新加入者经常被所有不同的服务所淹没,并试图了解它们是如何组合在一起的。通常,有一些图表可以尝试帮助解决这种情况,但它们通常已经过时或无法提供全貌。
另一方面,域分区服务仍然存在一些复杂性,因为我们仍在处理分布式架构中的许多服务。因此,与单体相比,它在架构上更加复杂。
  • 开发时间

根据我的经验,当您考虑广泛的场景时,域分区服务架构模式可以缩短开发时间。
我认为可以公平地说单体应用的开发时间是三者中最慢的,因为找出需要进行更改的位置以及了解更改的影响范围可能很棘手——尤其是需要做什么进行测试以确保更改后没有任何损坏?
如果更改特别小且范围非常好(例如,针对服务),那么微服务将略微超出域分区架构。
但是,如果您需要修改两个或更多服务,那么这将比域分区架构花费更多的时间。这就是为什么,根据我的经验,域分区架构在开发时间方面通常是最快的,因为更改经常跨越分区。在微服务架构中,这意味着修改两个不同的服务。
我要指出的最后一点是,单体应用通常具有庞大而缓慢的测试套件,这使得像 TDD 这样的实践难以遵循,而且通常很难在本地运行大部分测试套件。
微服务和域分区服务都不会遇到这个问题,但是它们的测试套件通常很快——我每天工作的服务的测试套件有四个域分区,在本地运行只需一分钟多一点。最慢的单个规范在几秒钟内运行,而我处理的单个规范可能需要超过 5 分钟的整体应用程序。
  • 部署

单体应用通常在三种模式中部署最慢,这通常是因为它们必须处理整个架构的所有问题——通常包括部署额外的云服务、编译资产等。
相反,微服务应该快速部署,因为它们应该很小,并且因为它们涵盖了非常具体的职责,需要部署的步骤很少,至少与单体相比是这样。
虽然域分区服务比微服务大,但它们的部署时间通常不会与微服务架构大不相同。唯一的区别是通过分区有比传统微服务更多的代码——这在大多数情况下不会显着影响部署时间。
也有例外,例如如果某个分区需要一些其他分区不需要的额外部署步骤,但总的来说,域分区服务和微服务之间的部署时间在我的经验中可以忽略不计。
  • 成本

工程师经常忘记的一个指标是运行服务的成本。考虑到当今大多数公司都在使用容器,我们将从这个角度来解决它。
如果您不处理大量吞吐量(让我们现实一点,大多数公司不必处理大量吞吐量),Monolith 实际上相对具有成本效益。这是因为您不需要部署多组服务,相反,您只需要为单体应用部署足够的容器来处理其所有工作负载。
然而,基于微服务的架构很快就会变得昂贵。每个微服务至少需要 2-3 个容器来确保高可用性,因此如果您有 10 个服务,那么至少需要 20-30 个容器用于生产,并且在考虑临时环境时可能会增加一倍。这只是开始,当您考虑还需要其他资源(例如负载平衡器)时。
与其他类别类似,域分区服务位于中间的某个位置。您可能需要比单体应用更多的容器,但没有微服务那么多,因为与微服务架构相比,我们的服务数量减少了。
 
概述
有许多领域和技术可以尝试分析任何架构,我已经涵盖了上面的主要领域。
这种架构的一个重要优点也值得强调,它能够相对轻松地将分区提取到自己的服务中。您需要或想要这样做的原因可能有很多,例如:
  • 您可能分区不正确,需要将一个分区移动到另一项服务。
  • 服务中的一个分区可能具有随着您的架构的发展而出现的特定需求。例如,一个分区需要处理比给定服务中的其他分区多得多的吞吐量。在这种情况下,提取该分区并扩大该服务的容器数量可能是有意义的,而不是扩大现有服务(这可能更昂贵)。

总的来说,我相信域分区服务是大多数中小型公司(绝大多数行业)的绝佳选择。它将领域置于架构的中心,使工程师能够快速完成他们的最佳工作,并且对于大多数用例和团队规模而言相对而言可扩展。
 
您如何决定如何分区您的域?
这种架构与其他架构的主要区别在于它的分区,因此确定如何分区至关重要。
对于每一项需要大量添加或更改功能的重要工作,这是一个考虑该工作在何处以及如何适应的机会。因此,其基本的领域驱动设计 得到了实践。
一旦进行了域建模练习,新功能应该去哪里通常就会变得很明显。以下是一些需要考虑的事项,按此顺序:
  1. 这个新功能是否属于现有分区?
  2. 这个新功能是否属于现有域?(很可能已经有支持它的服务)

如果其中任何一个的答案是肯定的,那么您可能已经找到了放置新功能的位置。如果两者的答案是否定的,您可能需要创建一个新服务!
还有一些其他考虑因素,例如新功能是否具有定制要求(例如,新的云服务,或者必须处理与任何现有服务/分区相比的大量吞吐量),这可能会影响您的决定。如果有令人信服的理由,那么决定为一个域提供两个服务而不是一个服务是完全合理的。
 
域分区的实现
这部分值得单独写一篇文章,在未来,我计划更详细地充实这一部分。现在,我想专注于架构的高级视图,但是,我认为该架构有一个非常重要的构造对其成功至关重要。
在域分区之间实施清晰而严格的边界至关重要。您如何执行此操作将因语言而异,例如在 Java中,每个分区可以为每个分区公开少量(最好是一个)公共类——其余的应该是包私有的,因此这些类在外部无法访问那个包裹。这种可见性配置然后为您提供了一个分区。
这确保了分区之间的清晰接口,并允许在不影响其他依赖的分区的情况下重构分区的内部。此外,如果需要将一个分区提取到另一个服务,任何依赖分区都可以轻松迁移以调用新服务而不是内存调用。
最后,API 网关将所有内容整合在一起。这意味着服务之间的集成点较少,每个都有自己的身份验证要求和配置。您最终仍会得到相同的端点(例如POST /payments和GET /promotions),但它们会从单个服务公开。
 
概括
我希望你觉得这篇文章有帮助。我从Shopify 解构单体应用[url=https://eng.uber.com/microservice-architecture/]的旅程[/url]、Uber 减少微服务数量的旅程、关于微服务问题的大量演讲和研讨会,以及我自己实现域分区的经验中汲取灵感。
这两篇文章都值得一读,因为它们正在解决类似的问题,尽管方式略有不同。Shopify 正在采用模块化单体方法,而 Uber 正在采用一种他们称之为面向领域的微服务架构的方法。
在 Uber 的案例中,他们仍然维护着大量的微服务,但它们被分组在一个 API 网关后面——类似于这里概述的方法,但有细微的不同,因为我们在大多数情况下将其分组为单个服务。优步可能出于其可扩展性要求而做出此选择,但大多数公司并没有这样的担忧。