架构轮回:为什么我们兜兜转转又回到了"单体"?
前阵子,我在整理公司技术债的时候,突然发现一个有趣的规律:我们团队在过去八年里,把核心业务系统从单体拆成微服务,最近又开始把部分微服务合并回单体。这种”折腾”让我陷入了深思——这到底是技术的进步,还是我们在原地打转?
上周跟一位老同事喝咖啡,他苦笑着说:”咱们这是用八年时间,证明了老祖宗说的’分久必合,合久必分’。”这句话虽然有点自嘲,但确实道出了很多技术团队的真实处境。今天想跟大家聊聊这段经历,希望能给正在架构选型中纠结的你一些参考。
一、我们当初为什么急着逃离单体?
2016年,我所在的团队维护着一个庞大的Java单体应用。说实话,那段日子确实不好过。
记得有一次,产品经理想加一个短信通知功能,我评估需要三天。结果三天后,我沮丧地告诉他:”还需要两天。”原因很简单:用户模块、订单模块、通知模块的代码像意大利面一样缠在一起。我想在订单完成后触发通知,却发现订单服务直接调用了用户服务的实现类,而用户服务又耦合了配置中心的具体实现。牵一发而动全身,改一个小功能要通读十几个类的代码。
更崩溃的是部署。每次发布都是一场”仪式”——周五晚上10点开始,运维同学战战兢兢,开发同学严阵以待。因为哪怕只改了一行代码,也要把整个应用重新打包、测试、上线。有一次,一个前端的CSS改动,导致整个应用启动失败,全公司停摆两小时。CTO当场拍板:”必须拆!”
当时的单体应用大概长这样:
1 | // 典型的"大泥球"代码 |
这种代码结构下,技术债务呈指数级增长。新同事入职,光是搭建本地环境就要两天,跑通全流程需要一周。我们当时天真地以为:只要拆了,一切都会好起来。
二、微服务的美好承诺与残酷现实
2017年,我们开启了轰轰烈烈的微服务改造。Spring Cloud刚火起来,Docker和Kubernetes开始普及,一切看起来都那么美好。
我们按照业务领域拆分:用户服务、订单服务、商品服务、支付服务、通知服务… 每个服务独立仓库、独立部署、独立数据库。团队成员也按服务分组, ownership清晰。前三个月,大家确实感受到了微服务的魅力:发布频率从两周一次变成每天多次,技术栈也可以按需选择(用户服务用Java,推荐服务用Python)。
但蜜月期很快就结束了。第一个暴击来自一次核心链路故障。
那是一个周一的早上,订单创建接口突然大面积超时。我们排查了三小时,最后发现是用户服务的一个慢查询导致线程池打满,进而拖垮了订单服务。更讽刺的是,这个查询本身跟订单流程完全无关,只是因为订单服务需要查询用户信息。在单体时代,这最多是个慢接口;在微服务时代,它演变成了级联故障。
这次事故让我意识到:分布式系统最大的特点,就是它会以各种你意想不到的方式失败。
接着是数据一致性问题。在单体时代,我们用一个@Transactional注解就能搞定的事,现在需要引入Saga模式、TCC事务、最终一致性。我清楚地记得,为了做一个”下单减库存”的功能,我们写了将近200行补偿逻辑的代码,而在单体时代只需要5行。
1 | // 微服务时代的"简单"事务 |

这还只是业务复杂度。运维复杂度更是指数级上升:服务发现、配置中心、链路追踪、日志聚合、熔断降级… 每个微服务都需要这些基础设施。我们团队从8个后端开发,变成了”4个业务开发+4个运维开发”。更糟的是,每次排查问题,需要在Jaeger里追踪十几个Span,在Kibana里跨多个索引查日志,在Grafana里看几十个监控面板。
最讽刺的是,很多服务其实根本没必要独立。我们的通知服务,每天只有几百次调用,却占用着完整的RDS实例和Kubernetes Pod。商品服务因为业务稳定,三个月才发布一次,却要承受服务治理的全套开销。
三、我们正在回归的”新单体”
2021年,我们开始重新审视架构。经过深入分析,发现了一个尴尬的事实:我们60%的微服务,调用方只有一个;80%的服务,QPS不到100。这些服务存在的意义,更多是为了”符合微服务规范”,而非解决实际问题。
于是,我们开始了”有选择的回归”。但不是简单地回到2016年的”大泥球”,而是走向模块化单体(Modular Monolith)。
所谓模块化单体,就是在单个部署单元内,通过严格的模块边界来实现隔离。每个模块有自己的领域模型、自己的数据访问层,模块间通过定义良好的API通信,禁止直接调用实现类。技术上,我们使用Java 9+的模块系统(JPMS),配合Maven多模块项目结构。
1 | // 模块化单体中的订单模块 |

