先说个背景吧。2019年我们团队15个人,把原本一个能跑的单体商城拆成了23个微服务,当时用的Spring Boot 2.3,挂在Kubernetes 1.18上,每个服务限了512MB内存,专门搭了SkyWalking和Prometheus。拆的时候大家是很兴奋的,觉得网关、熔断、链路追踪一上,架构就高级了。我作为当时大力推动拆分的“技术骨干”,现在脸有点肿。因为这个拆完的结果是:我们的用户量没翻倍,问题倒是翻了三倍。
真正让我动摇的是今年3月的一次线上事故。用户下单后一直转圈,我们排查了快两个小时,最后发现是订单服务调用库存服务,库存服务又去调商品服务,商品服务再回调订单服务的提货券接口——整整7跳调用链。每跳平均多了40ms,7跳就是近300ms的额外延迟。而我们的数据库连接池还是在每个服务里各自配置的,库存服务一个慢查询,直接拖垮了它连接池里的所有线程,导致其他依赖它的服务跟着雪崩。这还不算完,那天最讽刺的是:等我们要回滚的时候,发现订单服务的旧版本有接口不兼容,必须连商品服务一起回滚。因为当时我们只做了服务级的版本控制,没做契约测试——文档倒是写了三十多页,没人看。然后我们就这么在凌晨2点,手动回滚了7个服务。
既然已经这么痛苦,我干脆花了两周时间,把这个服务又给拆回了三个业务模块:用户/商品/交易,部署成了三个大单体应用。不是双写迁移,就是简单地停掉部分服务,把代码合并,重启。中间还踩了一些坑,挺丢人的:一个定时任务原来在多个服务里各跑一遍,现在合并后重复触发;Redis中的key在服务间共享的命名格式有冲突;还有MyBatis的Mapper扫到了同名XML,直接启动失败。这些都花时间磨过去了。现在跑了一个半月,我截取了一组数据对比:同样一批业务请求,之前拆成微服务时P99延迟稳定在350ms至420ms,拆回单体后,P99降到了115ms左右;整个系统的内存占用从原来的12GB降到了4.2GB(单体和微服务的JVM堆都设置到最大4GB,微服务光基座开销就占了2/3)。更让我意外的是部署时间,微服务每次全量更新需要轮流重启23个服务,加上镜像构建平均要50分钟,而单体只需要一台机器、一个Jar包,3分钟搞定。
我说这些不是想全盘否定微服务。事实上,我们的业务搜索、推荐、支付这些模块之间几乎没有共享的数据库表,就是独立业务,拆出去是合理的。但问题在于,我们为了”解耦“而硬生生拆出来的订单、库存、商品、优惠券这四个服务,本质上就是同一个核心交易域的四个步骤。它们之间根本不存在独立的业务能力,只有通过远程调用来维持的脆弱关系。很多人聊微服务的优势喜欢说”独立部署“”故障隔离“,但如果你真的遇到过为了改一行日志而重新构建一个镜像、为了排查一个老接口问题要连跳7个服务、为了保持分布式事务的一致性而反复改补偿代码的场景,你就会理解:许多微服务的复杂度,其实是在用工程化的方法解决一个根本不存在的架构问题。
顺便说一句,我曾经也是那套正确治理的理论信徒,认为微服务能解决团队协作的边界问题。但现在我觉得,团队的沟通成本不是靠拆分服务就能消解的——代码里如果你都画不清模块依赖,服务间就更画不清了。它更像是把原先代码模块间的编译期耦合,悄悄地变成了运行期的网络故障。如果你问我什么情况下该考虑微服务?我的答案是:先看团队会不会因为改同一个模块而频繁吵架;再看数据库层面能不能按某个维度分库,不能被多个领域死死缠住;最后,也是最关键的一条——你们有没有足够的时间来维护监控、链路追踪、配置中心、网关这些基础设施。没有的话,请老老实实先写一个内聚的单体。
现在再回头看这个“拆回单体”的动作,其实并不是倒退。我反而觉得,它终于让我找到了系统该有的规模质感。我们核心域的三个大单体,内部模块间的调用方式全部改成函数直接调用,代码可读性比以前强了很多。如果你问我对开发团队有什么建议,我会说:别急着追新架构,先把你的业务复杂度量化出来——每天有多少请求量,要求多少延迟,团队能承受多少认知负担?如果没有这些数字的支撑,你所谓的“微服务架构升级”,很可能只是把你的单体做了一次距离更远的拆分而已。