17611538698
info@21cto.com

从单体架构到模块化单体架构:比微服务架构更明智的选择

架构 0 15 1天前
图片

导读:从单体架构到微服务,复杂性有时超过它的便利和强大特性。

微服务引入了分布式系统的复杂性,而大多数研发团队低估了这些复杂性:故障、协调阻力、可观测性蔓延与成本飙升。


大约在 2015 年左右,微服务架构成了主流。注意不是模式,而是主流。

从架构角度来说,要么采用微服务,要么就只能被淘汰。它的宣传语亦极具诱惑力:独立扩展、多语言持久化、团队自治——这意味着工程师可以独立发布产品,而无需等待运维的 Gary 合并他的 pull request。那些年很多技术会议都围绕微服务展开。而很多系统却因此变得更加糟糕。

并非所有项目都需要分布式模式。

有些项目确实需要——当面临真正的规模压力,组织边界与服务边界清晰对应,团队也足够成熟,能够承担运营成本而不至于不堪重负。但大多数项目呢?大多数是中型 SaaS 平台或内部工具,它们采用微服务仅仅是因为这种理念太过普遍,以至于不采用它就感觉像是技术上的失职。

现在我们看到的是收缩。但并非彻底撤退——没那么戏剧性——而是一种重新调整。

技术团队正在构建模块化单体架构,或者迁移回这种架构,他们那种沉稳的决心表明他们已经见识过一些事情。他们曾在凌晨三点调试分布式跟踪,他们眼睁睁地看着原本只需 11 分钟的部署流水线爆炸式增长到 40 分钟,因为现在有 19 个服务,依赖关系图看起来就像是嗑了药的人设计的神经网络。

这不是怀旧,这是算术。

“微服务税”,没人警告过你


他们在文章里没提到的是:微服务本质上是分布式系统的问题。就这么简单,而分布式系统正是简洁性走向终结的地方。

网络延迟会接踵而至——突然间,每个函数调用都要经过网络传输,到达负载均衡器,如果下游 Pod 正在重启,可能还要重试。部分失败就成了家常便饭。服务 A 成功,服务 B 超时,服务 C 因为有人在午休时间部署而返回 503 错误,现在你陷入了一种进退两难的境地:订单只完成了一半,用户不断刷新页面,担心自己的信用卡被重复扣款。

数据一致性?没了。你想要强一致性?当初就应该坚持进程内一致性。现在你只能用 Saga 模式、两阶段提交,或者求上帝保佑,用补偿事务来保证最终一致性,而这些事务本身也会失败。

你的部署流水线就像“鲁布·戈德堡机械”一样支离破碎。Helm Chart、Kubernetes 清单、服务网格配置……十五个代码仓库,每个仓库都有自己的 CI/CD 配置、版本控制策略,以及各自不同的集成测试失败率。团队的确可以独立发布,但他们需要不断协调,因为服务 Q 依赖于服务 R 在 2.1.3 版本中改变的行为,而没人记录下来——毕竟,在快速迭代、不断破坏的情况下,谁有时间去记录每一个内部约定呢?

接着,可观测性本身又成了一门学科。

那便是分布式追踪,跨集群的日志聚合。有些指标只有眯着眼睛仔细看,并且知道同事上个季度搭建的仪表盘才能理解。调试简直就是考古——追踪一个请求 ID 穿过六个服务、三个消息队列和一个 Redis 缓存,而这个缓存里的数据是否过期取决于 TTL 是否过期。

然后,基础设施成本飙升。你运行着服务网格、分布式追踪后端、集中式日志记录、密钥管理器,可能还有服务目录,因为没人记得 customer-prefs-svc 到底是干嘛的了。每个服务都需要自己的数据库——对吧?没错。但现在你有十七个 PostgreSQL 实例,你的云服务账单看起来就像个电话号簿。

CNCF 与 ThoughtWorks Radar 的报告都用咨询界委婉的说法表达了相同的观点:

