微服务折腾两年后,我们退回单体架构的实践复盘
2026/9/16 9:06:53 网站建设 项目流程

1. 为什么逆行:一个“不合时宜”的技术决策

先说结论:我带着一个二十多人的研发团队,花了三个月时间,把一套已经运行了两年的微服务系统,重新合并成了单体架构。不是某个模块,不是某个子服务,而是整个核心交易链路全部退回。团队里有人说我开历史倒车,也有人等着看笑话,但系统上线稳定运行四个月后,所有人都在庆幸做了这个决定。

先交代下背景。我们做的是一款面向中小商家的小程序商城系统,典型业务包括商品管理、订单交易、库存扣减、会员积分、营销活动、支付回调这些模块。最开始的架构演进路径和大多数团队类似:单应用时代一年半,业务快速迭代,代码确实开始膨胀,发版越来越费劲,于是决定微服务化。按照当时流行的拆法,把系统拆成了商品服务、订单服务、库存服务、会员服务、支付服务、营销服务、用户服务、消息服务,一共八个核心服务,加上网关、注册中心、配置中心、监控平台,前后端联调整整折腾了两个月才跑通基础链路。

两年后的今天,这套系统遇到了很实际的问题:业务增长没有预期那么快,但团队维护成本却成倍增长。每个月光处理跨服务调用问题就要花掉大量时间,一次普通的全链路联调需要启动六个服务,环境问题频繁到让人崩溃。我们算了一笔账:微服务架构带来的这不是技术债的问题,而是技术杠杆用错了方向。

不是说微服务不好,而是它对团队规模、业务复杂度、技术储备有硬性门槛。我见过很多团队在拿单体架构的复杂度去硬扛微服务的成本,最后两边不讨好。这篇文章我不会劝你千万别用微服务,而是想拆解一下我们当时是怎么想的、踩了哪些坑、退出的时候又做了什么,让还在纠结“拆还是不拆”的人有个真实参照。

2. 当初为什么要拆:一个被饭局和KPI推动的架构决策

2.1 拆之前我们面临的真实痛点

当时业务确实到了一个瓶颈。代码量大概二十万行左右,十来个开发维护一个Spring Boot应用。痛点很典型:大家改同一个代码仓库,稍微多点并发就容易冲突;发布代码要排队,一个功能延期可能连累其他人的版本一起上线;线上出问题时,日志堆在一起,排查效率极低。

这些痛点是真的,但后来仔细复盘才发现,这些痛点是“研发流程问题”和“工程管理水平问题”,单纯通过架构拆分来解决,本质上是用一个复杂系统去掩盖另一个管理问题。

举一个细节:那时候我们的测试环境只有一套,代码合并进入主干后,任何一个人的半成品代码都可能让整个测试环境挂掉。本来应该是流水线、分支策略、环境治理来解决的事,当时想着拆成微服务之后,各个团队可以独立开发、独立发布、独立测试环境,问题就自动解决了。

2.2 微服务架构的选型过程与设计思路

我们确定的拆分方案是:按业务域拆分为八个服务,Spring Cloud Gateway做统一入口,Nacos做注册与配置中心,OpenFeign做服务间调用,Sentinel做流量治理和降级熔断,Seata处理分布式事务,链路追踪采用SkyWalking,消息中间件用的是RocketMQ。这套组合在当时的Java微服务技术栈里非常主流,几乎零争议地通过了内部技术评审。

服务间调用关系倒是梳理得很清晰。核心链路是:客户端请求经过Gateway,先调用用户服务做登录鉴权,然后调用商品服务获取商品信息,接着调用库存服务预占库存,下单请求转发到订单服务,订单服务再通过RocketMQ发消息通知库存服务和会员服务做异步处理。看起来多标准,流程图也画得很漂亮,但实际上线后问题接连不断。

2.3 拆分的代价:基础组件学习成本猛增

团队为了上微服务,先是全员突击学习了Spring Cloud Alibaba生态。Nacos怎么搭集群、Sentinel怎么配限流规则、Seata的AT和TCC模式有什么区别、SkyWalking怎么部署探针,这些都是过去写单体应用不需要考虑的东西。前后花了两周时间做技术培训,才勉强做到能上手干活。

但培训只是最浅的成本。真正的成本是这些人从此需要花大量精力去维护和排查基础设施层面的问题,而不是安心写业务代码。微服务架构是一个永远需要人在旁边盯着运转的系统,每个组件都可能成为新的故障点。

我们曾经遇到过一次比较典型的故障:Nacos注册中心因为GC停顿导致心跳超时,触发了大规模服务实例下线,结果所有服务之间的调用关系瞬间断裂,线上直接雪崩。排查了整整半天,最后发现源头就是一个参数配置不合理。这种问题在单体架构里几乎不存在,但在微服务架构里,成了家常便饭。

