是时候回到 Monolith 了吗?

2025-06-08

是时候回到 Monolith 了吗?

历史总是重演。一切旧事物都会焕然一新,而我见证了各种想法被抛弃、重新发现,然后凯旋而归,最终超越了当时的潮流。近年来,SQL 强势回归。我们又一次爱上了关系型数据库。我认为单体架构将再次迎来它的太空漫游时刻。微服务和无服务器是云供应商推动的趋势,旨在向我们出售更多的云计算资源。对于大多数用例而言,微服务在经济上几乎毫无意义。没错,它们可以缩减规模。但当它们规模扩大时,它们会以红利的形式获得回报。仅增加的可观察性成本就足以让“大型云”供应商赚得盆满钵满。

您可以在此处查看此帖子的视频版本:

我最近主持了一个会议小组,讨论了微服务与单体架构的比较。小组成员(即使是单体架构的拥护者)一致认为,单体架构的扩展性不如微服务。

对于亚马逊、eBay 等公司所取代的那些庞大的旧式单体应用来说,情况或许确实如此。它们的确拥有庞大的代码库,每次修改都很痛苦,而且扩展起来也极具挑战性。但这种比较并不公平。新方法通常比旧方法更胜一筹。但如果我们用更新的工具构建单体应用,是否能获得更好的可扩展性呢?

其局限性是什么?现代巨石又是什么样子的?

模量

要了解后者,您可以查看Spring Modulith项目。它是一个模块化单体应用,允许我们使用动态隔离的组件构建单体应用。通过这种方法,我们可以分离测试、开发、文档和依赖项。这有助于在极低的开销下实现微服务开发的隔离性。它消除了远程调用和功能(存储、身份验证等)复制的开销。

Spring Modulith 并非基于Java 平台模块化(Jigsaw)。它们在测试和运行时强制分离,这是一个常规的 Spring Boot 项目。它具有一些额外的运行时功能以实现模块化可观察性,但它主要是为了执行“最佳实践”。这种分离的价值超越了我们通常在微服务中所使用的价值,但也有一些权衡。让我们举个例子。传统的 Spring 单体架构通常采用分层架构,其中包含以下软件包:



com.debugagent.myapp
com.debugagent.myapp.services
com.debugagent.myapp.db
com.debugagent.myapp.rest


Enter fullscreen mode Exit fullscreen mode

这很有价值,因为它可以帮助我们避免层与层之间的依赖。例如,数据库层不应该依赖于服务层。我们可以像这样使用模块,有效地将依赖关系图强制朝一个方向发展:向下。但随着业务的增长,这样做意义不大。每一层都会充斥着业务逻辑类和数据库复杂性。使用 Modulith,我们的架构应该更像这样:



com.debugagent.myapp.customers
com.debugagent.myapp.customers.services
com.debugagent.myapp.customers.db
com.debugagent.myapp.customers.rest

com.debugagent.myapp.invoicing
com.debugagent.myapp.invoicing.services
com.debugagent.myapp.invoicing.db
com.debugagent.myapp.invoicing.rest

com.debugagent.myapp.hr
com.debugagent.myapp.hr.services
com.debugagent.myapp.hr.db
com.debugagent.myapp.hr.rest


Enter fullscreen mode Exit fullscreen mode

这看起来非常接近真正的微服务架构。我们根据业务逻辑分离了所有部分。这样可以更好地控制交叉依赖,各个团队可以专注于各自独立的领域,而不会互相干扰。这充分体现了微服务的价值,而且没有额外的开销。

我们可以使用注解进一步深入且声明式地强制分离。我们可以定义哪个模块使用哪个模块,并强制单向依赖。这样,人力资源模块将与发票模块没有任何关联。客户模块也是如此。我们可以强制客户模块和发票模块之间建立单向关系,并使用事件进行交互。Modulith 中的事件简单、快速且事务性强。它们可以轻松地解耦模块之间的依赖关系。这在微服务中是可以实现的,但很难强制执行。假设发票模块需要向其他模块公开一个接口。如何阻止客户使用该接口?

使用模块我们可以。是的。用户可以更改代码并提供访问权限,但这需要经过代码审查,这本身也会带来问题。需要注意的是,使用模块我们仍然可以依赖常见的微服务组件,例如功能开关、消息系统等。您可以在文档Nicolas Fränkels 博客中阅读有关 Spring Modulith 的更多信息。

模块系统中的每个依赖项都已映射并在代码中记录。Spring 的实现包含使用便捷的最新图表自动记录所有内容的功能。您可能会想,依赖项是 Terraform 存在的原因。那么,这种“高级”设计真的适合 Terraform 吗?

像 Terraform 这样的基础设施即代码 (IaC) 解决方案仍然可以用于 Modulith 部署。但它们会简单得多。问题在于职责划分。正如您在下图(取自此主题)中看到的那样,单体架构的复杂性不会随着微服务而消失。我们只是把这个棘手的问题推给了 DevOps 团队,让他们的工作更加困难。更糟糕的是,我们没有给他们提供合适的工具来理解这种复杂性,所以他们不得不从外部进行管理。

这就是为什么我们这个行业的基础设施成本不断上涨,而传统上价格应该呈下降趋势……当 DevOps 团队遇到问题时,他们会投入资源。但这并非在所有情况下都是正确的做法。

其他模块

我们可以使用标准 Java 平台模块 (Jigsaw) 来构建 Spring Boot 应用。这样做的好处是可以分解应用程序并遵循标准 Java 语法。但有时可能会不太方便。在使用外部库或将某些工作拆分到常用工具中时,这种方法可能效果最佳。

