> “我们应该像Netflix一样做微服务!”
这句话开发者们或多或少都听过,甚至自己也曾喊出过。从初创公司到大型企业,都梦想着摆脱庞大的“遗留系统(Legacy)”,转变为敏捷组织,实现微服务架构(MSA)转型。然而,统计数据显示,相当一部分MSA转型项目以失败告终,或者反而创建了一个比以前更复杂的“分布式泥球(Distributed Big Ball of Mud)”。
为什么会这样呢?正是因为“未经准备的拆分”。
今天,我将深入探讨“模块化单体(Modular Monolith)”,这是在转向MSA之前必须经历的必修课,同时它本身也是一种优秀的架构。

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语句和划分领域边界开始,您觉得如何?那是通往微服务最快的捷径。
发表回复