3. 微服务运行两年后:账算得清清楚楚

3.1 基础设施与人力成本的直接推演

我用一张表来对比一下同一个业务体量在两种架构下的直接成本差异。这里的数据是两年来的平均值,可能不完全精确,但足够说明问题。

项目单体架构时期微服务架构时期
应用服务器数量2台8C16G12台8C16G(各服务至少双实例)
中间件依赖MySQL、RedisMySQL、Redis、Nacos集群、RocketMQ、Seata、SkyWalking、Sentinel、Gateway
部署一套测试环境耗时10分钟1小时以上
全链路接口联调成员前后端各1人至少涉及4个服务端开发
线上故障定位平均耗时30分钟3小时起步
日常运维参与人数半个兼职一个专职+一个兼职

这个表不是用来吓唬人的,但微服务架构的底层逻辑就是这样:为了获得独立部署和弹性伸缩能力,需要付出与之匹配的基础设施成本和运维人力成本。

我们的业务峰值QPS也就几百,每天订单量最高的时候也就几万单。这样的体量根本没有达到必须通过水平扩展来支撑的地步,而且一个优化良好的单体应用配合MySQL读写分离、Redis缓存、消息队列削峰,完全能够从容应对。我们用十二台服务器跑着一套本来四台服务器就能跑得动的业务,分摊下来每个月多付的机器成本都还是小事,多出来的人力和时间成本才是真正赔不起的。

3.2 分布式事务:微服务架构的隐形吞噬者

订单创建涉及生成订单、扣库存、加积分三个操作。单体时代就是一个本地事务注解的事,要么全成功,要么全回滚,数据一致性有数据库事务保证。拆成微服务之后,跨三个服务的数据一致性成了大难题。

我们用了Seata的AT模式,这个方案在初期确实解决了问题,但代价是所有参与事务的服务都要建立undo_log表,每个业务操作都要额外记录前后镜像。亚特兰大模式本身性能损耗不小,高并发下事务耗时明显增加。更难受的是,一旦事务参与者执行超时或网络闪断,排查问题需要同时翻几个服务的日志,还得理解全局事务ID的传递过程。

有一次大促压测就暴露了问题:订单服务的数据库连接池直接被占满,原因就是Seata全局锁的等待机制。压测到第八个小时,数据库连接数飙到了上限,所有请求都卡在等待全局锁释放上。后来我们不得不对部分业务做了妥协——把积分加送改成了最终一致性的异步消息方案,才勉强把高峰扛过去。

但要真严格说,如果当初这些服务都还在一个进程里、一个数据库里,本地事务一条SQL就解决了,哪需要这么多弯弯绕绕。

3.3 微服务面试题里的理想世界 vs 现实世界的差异

现在微服务面试题基本上是Java后端岗位的标配:服务拆分的粒度怎么定、CAP理论怎么理解、分布式事务有哪些解决方案、服务熔断降级怎么实现、如何保证消息不丢失。这些内容我当时都能熟练回答,团队成员也都能够流畅背诵,但实际落地完全不是一回事。

面试题里会告诉你注册中心挂了怎么办,但不会告诉你每天各种中间件莫名其妙地重启和告警有多消耗精力;面试题里会告诉你怎么配置Sentinel限流,但不会告诉你规则配置上线后因为参数不精确导致误杀了一堆正常用户;面试题里会强调服务独立部署、独立扩展的好处,但不会告诉你当服务数量达到八个以上,每次发版要同时盯着多个构建管道时的精神压力。

如果面试官问你为什么选择微服务,标准答案是提升研发效率、独立部署、弹性伸缩。但现实是,对于中小团队、中等业务复杂度的系统,这三个好处都发挥不出来。技术选型只有一个标准,就是和团队规模、业务状态匹配,而不是和流行趋势匹配。

4. 中间件组合是放大器,而不是解药

4.1 Nacos与Sentinel在轻量场景下的真实表现

热词里频繁出现的Nacos和Sentinel,确实是微服务治理中很核心的两个组件。但在轻量级业务场景下,它们的优势完全发挥不出,反而成了运维负担。

Nacos作为注册中心和配置中心,成熟度没有问题,问题是它本身就要求至少三节点集群才能达到高可用。而在一个只有十几台服务器的团队里,这三台机器的成本不是小数。更麻烦的是,配置中心一旦接入,所有的配置管理都集中到一个组件上,这个组件本身就成了新的单点,一旦Nacos出问题,所有服务的配置刷新和注册发现都会受影响。

