微服务拆回单体实践:47个服务砍到8个,P99降67%,但我第2周就后悔了

🔑 关键词:微服务拆分,模块化单体,架构治理,Kubernetes,DevOps

📖 摘要:一篇带真实数据和踩坑的复盘:2023年我们把电商中台从47个微服务回收到8个模块化单体+2个独立服务。部署频率、P99、MTTR、云成本怎么变,哪些情况千万别拆,具体步骤和参数都在里面。

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,那玩意儿害人。

🏷️ 标签: