🚀 通往微服务最安全的垫脚石:为什么应该先经历“模块化单体”?

> “我们应该像Netflix一样做微服务!”

这句话开发者们或多或少都听过,甚至自己也曾喊出过。从初创公司到大型企业,都梦想着摆脱庞大的“遗留系统(Legacy)”,转变为敏捷组织,实现微服务架构(MSA)转型。然而,统计数据显示,相当一部分MSA转型项目以失败告终,或者反而创建了一个比以前更复杂的“分布式泥球(Distributed Big Ball of Mud)”。

为什么会这样呢?正是因为“未经准备的拆分”

今天,我将深入探讨“模块化单体(Modular Monolith)”,这是在转向MSA之前必须经历的必修课,同时它本身也是一种优秀的架构。

image


1. ⚠️ MSA的幻想与现实:网络不是免费的

首先,我们需要明确我们为什么要转向MSA,以及在这个过程中我们忽略了什么。

MSA的承诺(理想)

  • 敏捷性: 各服务可以独立部署。
  • 可扩展性: 只有流量大的服务可以进行横向扩展(Scale-out)。
  • 技术自主性: A团队可以使用Java,B团队可以使用Python。

MSA的现实(成本)

然而,为了获得这些优势,需要付出的成本是巨大的。

  • 网络调用的复杂性: 函数调用(In-process)变为网络调用(RPC/HTTP)。需要处理失败、超时、重试逻辑。
  • 数据一致性问题: 数据库拆分后,事务管理变得困难。强制使用2PC或Saga模式等复杂技术。
  • 运维开销: Kubernetes、服务网格、分布式追踪等基础设施的复杂性呈指数级增长。

最大的问题是“错误的边界划分”。

在没有明确定义服务间边界(Bounded Context)的情况下,物理上分离服务器会导致服务间API调用混乱,形成“意大利面条式通信”。这会带来比单体系统性能更慢、管理更困难的最坏结果。


2. 🧩 什么是模块化单体(Modular Monolith)?

模块化单体指的是“部署单元是一个(Monolith),但内部结构像微服务一样严格模块化(Modular)的架构”

  • 物理集成: 作为单个JAR/WAR文件,单个二进制文件部署。
  • 逻辑分离: 内部包或模块被彻底分离。模块间的引用受到严格控制,禁止直接访问彼此的内部数据库表。

简单来说,它就像“住在同一个屋檐下,但房间完全独立的室友”


3. 💡 为什么应该先做“模块化单体”?(5个核心原因)

这是本文的核心。为什么不直接转向MSA,而要先经历这个阶段呢?

① 定义边界(上下文)的成本较低 🛠️

MSA的核心是“在哪里进行拆分”。然而,在对领域理解不足的早期阶段,很难准确地定义这些边界。

  • MSA: 分离代码并部署到单独的服务器上,但后来发现边界划分错误。重新合并或修改API的成本是巨大的。
  • 模块化单体: 代码在一个项目中。只需更改包结构或使用重构功能(Move Class)即可修改边界。犯错后回滚的成本几乎为零。

② 摆脱“分布式事务”的噩梦 💾

MSA最大的难题是数据一致性。如果订单服务和支付服务分离,它们就无法捆绑在一个事务中。

  • 模块化单体: 逻辑上模块是分离的,但物理上可以使用一个数据库(当然,最好是分离模式)。如果需要,可以直接使用强大RDBMS的事务功能。在业务逻辑稳定之后,再拆分数据库也不迟。

③ 100%获得重构工具的支持 ⚡

IntelliJ或Eclipse等强大的IDE在单个项目内的代码追踪和重构方面进行了优化。

  • 当问“谁调用了这个函数?”时,在MSA环境中,你可能需要grep代码或查看分布式追踪工具。
  • 在模块化单体中,只需Cmd + Click一次即可了解所有调用关系。这在开发生产力和代码质量维护方面产生了决定性的差异。

④ 在没有基础设施复杂性的情况下,只关注“模块化” 🏗️

实施MSA需要大量的DevOps工程资源,例如Docker、k8s、Istio以及多样化的CI/CD流水线。编写业务逻辑已经很忙了,还要操心基础设施。

模块化单体的部署流水线很简单。它允许你在没有基础设施复杂性的情况下,专注于实践和实现“良好架构(高内聚、低耦合)”

⑤ 没有性能损失(无网络延迟) 🚀

无论网络有多快,都无法超越内存中的函数调用。

模块化单体中,模块间的通信是简单的办法调用。没有序列化/反序列化(JSON Serialization)的开销,也不必担心网络超时。


4. 📝 如何进行转换?(实践指南)

仅仅声称是模块化单体是不够的。必须遵守以下原则。

Step 1. 按领域单元隔离包

必须放弃现有的分层架构(Layered Architecture: 将Controller、Service、DAO等结构集中在一起)。相反,按领域(功能)组织包。

  • Bad: com.mycompany.controllers, com.mycompany.services
  • Good: com.mycompany.order, com.mycompany.payment, com.mycompany.user

Step 2. 强制执行依赖规则(利用ArchUnit)

如果只是口头说“不要引用”,没有人会遵守。对于Java,可以使用ArchUnit等工具通过测试代码强制执行架构。

> “订单(Order)模块不能直接引用支付(Payment)模块。

> 只能通过公共接口(Event)进行通信。”

Step 3. 数据库逻辑分离

这是最重要的。一旦你连接(Join)了其他模块的表,模块化就失败了。

  • 按模块划分模式(Schema),或者通过给表名添加前缀来管理。
  • 如果需要其他模块的数据,不应通过连接,而应调用该模块的API(服务方法)来获取。

Step 4. 内部通信通过接口进行

模块间的通信必须严格通过公开接口(Public Interface)进行。实现(Implementation)应该隐藏为包私有(Package-private)。


5. 🎓 结论:模块化单体也可以是“目的地”

Shopify和Stack Overflow等大型科技公司也曾从MSA回归到模块化单体,或者将其作为核心架构来维护。

模块化单体不仅是通往MSA的“过渡性垫脚石”,对于许多组织来说,它也可以是“最现实、最有效的最终目的地”

请记住。

> “如果在一个进程中都无法干净地分离模块,

> 那么一旦拆分成微服务,地狱(Distributed Hell)就会降临。”

>

与其现在就构建k8s集群,不如从整理代码的import语句和划分领域边界开始,您觉得如何?那是通往微服务最快的捷径。



Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注