项目从1个模块拆成8个微服务,然后我又合了回去

摘要:我们项目从 1 个 SpringBoot 单体拆成了 8 个微服务,用了半年。然后在接下来的一年里,分布式事务、调试地狱、运维成本翻倍,团队被折磨得够呛。最后我做了一个决定:合回去。不是退回到大泥球,而是用模块化单体的方式重新组织代码。这篇文章复盘了整个过程——当初为什么拆、踩了什么坑、为什么合、合回去之后怎么做的。

事情的开始:我们为什么要拆

2023 年底,我加入了一个做 SaaS 的项目。那时候项目还小,一个 SpringBoot 应用跑得好好的,用户量 5 万出头,QPS 不到 200。

有一天老板说:"我们要拆微服务。"

我当时第一反应是:好呀,拆了更专业,大家都在拆微服务,不拆就落伍了。

拆的理由听起来也挺合理的:

  • 用户量在涨,马上到 10 万了,以后肯定要扩
  • 团队从 3 个人扩到了 8 个,一个代码仓库 merge 经常冲突
  • 路演模块的慢查询偶尔拖慢整个应用,如果能独立部署就好了

现在回头看,这三个理由里只有第二个勉强站得住脚。用户量 10 万是什么概念?一个单体扛百万 DAU 都没问题。至于慢查询——加个索引的事,非要动架构。

但我们当时就是拆了。大家都觉得微服务是现代软件的标配,不拆显得不够"架构师"。

2026 年的面试风向已经变了。gankinterview.cn 的一篇文章提到,面试官现在问的是"你的业务量级真的需要微服务吗",而不是"你用过微服务吗"[1]。可惜两年前没人这么问我。

拆分过程:8 个微服务是怎么诞生的

我们照着 DDD 画了一圈领域边界,最后定了 8 个服务:

投资者服务 (investor-service)
路演服务 (roadshow-service)
企业服务 (company-service)
互动服务 (interaction-service)
通知服务 (notification-service)
数据服务 (data-service)
资料服务 (material-service)
网关服务 (gateway-service)

边界画得挺清晰的。每个服务有自己的数据库,用 Feign 做 RPC 调用,Nacos 做注册中心和配置中心,Sentinel 做限流熔断,SkyWalking 做链路追踪。

技术栈选得中规中矩,没什么花活:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848
    sentinel:
      transport:
        dashboard: sentinel:8080

拆的过程大概花了三个月。改代码、配 CI/CD、写 Dockerfile、调 K8s 部署,加班是常态,但大家干劲十足——毕竟在"做架构"。

上线那天,8 个服务在 K8s 里整整齐齐跑起来,Grafana 面板上每一个 Pod 的绿色指示灯,看着确实爽。

蜜月期之后,问题一个接一个

爽了大概两个月。

分布式事务的噩梦

第一个大坑是分布式事务。

上市公司预约路演的流程原来是一个 Service 方法里搞定:校验企业信息 → 创建路演场次 → 开通互动提问 → 发通知。数据库事务一包,要么全成、要么全滚。

拆了之后变成:roadshow-service → company-service(校验企业)→ interaction-service(开通互动)→ notification-service(发通知)。

四个服务,四个数据库,跨服务调一个 RPC 就断一个数据库连接。Spring 的 @Transactional 彻底废了。

我们试过 Seata 的 AT 模式:

@GlobalTransactional
public Roadshow createRoadshow(RoadshowDTO dto) {
    companyClient.verifyCompany(dto.getCompanyId());
    interactionClient.enableQA(dto.getRoadshowId());
    roadshowRepo.save(roadshow);
    notificationClient.send("路演预约成功");
}

看上去挺优雅的。但问题出在异常场景——互动服务超时了,Seata 回滚了路演创建和企业校验,但通知那边已经发出了"路演预约成功"的短信。用户收到通知去页面一看,路演不存在。

这就是最终一致性问题。SAGA 也好,TCC 也好,本质上都是把"保证不出错"变成了"出错后想办法弥补"。对电商这种场景来说,补偿逻辑写起来非常痛苦。

后来我读到 Matthias Patzak 的观点:"短期内模块化单体在开发速度和重构灵活性上具有压倒性优势。"[1:1] 这话一点不夸张。

一个 Bug 需要起 4 个服务才能调

有一次线上报了一个 Bug:投资者打开互动提问,提问提交成功了但路演页面显示"未开通互动"。

排查过程是这样的:

  1. 看 roadshow-service 日志 → 调用 interaction-service 返回了超时
  2. 看 interaction-service 日志 → 开通互动成功,回写状态时调 roadshow-service 超时
  3. 看网络 → 两个服务之间间歇性丢包

开发环境复现:启动 gateway、roadshow-service、interaction-service、company-service 四个服务 + Nacos + MySQL + Redis。本地 16G 内存跑不满。

整个过程花了将近两天,真正修代码只用了半小时。剩下的时间全在搭环境和追调用链。

运维成本翻了三倍

CI/CD 从 1 条流水线变成 8 条。每次升级依赖包要改 8 个 pom.xml。日志分散在 8 个地方,查一个问题要拼多个 ELK 查询。

