2023年4月我接手一个电商中台,代码仓库47个,K8s 1.25集群里跑着210个Pod,Istio 1.15,Prometheus 2.40,PostgreSQL 14,Redis 6.2,Kafka 3.3。 每天订单8万,峰值QPS 320,看上去挺唬人。 但发一次版要23分钟,周五失败率18%,P99 780ms,MTTR 47分钟。 新同学 onboarding 从3天拖到11天,改一个订单字段要动5个仓库、3个Helm chart。 老板问为什么慢,团队说微服务就这样。我当时也差点信了,后来发现不是微服务的问题,是我们把CRUD壳子当成了服务。
我先拉了14天OpenTelemetry 1.17+Jaeger 1.47的trace,采样10%,导出CSV,按调用量、错误率、数据耦合做了个矩阵。 结果很尴尬:47个服务里31个只被1个上游调用,22个共享同一个PostgreSQL库,7个Kafka topic其实是同一条业务链。 跨服务事务靠3个Saga补偿,补偿失败率0.7%,听起来小,但每天8万单就是560单要人工修。 我们内部算了一笔认知负载税:每多一个服务,值班文档、告警规则、CI配置、权限申请平均增加1.5人天/月。 47个服务就是70人天/月,12个人的团队根本扛不住。这个数比云成本更吓人。
于是决定合并,但不是拍脑袋回单体。 第一步,画依赖图,把调用量小于50次/天、且没有独立扩缩容需求的服务标红。 第二步,定边界。我不信DDD限界上下文直接映射微服务,至少在我们这里,事务边界比限界上下文更硬。 第三步,用Spring Boot 3.1 + Gradle 8.2多模块,模块之间只能走application service,禁止跨模块直接查表,ArchUnit 1.1写测试卡边界。 第四步,先合读,再合写,最后合库,每次只合1个模块,观察2周。 最后从47个Deployment降到3个Deployment,8个模块,保留支付网关和文件转码两个独立服务。
对比数据如下,都是2023年Q2到Q3的真实口径:
| 维度 | 47个微服务 | 8模块化单体+2服务 |
|---|---|---|
| 服务/模块数 | 47 | 8+2 |
| Pod数 | 210 | 24 |
| 部署频率 | 2次/周 | 14次/周 |
| CI时长 | 23分钟 | 11分钟 |
| P99 | 780ms | 230ms |
| MTTR | 47分钟 | 12分钟 |
| 月云成本 | 1.8万元 | 0.6万元 |
| 季度生产事故 | 18次 | 7次 |
| 数据库连接峰值 | 940 | 400 |
P99降了67%不是玄学,主要是少了5跳网络和3次JSON序列化。 但别高兴太早,第2周我就后悔了。
单体构建从4分钟涨到18分钟,测试从9分钟到26分钟,团队有人拍桌子说这是开倒车。 我也怀疑自己。后来开Gradle configuration cache、test fixtures、并行构建,把CI压到11分钟,还是比原来4分钟慢。 更麻烦的是,合库之后有个慢SQL把订单模块拖死,之前微服务至少只死一个服务。 我们加了pg_stat_statements、连接池HikariCP maximumPoolSize=50、每个模块独立catalog,才把隔离做回来。 所以模块化单体不是免费午餐,它把分布式复杂度换成了代码边界复杂度,后者更便宜,但不是零。
现在我的观点很直接:微服务是组织扩张的止痛药,不是技术先进奖。 服务数超过团队数×3,就要警惕认知负载税;两个服务共享数据库表且强事务一致,先别拆;没有独立部署频率、独立扩缩容、独立故障隔离这三个数,就别开新服务。 判断步骤我放这里:1)统计30天部署频率、变更失败率、MTTR、P99;2)用OpenTelemetry采样10%跑7天,生成调用矩阵;3)调用量小于50次/天且无独立扩缩容的,进入合并候选;4)合并时先合读、再合写、最后合库;5)用ArchUnit或Spring Modulith卡模块边界;6)每合一个观察2周,看P99、CI、MTTR三个数。 我们回迁后第4个月,又有团队想拆。我只问那三个数。答不上来,就继续模块化单体。 别把K8s当架构,它只是调度器;也别把微服务数量当KPI,那玩意儿害人。