我们做过一次代价很高的架构决策:一个 8 人的团队,为了"面向未来",把一个还没上线的系统拆成了 6 个微服务。结果前半年有三分之一的时间花在排查跨服务问题和维护发布脚本上,业务功能推进缓慢。拆回模块化单体之后,交付速度立刻回来了。这篇讲这次教训,以及我们后来用的判断标准。
微服务的账要算清楚
微服务买到的是"独立部署与独立扩缩容",付出的是一整套分布式基础设施,而且这套东西是日常持续付出,不是一次性投入:
- 服务发现、配置中心、网关、链路追踪、日志聚合——每一样都要有人搭、有人维护。
- 原本一次数据库事务能解决的一致性问题,变成需要补偿、幂等、重试策略。
- 本地起一个完整的开发环境变得困难,"改一行代码要跑三个服务"。
- 联调与发布从"发一次"变成"按依赖顺序发多次",回滚也要按顺序来。
对 5~15 人的团队,这些开销很可能超过模块化带来的收益——尤其是在业务边界还没稳定的时候,拆出来的服务边界十有八九是错的,而这时候再改边界,成本比改模块高一个数量级。
什么时候真的该拆
我们的判断标准是"出现证据才拆",而不是"提前规划"。四个信号,出现两个以上再考虑:
- 团队规模与节奏:20 人以上、且多个特性团队需要在同一代码库上并行发布,互相阻塞已成常态。
- 扩缩容诉求明显不同:比如搜索模块的流量峰值是订单模块的几十倍,绑在一起扩不划算。
- 技术栈必须分离:核心系统是 Java,而 AI 推理只能跑 Python,两边确实没法共处一个进程。
- 合规或安全边界要求隔离:例如支付核心与营销活动必须分属不同的网络与审计域。
模块化单体:把边界画在代码里
模块化单体的做法是"逻辑上分离、部署上合并":按业务域划分模块,每个模块有自己的入口、领域逻辑与数据访问,模块之间只能通过明确定义的接口交互。落到代码上就是三条纪律:
- 一个模块一个目录,对外只暴露 index(或对外接口层),内部实现不导出。
- 禁止跨模块直接查表:要用别人的数据,就调它的接口——这条是将来能不能顺利拆分的关键。
- 每个模块拥有自己的数据表命名空间,不允许出现"两个模块共写一张表"。
💡 实践建议
先用单体跑通业务,把模块边界在真实需求里磨清楚;等到某个模块明确需要独立部署时,再把它"整体搬出去"——因为接口已经存在,拆分往往只是替换调用方式加上一份独立流水线。反过来,先拆后磨边界的代价,我们付过一次,不希望你再付一遍。
如果确实要拆,先补这三件事
决定拆之后,别急着写服务。先把三件事准备好:① 统一可观测性(请求 ID 贯穿全链路、日志与指标集中查询),否则线上问题无从下手;② 发布与回滚的流水线(每个服务独立构建、独立回滚);③ 一套服务模板与约定(错误码、鉴权、重试与超时策略统一),避免每个服务长成不同的样子。
这三件事没准备好就开拆,等于把单体里的编译期错误,换成分布式环境里的偶发故障。我们现在的默认选择是模块化单体,只在有证据时按模块拆出服务;如果你正在为"该不该拆"纠结,可以把团队规模、发布节奏和当前痛点发给我们,一起算算这笔账——联系方式在这里。