团队系统性地低估了这些成本。尤其是在系统本身并不具备这些成本的情况下。尤其是在你只有七八名工程师时,却仅仅因为技术大会上的架构图看起来是微服务架构,就把应用程序拆分成微服务架构的情况下。

你会得到这样的结果——我已经见过太多次了,知道其中的规律——那就是:

  • 总体它们之间是紧耦合的。服务共享数据库模式。或者它们以同步方式相互调用,使得网络边界形同虚设。
  • 沟通过于频繁。因为你划定的界限有误——事实证明,“用户”和“偏好”确实需要放在一起——所以现在每个请求都需要四次往返。
  • 共享数据库。这是每个人都警告过的反模式,但它之所以会发生,是因为清晰地拆分数据模型比拆分代码库更难。
  • 协调工作量增加。站立会耗时更长,计划会议耗时更长。


这不是敏捷性,而是无意中造成的复杂性,外加更好的公关效果。

模块化单体架构:究竟是什么?


模块化单体架构是一个可部署的单一工件——一个进程,一个运行时——它被无情地、结构化地划分为具有强制边界的、定义明确的模块。

但它不是“一团乱麻”,那是懒惰的单体架构,所有东西都互相引用,依赖关系图简直就是一场有向循环灾难。模块化版本则应用了强有力的领域驱动设计。限界上下文不是理想状态,而是强制执行的。模块拥有自己的数据。它们公开明确的接口。内部实现保持内部,受到语言可见性规则或架构测试的保护,如果有人试图越界访问,构建就会失败。

实际操作:

  • 明确的领域边界。每个模块代表一个完整的业务功能:计费、库存、通知。而不是“工具”、“辅助”或“共享”模块。
  • 不存在环境状态共享。模块之间不会访问彼此的数据库或内部类。通信通过已定义的契约进行——接口、事件、显式发布的 API。
  • 高内聚,低耦合。共同变化的部分保持共存。独立部分保持独立,即使它们被编译到同一个制品中。
  • 单一部署单元:一个 JAR 文件。一个容器。一个版本控制项,一个部署项,一个回滚目标。


你可以通过以下方式强制执行:

  • 包可见性规则。Java 的模块系统。C# 的内部访问修饰符。以及你所使用语言提供的任何用于隐藏内容的机制。
  • 架构适应性函数。例如,如果有人添加了非法依赖项,ArchUnit 等工具就会导致构建失败。
  • 依赖倒置。模块依赖于抽象概念,而不是具体实现。
  • 责任明确。每个模块都由一个团队或个人负责其合同。


听起来很简单,因为它确实很简单。但简单并不容易——它需要自律。

为什么它的效果比你想象的还要好


进程内调用速度快得惊人:无网络连接。无序列化。无重试逻辑。


当一个模块调用另一个模块时,这是一个函数调用。耗时仅几纳秒。错误处理采用 try-catch 语句,而非断路器或隔离区。你无需分布式追踪就能找出失败原因——只需一个堆栈跟踪即可。一个堆栈跟踪,在一个进程中。

这并非小事。消除网络边界就消除了一整类故障模式。系统又恢复了可读性。

部署工作变得枯燥乏味(这是好事)


一个工件对应一条CI/CD 流水线。构建、测试、打包、部署。回滚只需一次操作——重新部署之前的版本。无需跨十二个代码库进行编排。不会出现“服务 F 使用的是 v2.3,但服务 G 需要 v2.4,所以我们陷入了这种奇怪的兼容性困境”。

版本控制也变得合理。对整个系统进行版本控制。破坏性变更指的是内部重构,而不是需要协调部署和向后兼容补丁的跨服务 API 迁移。

变更的提前期(DORA 指标,每个人都关心)得到了改善,因为你无需等待其他三个团队合并他们的变更,你的变更就可以上线。

领域建模走向现实


当所有环节都整合在一个流程中时,过早地提取资源就变得更加困难。你必须在划定界限之前仔细考虑所有相关的具体情况。这是一件好事。大多数团队过早地划定界限,仅仅基于对规模或团队结构的猜测,然后花费接下来的两年时间来应对由此产生的后果。