Sentinel的接入成本同样不低。每个接入Sentinel的应用,需要引入依赖、配置数据源、定义限流规则。规则本身用硬编码或Nacos动态推送,但规则是否合理、是否误伤,需要大量压测数据支撑。中小企业团队不具备这样的数据能力,结果就是规则要么设置得太宽松形同虚设,要么设置得太严格时不时误伤。

这套组件组合适合的是流量规模较大、团队具备专门的中间件运维能力的场景。对于几百QPS的小体量业务,它实际是给系统加了一个放大器:流量没有大到需要精细治理,但运维复杂性却被放大到了新的量级。

4.2 从微服务退回单体不等于放弃模块化

这里必须澄清一个误区:回到单体架构不是回到一坨代码堆在一起的大泥球。我们做的是模块化单体,在代码层面保留清晰的业务边界,只是取消了物理层面的进程拆分。

订单、商品、库存、会员仍然是独立的模块,模块之间通过Java接口调用,而不是通过远程HTTP调用。数据库不做物理拆分,但表结构按业务域划分清楚,订单表和订单明细表在一起,商品和库存在一起。模块间的数据交互直接走本地事务,模块内的逻辑依然保持高内聚低耦合。

这样一来,开发人员还是可以按模块认领代码,只是不需要启动多个服务。我们在Monorepo里维护代码,每个模块有独立的Package目录,通过代码评审强制模块边界。后续如果业务真的增长到必须拆分,依然可以按模块边界拆分成微服务,模块化单体是天然的过渡形态。

4.3 退回后的实际效果:成本、性能与维护体验

退回单体后,最直观的变化就是部署变得极其简单。以前发一次版本需要依次构建八个服务,推送到服务器,等待各自启动完成,人工检查注册状态。现在就是一个应用构建、一个Docker镜像、一条命令启动,从打包到上线完成差不多五分钟。

性能方面反而提升明显。原来的服务间调用改成本地方法调用,省去了网络IO、序列化和反序列化开销。接口响应时间平均下降了42%,听起来不可思议,但想一想一个订单创建请求,原来要在网关、用户服务、库存服务、订单服务之间至少走四次网络调用,现在一次本地调用就完成了,这个降幅完全合理。

团队的幸福感提升是另一个隐性收益。以前排查一个分布式链路问题要在几个服务的日志文件之间来回切换,现在一个应用日志按traceId搜到底。开发同学不用再关心Nacos的配置分组是什么、Feign超时时间设多少、Sentinel的熔断阈值怎么调,这些时间全部还给了业务开发。所有人都说,这半年写的业务代码比过去两年加起来都多。

5. 具体怎么退回:合并方案、数据迁移与流量切换

5.1 迁移前评估:哪些该合,哪些该留

做合并决策时,我们定了几个原则。第一,所有必须保持强一致性的核心业务全部合并回单体。订单、库存、会员积分这些一次操作需要多个数据表强一致变更的,必须回到本地事务。第二,异步能力保留在消息队列层面,不需要进程级拆分就能实现削峰和解耦。第三,有独立伸缩诉求或独立部署诉求的功能,暂时保留独立服务,比如定时任务服务和短信通知服务。

最终迁移方案是:八个服务合并为三个可执行文件——核心应用(包含商品、订单、库存、会员、营销)、消息消费应用(配合RocketMQ做异步处理)、网关应用(保留网关层,为后续可能的扩展留出空间)。数据库从六个物理库合并为一个主库加两个从库,主要业务表都在主库,日志类数据单独保留分析库。

5.2 代码层面的平滑合并策略:包迁移与表合并

代码合并这块没有捷径,但有一套相对稳妥的流程。第一步,把所有微服务代码拉进同一个仓库,建立统一的分支管理规范,这个阶段不做任何结构修改,保证每个人还能在自己熟悉的地方开发。第二步,统一依赖版本,把每个服务里各自引用的不同版本的Spring Boot、MyBatis Plus、工具包全部对齐到一个基线版本。第三步,按模块把包名从com.company.orderservice改成com.company.core.order这样统一的前缀,这个阶段纯粹机械操作,不会引入逻辑变更。

数据库合并是整个迁移中最需要谨慎的一步。原本订单服务、商品服务、库存服务各自有自己的库,合并之后所有表需要统一到一个库里。需要先解决表名冲突、主键冲突、外键关系重塑三件事。我们没有直接改表名,而是采用了双写方案:迁移期新表和老表同时写入,校验数据一致性后再切换读取。这个过程持续了两周,每天自动跑对账脚本,确保新老数据完全一致,直到连续三天数据对账无误,才真正完成切换。

5.3 分布式事务替换步骤:从Seata到本地事务

Seata是在合并过程中最后移除的组件。我的建议是先改代码,再撤组件。具体操作是把原来加了@GlobalTransactional注解的方法,拆解成普通的@Transactional方法,因为相关操作已经从跨进程变成同库操作,本地事务已经能够覆盖。