gankinterview.cn 提供了一个量化对比[1:2]:百万级用户系统,模块化单体每月基础设施成本约 3000 美元,微服务架构约 12000 美元——差 4 倍。

我们没到那个量级,但运维人力成本是实打实的。原来一个后端运维就能搞定的事,拆完之后所有开发都要学 K8s、学链路追踪、学怎么排查分布式问题。团队只有 8 个人,实在不划算。

网络延迟和雪崩

原来一个方法调用是纳秒级。变成 RPC 后是毫秒级。

不只是延迟。有一次 data-service 跑一个周度统计报表,调用了 roadshow-service 的一个批量查询接口,返回数据量有点大,直接把 roadshow-service 的内存打满了。然后 gateway 的超时重试放大了流量,连锁反应把 interaction-service 也拖挂了。

这个就叫"分布式雪崩"。单体里一个 OOM 最多重启一下,微服务里一个 OOM 能拖死半个集群。

为什么决定合回去

2024 年底,我做了一个决定:合回去。

不是拍脑袋。看下面的表格:

维度 拆分前(单体) 拆分后(8 微服务)
部署时间 5 分钟 30 分钟(编排依赖)
新增一个跨模块功能 1-2 天 1 周左右
线上问题平均排查时间 30 分钟 2-4 小时
运维人力 0.5 人 1.5 人
CI/CD 流水线维护 1 条 8 条
生产事故(半年) 2 次 5 次+

数据摆在这儿,没什么好解释的。

核心问题是:我们团队 8 个人,业务耦合度高,没有任何一个模块需要独立扩缩容。拆分带来的收益(独立部署、技术栈自由)我们一个都没享受到,付出的代价倒是全吃了。

2026 年 51CTO 将模块化单体列为年度架构趋势,指出"微服务架构的过度拆分问题被广泛反思"[2]。这件事早两年就该反思了。

合回去之后:模块化单体怎么做

合回去不是把代码全倒回一个大 jar 包了事。那是退回到大泥球,不是解决问题。

我们做的事情是:

用 Maven 多模块保持边界

platform/
├── platform-investor
├── platform-roadshow
├── platform-company
├── platform-interaction
├── platform-notification
├── platform-common
└── platform-start (启动模块)

每个模块有自己的 packageinternal 子包。internal 下的类不对外暴露。模块之间通过 public 接口调用,编译期就能检查出越界引用。

这一步还借助了 ArchUnit 做架构测试:

@ArchTest
void modules_should_not_depend_on_internal_packages() {
    noClasses()
        .that().resideOutsideOfPackage("com.platform.roadshow..")
        .should().dependOnClassesThat()
        .resideInAnyPackage("com.platform.roadshow.internal..")
        .check(classes);
}

CI 里跑不通过就不让合并。

事务回归简单

原来跨模块调用就是一个方法调用,不需要 RPC,不需要分布式事务。Spring 的 @Transactional 又好使了。

如果将来真的要拆

边界已经画好了。哪天路演模块真的需要独立部署(团队变大、独立发布节奏),只需要把 platform-roadshow 抽出来,加一层 RPC 适配层就行。边界不需要重画。

什么时候该拆,什么时候不该拆

经过这一圈折腾,我自己的判断标准是这样:

该拆的情况:

  • 团队超过 50 人,多个小组并行开发不同业务域
  • 不同模块有完全不同的发布节奏(比如核心业务两周一发、后台管理一天一发)
  • 某个模块需要独立扩缩容(路演服务 QPS 1000,数据服务 QPS 5)
  • 技术栈确实需要异构(比如算法服务用 Python,业务用 Java)

不该拆的情况:

  • 团队不到 20 人,沟通成本本来就低
  • 业务强耦合,跨模块事务多
  • 没有独立扩缩容需求,整体 QPS 不超过 1000
  • 运维能力跟不上,团队没有专门的 DevOps

Martin Fowler 说的好:"除非系统复杂到无法作为单体管理,否则不要考虑微服务。"[1:3]

我以前觉得这是保守。现在觉得这是常识。

总结

一句话:模块化单体是小团队的甜点区。

微服务不是目的,是好架构的结果。架构的选择不是"哪种更高级",而是"哪种更适合你现在的阶段"。我们当时的问题不是单体不好,是代码写得烂。代码写得烂,拆成微服务只会变得更烂——只不过烂得更有层次感了。

如果再来一次,我会先把单体内部的模块边界理清楚,做好高内聚低耦合。等有一天,业务规模真的到了单体扛不住的时候,再拆也不迟。


参考:

作者:唐悦玮 | 公众号同名

从后端出发,用 AI 拓展到全栈的工程师。


  1. "微服务已死?2026 面试官开始推崇模块化单体": gankinterview.cn ↩︎ ↩︎ ↩︎ ↩︎

  2. "2026 技术架构新趋势:从微服务回潮到 AI 原生架构": 51cto.com ↩︎

posted @ 2026-07-04 17:26  唐悦玮  阅读(13)  评论(0)    收藏  举报