另一个选择是Maven 中的模块系统。该系统允许我们将构建拆分成多个独立的项目。这是一个非常便捷的过程,可以让我们免于构建庞大项目的麻烦。每个项目都是独立的,易于使用。它可以使用自己的构建流程。然后,当我们构建主项目时,所有内容将合并为一个整体。从某种程度上来说,这正是我们许多人真正想要的……

那么规模如何?

我们可以使用大多数微服务扩展工具来扩展我们的单体应用。大量与扩展和集群相关的研究都是针对单体应用开发的。由于只有一个移动部件:应用程序,因此流程更简单。我们复制额外的实例并进行观察。没有单个服务发生故障。我们拥有细粒度的性能工具,所有功能都以单一统一的版本运行。

我认为扩展比同等的微服务更简单。我们可以使用性能分析工具,合理估算瓶颈所在。我们的团队可以轻松(且经济地)搭建测试环境来运行测试。我们对整个系统及其依赖关系拥有统一的视图。我们可以单独测试单个模块,并验证性能假设。

追踪和可观察性工具非常棒。但它们也会影响生产,有时还会制造噪音。当我们试图解决扩展瓶颈或性能问题时,它们可能会让我们陷入错误的陷阱。

我们可以像在微服务中一样高效地使用 Kubernetes 来处理单体应用。镜像大小可能会更大,但如果使用 GraalVM 之类的工具,镜像大小可能不会太大。这样,我们可以跨区域复制单体应用,并提供与微服务相同的故障转移行为。不少开发者将单体应用部署到 Lambda 表达式中,我不太喜欢这种方法,因为它成本很高。但它确实有效……

瓶颈

但单体应用仍然会在数据库方面遭遇扩展瓶颈。微服务之所以能够实现如此大规模,是因为它们本身就拥有多个独立的数据库。单体应用通常只使用一个数据存储。这往往是应用程序的真正瓶颈。现代数据库的扩展方法有很多。集群和分布式缓存是强大的工具,它们使我们能够达到微服务架构难以企及的性能水平。

单体应用也不需要单独的数据库。使用 Redis 作为缓存时,拥有 SQL 数据库并不罕见。但我们也可以为时间序列或空间数据使用单独的数据库。为了性能,我们也可以单独使用数据库,尽管根据我的经验,这种情况从未发生过。将数据保存在同一个数据库中的优势非常巨大。

优势

我们可以在不依赖“最终一致性”的情况下完成事务,这是一个令人惊叹的好处。当我们尝试调试和复制分布式系统时,可能会遇到一个临时状态,这个状态很难在本地复制,甚至无法通过查看可观察性数据完全理解。

原始性能消除了大量网络开销。通过适当调整二级缓存,我们可以进一步消除 80% 到 90% 的读取 IO。这在微服务中是可能的,但实现起来会困难得多,而且可能不会消除网络调用的开销。

正如我之前提到的,微服务架构并不会消除应用程序的复杂性。我们只是把它移到了其他地方。就我目前的经验而言,这并非改进。我们添加了许多可移动的组件,增加了整体的复杂性。回归更智能、更简单的统一架构更有意义。

为什么要使用微服务

编程语言的选择是衡量微服务吸引力的首要指标之一。微服务的兴起与 Python 和 JavaScript 的兴起息息相关。这两种语言非常适合小型应用程序,但对于大型应用程序来说则不然。

Kubernetes 使此类部署的扩展变得相对容易,从而为本已增长的趋势火上浇油。微服务还具有相对快速地扩容和缩容的能力。这可以更精细地控制成本。在这方面,微服务被当作一种降低成本的方式出售给组织。这并非完全没有道理。如果之前的服务器部署需要功能强大(价格昂贵)的服务器,那么这种说法或许站得住脚。尤其是在极端使用率的情况下,例如突然出现极高的负载,然后又没有流量时,情况可能确实如此。在这种情况下,可以从托管的 Kubernetes 提供商那里动态(廉价地)获取资源。

微服务的主要卖点之一是其物流方面。这使得各个敏捷团队无需完全了解“全局”就能解决小问题。问题在于,它催生了一种每个团队“各行其是”的文化。在代码腐烂的规模缩小过程中,这个问题尤其严重。系统可能还能运行数年,但实际上却无法维护。

从 Monolith 开始,为什么要离开?

小组成员一致认为,我们应该始终从单体架构开始。单体架构更容易构建,而且如果我们选择微服务架构,以后还可以拆分。但为什么呢?

单个软件的复杂性,如果作为单独的模块,会更有意义,而不是作为单独的应用程序。资源使用和财务浪费的差异巨大。在这个削减成本的时代,为什么人们仍然选择构建微服务,而不是动态模块化的单体应用呢?

我认为我们可以从两个阵营学到很多东西。教条主义是有问题的,对一种方法的虔诚执着也是有问题的。微服务为亚马逊创造了奇迹。公平地说,他们的云成本已经覆盖了……

另一方面,互联网是建立在单体应用之上的。它们大多数都不是模块化的。两者都有通用的技术。我认为正确的选择是构建一个模块化的单体应用,并配备合适的身份验证基础设施,以便将来我们想要切换到微服务时可以利用它。

鏂囩珷鏉ユ簮锛�https://dev.to/codenameone/is-it-time-to-go-back-to-the-monolith-3eok
PREV
开源诱饵和转换
NEXT
暴躁的开发者宣言