实际踩了一个比较深的坑:你们很可能也想不到,Seata的AT模式在运行期间,会在参与服务的数据库里建立undo_log表并记录数据快照。迁移后如果直接停掉Seata,会有一批未完成的事务记录残留。我们的做法是提前在凌晨低峰期,通过人工检查相关服务的Seata事务状态,将所有活动事务清零,然后逐步灰度关闭Seata的客户端初始化,最后再清理undo_log表。

5.4 三个检查清单:静态检查、回归测试、发布节奏

阶段核心动作通过标准
静态检查全局搜索Feign Client调用,逐一改为本地接口调用;搜索@RefreshScope等微服务特有注解全部移除代码中无任何Feign/Sentinel/Nacos依赖
回归测试梳理核心业务链路,覆盖下单、取消、退款、支付回调四大流程,测试全在测试环境跑两轮完整回归核心链路用例通过率100%,性能测试峰值QPS不低于原微服务
发布节奏预留一周观察期,前三天灰度切换10%、30%、50%流量,后四天全量运行线上无交易类错误报警,核心接口耗时波动小于20%

另外强调一个容易被忽略的点:域名和内网地址变更。以前服务间通过http://order-service这样的服务名调用,合并后服务名消失了,调用方如果还保留着FeginClient和对应的URL,启动时就可能报错。我们在切换的第一步就统一把所有服务间调用改为本地调用,并且把所有对外SDK的地址配置从注册中心改为直连配置。

6. 如果只有两句话:什么情况下劝你别拆,什么情况下劝你退

6.1 什么时候微服务是合理的甚至必要的

我不是微服务黑粉。系统如果满足下面几个条件,微服务确实是合理的、甚至必要的选择。

第一,团队规模比较大,至少有五个以上研发小组能够独立负责各自的业务域,每个小组都有具备独立部署、独立运维能力的技术负责人。第二,业务模块之间有清晰的物理边界,比如订单和供应链确实属于两个领域,且未来可能分别部署在不同地区或由不同团队维护。第三,确实存在独立的水平扩展需求,比如某一类流量在特定时间段内暴增,需要单独扩容对应模块,而不是整个应用一起扩容。第四,公司愿意支付基础设施和中间件运维成本,有专门的平台团队或运维团队来支撑Nacos、Sentinel、SkyWalking这些组件的日常管理和维护。

方圆科技那类流量规模很大的平台型互联网公司,微服务是必需品。业务量到了那个级别,单体的容量瓶颈是真实存在的,而且组织架构本身已经按业务域划分成多个团队,微服务架构反而是匹配组织沟通结构的自然选择。

6.2 什么情况下你应该考虑退回单体

如果你的情况符合以下几条中的大部分,我建议你认真考虑退回单体。

团队规模小于三十个人,整个后端团队就十来个开发者;业务并发量没有高到单应用完全扛不动;业务模块之间存在大量的强一致事务需求;没有专职的运维或平台工程师,中间件出问题只能全团队一起排查;系统上线后最耗时间的事情不是写代码而是处理服务间调用异常、消息积压、注册中心告警这些基础设施问题。

这不是什么丢人的决定。先进的技术栈如果不适配团队的实际状况,就是负资产。现在很多公司面试要求每个人都得会微服务,但这种人才要求更多是基于招聘市场溢价的考虑,真实业务场景反而需要的是能把系统复杂度压下来的人。

6.3 从微服务退回单体的独特优势:是一种降维打击

最后说一个我们实践下来觉得很明显的收益:性能和稳定性根本不是同一级别。同样是下单接口,原来端到端平均耗时大概480毫秒,合并为单体后直接降到180毫秒以内。原因很简单,四次网络调用省掉了三次,中间件的每次序列化和网络开销全部归零。

稳定性上,以前可能因为Gateway的一个路由配置写错、Nacos一次全量推送、RocketMQ的一个Topic消费阻塞,导致某个链路挂掉。现在这些组件的负担全部卸掉,核心应用本身的稳定性反而恢复到了单体时代那种朴素而可靠的状态。过去三个月,我们的核心交易服务没有发生过一次P0级别的事故,而上一年度,平均每个季度至少两次。

我记得把最后一台Nacos节点从生产环境摘除的那天,负责运维的同事专门发了条朋友圈,说终于不用半夜爬起来看告警群了。说实话,那一刻我觉得这个决定不仅技术上正确,对团队里的人同样重要。搞技术的踏实感,有时候恰恰来自系统不折腾,而不是折腾得很高级。如果你们团队也在微服务的泥潭里挣扎,我的建议是别硬扛,退一步不丢人,能跑得稳比看起来酷重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询