这种架构带来了意想不到的好处:
- 开发效率回归:本地启动从15个服务变成了1个应用,新同事第一天就能提交代码。
- 事务简单性:80%的场景又回到了熟悉的ACID事务,代码量减少了70%。
- 部署灵活性:我们采用”模块化编译、选择性部署”策略。核心模块(订单、用户)全量部署,边缘模块(报表、分析)按需编译成独立Jar,通过插件机制动态加载。
- 性能提升:模块间调用从RPC变成了本地方法调用,核心链路延迟降低了60%。
当然,我们也保留了真正的微服务。支付服务因为对接多个渠道商,独立部署更有利;通知服务因为需要弹性伸缩,也保持独立。但这些都是经过严格评估的,而不是为了微服务而微服务。
四、我的架构决策框架:什么时候该”分”,什么时候该”合”?
经历了这些年的折腾,我总结出一个简单的决策框架,现在每次架构评审都会用到:
优先考虑”合”(模块化单体)的场景:
- 团队规模小于15人(康威定律的反向应用)
- 核心业务流程需要强一致性
- 服务间调用QPS高于内部调用(说明边界划错了)
- 大部分服务只有一个调用方
- 团队DevOps能力较弱
考虑”分”(微服务)的场景:
- 某个模块需要独立技术栈(如AI模型服务)
- 需要独立弹性伸缩(如秒杀活动服务)
- 需要独立安全隔离(如支付核心)
- 多团队并行开发,且团队间有明确的业务边界
- 模块有明确的复用价值,会被多个业务线调用
关键原则:
- 从单体开始,但保持模块化:新项目我坚决主张从模块化单体开始,用Maven/Gradle模块+严格包结构约束。拆分的成本远低于合并的成本。
- 数据拆分是最后的手段:很多团队一上来就拆数据库,这是最危险的。数据关系是业务耦合度的最真实反映。如果两个模块需要频繁Join,它们就不该分开。
- 监控先行:在考虑拆分前,先确保你有完善的调用链监控。很多团队拆了后才发现,原来服务A每天调用服务B 10000次,延迟占比高达30%。
- 接受不完美:架构是演进而非设计出来的。我们的目标不是完美的架构,而是能支撑业务、让团队高效交付的架构。
五、写在最后:架构没有银弹,只有权衡
回顾这八年的架构演进,我最大的感悟是:我们不是在”选择”架构,而是在”管理”复杂度。
单体架构的复杂度在代码层面:耦合、混乱、难以测试。微服务架构的复杂度在系统层面:网络、分布式、运维。没有哪种架构能消除复杂度,只是将复杂度从一个地方转移到另一个地方。
现在的我更倾向于”务实主义”。当团队有人提出要新建一个微服务时,我会问三个问题:
- 这个服务能独立产生业务价值吗?
- 它有独立的伸缩和部署需求吗?
- 拆分后节省的维护成本,能抵消增加的运维成本吗?
如果答案都是”是”,那就拆;只要有一个”否”,我们就考虑在单体里加模块。
技术圈很喜欢造新词、追新概念。但作为经历过一轮完整周期的老程序员,我想告诉年轻的朋友们:最好的架构,是让团队能睡安稳觉的架构。不管是单体还是微服务,能稳定支撑业务、让开发效率最大化的,就是好架构。
前几天,我又遇到了2016年那个让我加班三天的产品经理。他问我:”现在改个短信通知要多久?”我想了想:”两个小时,包括测试。”他惊讶地问:”怎么这么快?”我笑了笑:”因为我们把通知服务又并回主应用了。”
他愣了一下,然后我们都笑了。这或许就是技术人特有的黑色幽默吧。
关于作者:一位在架构坑里爬进爬出多次的老程序员,目前专注于用”不那么时髦但管用”的技术解决业务问题。个人信条:代码是写给同事看的,不是写给机器看的。