先交代背景。2022年3月,我在一家做B2B供应链SaaS的公司,团队9个后端加1个运维。系统是一个Spring Boot单体,68万行Java代码(不算测试),187张表,单库1.1TB。每次发版Jenkins排队40分钟起步,周五下午谁敢点合并,群里就有人发菜刀表情。
痛点是真的:Maven全量编译6分半,本地起服务11分钟,改一个订单状态机的枚举,测试跑25分钟。有次两个需求撞车,为了不互相影响硬等了三天。那会儿我刚读完Sam Newman的《Building Microservices》,在公司分享会上讲了一小时“领域边界”,讲得自己都信了。于是2022年4月开始拆。
拆完之后的47个服务,账是这样的
我不打算列每一条,挑几个对得上的数字:
| 指标 | 单体(2022.3) | 微服务(2022.11) |
|---|---|---|
| 线上服务数 | 1 | 47 |
| 下单链路P99 | 380ms | 1.9s |
| K8s节点 | 6台 4C8G | 41台 |
| 月云账单 | 约8000元 | 约5.6万元 |
| 一次完整发布 | 40分钟 | 3小时20分(12条流水线串行) |
| 参与人数 | 9 | 9(外加1个被我拖下水的运维) |
下单这个核心流程,从网关到落库跨了7个服务。出一笔异常订单,我得同时开6个终端敲kubectl logs -f,再对着Jaeger调用图人肉拼时间线。有个周五晚上,一笔12万的订单卡在“库存预占”和“订单创建”之间,原因是库存服务收到消息后重启,幂等没做,重试4次,预占了4份库存。我们修到凌晨两点,第二天早上又发现对账脚本跑不动了。
我们把三件事想反了
第一,把“代码耦合”当成了“进程耦合”。真正让我们慢的是订单模块直接读库存表、促销逻辑散在9个Service里,不是它们跑在同一个JVM。拆成服务之后,这些耦合变成了HTTP调用,反而更难看见——编译器不再报错,IDE跳不过去,只能靠人肉记忆。
第二,以为数据库可以“先不动”。47个服务共用一个1.1TB的单库,每个服务拿着全权限账号。这就好比给47个人每人一把仓库大门钥匙,然后指望他们只拿自己的货。后来真出事了,一个改用户中心的服务顺手update了订单表的一个字段,因为“反正以前也这么写的”。回滚花了4个小时。
第三,低估了基础设施的账。Istio 1.15我们上了,sidecar每个pod多吃50到80MB内存,链路延迟加大概1到2ms。41台节点里有4台是在跑可观测性和网关这些东西,跟业务没半点关系。这不是技术问题,是我当时根本没算过这笔账。
后来我看到Amazon Prime Video在2023年3月发的博客(作者Marcin Kolny),他们把视频监控服务从Step Functions微服务架构挪回一个单体,成本降了90%。更早的还有Segment,2018年5月宣布把140多个服务合并回一个单体,文章标题就叫《Goodbye Microservices》。读完我的感觉是——原来不是只有我们蠢。
合回去的6个月,我们是这样做的
一行业务代码都没删,全是搬。
第一步,先量化边界,别靠直觉。 我写了个Python脚本跑git log --numstat,统计过去12个月里哪些文件经常在同一次commit里被一起改(temporal coupling)。结果和架构图几乎是两回事:我们以为独立的“促销”和“结算”,共变率0.63;而已经拆出去的“发票服务”,一年里只有2次单独变更。同类分析可以用Adam Tornhill的code-maat。
第二步,用一个进程内的模块框架,先在单体里把边界划出来。 我们用的是Spring Modulith(1.0版本2023年8月发布),用@ApplicationModule标注模块,依赖关系写进代码。再配ArchUnit写断言,我加了7条规则,其中一条是禁止order包直接依赖inventory的internal包。CI里跑,越界就红,比开会吵架管用。
第三步,按“变更频率 × 团队边界”排序,一次只合并一个。 优先合变更最少的三个:发票、通知、对账。方法很土——把微服务Controller换成Spring Modulith的模块入口,Feign调用换成方法调用,新旧两套都留着,用feature flag切流量,观察两周再删旧代码。
第四步,数据库最后合。 严格说不算合,是把47个服务的数据库账号收回来,改成每个模块一个schema,权限逐表授。这一步花了5周,比我想的久。
最后剩了6个服务没动:对外支付网关(合规)、文件处理(独立扩缩容、吃CPU)、消息推送(双十一流量是平时40倍)、还有一个给三家大客户单独部署的定制服务。这6个留着的理由都能量化,剩下41个全合了。
什么情况下确实该拆
说了这么多不是反对微服务,是我现在有个不太一样的心智模型:微服务是组织架构的产物,不是技术架构的选择。
几条可以对着看的判断:
- 团队规模超过30人,并且已经在为发布窗口吵架。9个人的团队天天吵发版时间,那大概率是需求管理的问题,不是架构问题。
- 某个模块的扩缩容曲线和其他模块差5倍以上。
- 合规或安全要求物理隔离,比如卡数据、医疗数据。
- 发布节奏有硬冲突,一个模块一天要10次,另一个一个月2次,短期调不了。
这几条都占不上,又觉得单体痛,先做模块化单体(modular monolith),代价小得多。Shopify到今天核心还是一个Rails单体,2023年黑五当天顶住了每秒百万级的请求。Google 2023年在HotOS上有篇论文《Towards Modern Development of Cloud Applications》,核心观点是逻辑上的服务边界和物理部署单元应该解耦——你可以保留服务的开发体验,但把部署合并。这跟我们最后的做法基本一致,只不过我们是先踩了8个月坑才想明白。
最后补一句:那8个月不算白费。至少现在每个人都能说清自己负责的模块边界在哪、依赖谁、被谁依赖。只是这些边界现在画在代码里(ArchUnit规则加Spring Modulith的verify),不是画在K8s的yaml里。