模块化单体架构有助于稳定领域模型。你会发现自然的分界线在哪里——不是你预想的位置,而是它们实际存在的位置,这些位置会通过使用情况、变更模式和性能分析展现出来。当你最终提取出某个服务时,是因为你掌握了证据:这个模块具有不同的扩展特性,或者这个团队的发布节奏确实存在差异,又或者这些数据需要进行监管隔离。

抽象概念更清晰、更稳定、更不容易出错。

微服务成为一种选择,而非默认设置。关键在于:一个构建良好的模块化单体架构已经为微服务架构做好了准备。


每个模块都是相互隔离的。它有自己的数据契约、领域逻辑和接口。当你需要提取某个模块时——真正需要,并且有证据支持——你可以:

  • 将模块移至其自身的存储库
  • 给它一个数据库
  • 将其封装在 HTTP 或 gRPC API 中
  • 更新单体架构以远程调用它


这就是正确运用绞杀榕模式的方法。你不是在重写整个世界,而是在压力下有选择地提取组件,并且有明确的理由:这个模块需要独立扩展,或者这个团队需要部署自主权,或者这个功能确实对延迟非常敏感。

风险急剧下降,因为你不再是凭感觉行事,而是根据实际需求做出反应。

数据实际显示了什么


这并非纸上谈兵。Shopify曾公开谈论过其微服务架构扩张带来的代价——数百个服务、协调开销拖慢了速度、服务间通信导致性能下降。GitHub的工程师也曾撰文探讨过类似的挑战。这些并非小型公司,而是规模庞大的平台,即便如此,他们也发现并非所有问题都需要分布式解决方案。

ThoughtWorks 的技术雷达——业内较为冷静的评估之一——曾多次指出,模块化单体架构是新系统的合理默认选择。不是备选方案,而是默认方案。只有在有充分理由的情况下,才能偏离这一基准。

内部平台团队(即那些在生产环境中运行数百个服务的团队)越来越多地反映,被动地而非主动地进行服务提取,能够提升稳定性并提高开发人员的效率。只有当不进行服务提取的代价超过进行服务提取的代价时,才应该进行服务提取。在此之前,你只是在为尚未实现的规模和尚未固化的组织边界进行预先优化。

规律是一致的:建筑设计应该遵循事实,而不是时髦。

何时真正需要微服务


模块化单体架构并非万能。在某些情况下,从一开始就采用分布式架构是合理的:

  • 大型独立团队。如果您拥有五十名工程师,并且产品边界清晰,那么独立部署能力或许值得付出协调成本。
  • 极高的扩展需求。真正的流量高峰需要对特定组件进行横向扩展,而不是对整个应用程序进行扩展。
  • 监管隔离。合规边界、多租户要求,这些都要求进行物理隔离。
  • 多语言需求虽然罕见,但确实存在:有时你确实需要用 Python 进行机器学习推理,用 Go 编写低延迟 API,而且两者都不能妥协。


本质的区别其实在于时机和意图。你选择分布式架构是因为迫于实际压力,而不是因为现在是 2026 年微服务仍然很流行。

值得提出的问题


向模块化单体架构的转变远比钟摆摆动更为微妙。它代表着架构思维的成熟——认识到复杂性是有代价的,分布式是一种工具而非最终目标,最佳架构是允许你将最艰难的决策推迟到拥有数据之后再做。

以前的问题是:“我们能以多快的速度过渡到微服务?”

更好的问题是,经验丰富的开发人员在周一早上面对一个全新的项目或一个需要重构的遗留系统时会问自己的问题:

“我们能在保持适应性的同时,尽可能地保持简单?”

这就是模块化单体架构大放异彩的地方。在混乱和过早优化之间的过渡地带,你构建的是现有系统,而不是你想象中三年后达到 Netflix 规模时需要的系统——而你肯定还达不到 Netflix 的规模。

构建单体架构,使其模块化。仅在必要时才提取服务,而不是在可以提取时才提取。

其他,都只是赶时髦而已。

作者:场长

评论

我要赞赏作者

请扫描二维码,使用微信支付哦。

分享到微信