微服务拆错了怎么回退?一个 12 人团队从 7 个服务合并到 3 个的架构复盘

🔑 关键词:微服务回退, 模块化单体, 软件架构选型, 分布式事务, 架构复盘

📖 摘要:从一次跨境 ERP 救火讲起:7 个微服务压垮 12 人团队,Kafka 积压 270 万条、Postgres 连接打满,最后用 Outbox+幂等+灰度合并回 3 个服务,并给出拆分阈值和回退步骤。

先说现场:7 个服务,12 个人,大促当天差点崩

图片

我不是架构师,起码名片上不是。2023 年 11 月,我去帮一个做跨境 ERP 的团队救火,12 个人,后端 5 个,前端 3 个,测试 2 个,运维 1 个,还有个产品兼老板。 他们把订单、库存、支付、履约、通知、报表、权限拆成 7 个 Spring Boot 服务,跑在 K8s 1.29 上,每个服务一个 Postgres 库,2024 年初升到 Postgres 16,中间用 Kafka 3.6 传事件,Redis 7.2 做缓存。 听起来挺整齐,问题是每次改一个“下单送积分”需求,要动 4 个服务、2 个库、3 个 Kafka topic,发布窗口从周三晚上 10 点拖到凌晨 2 点,失败一次回滚 40 分钟。 最惨的是大促当天,订单服务 p99 从 420ms 涨到 1.8s,Kafka 积压 270 万条,Postgres 的 max_connections=100 被打满,HikariCP 每个服务 maximumPoolSize=20,7 个服务加起来理论 140,实际也差不多,pgbouncer 1.21 都没救回来。 我当时在会议室吃冷掉的包子,看监控大屏,心里想:这不是架构先进,这是把单体的一张大表拆成了七张要互相打电话的小表。

图片

诊断:不是微服务错,是边界切错了

图片

我们先没急着合并,先拉 OpenTelemetry 1.22 的 trace。一个下单请求平均跨 14 跳,最长的链路是 API 网关 -> 订单 -> 库存 -> 支付 -> 履约 -> 通知 -> 报表,其中 5 个数据库事务,3 个 Kafka 生产,2 个 Redis 锁。 Saga 补偿成功率 92.7%,剩下 7.3% 靠人工对账,财务每周花 6 小时。 更搞笑的是,订单和库存的代码变更经常在同一个 PR 里,因为它们本来就是一个业务动作,但被两个团队 owner 卡住,review 要等 1.5 天。 我们统计了 8 周:发布冲突 6 次/周,回滚 2 次/周,Kafka consumer lag 报警 19 次,但没人处理,因为钉钉群每天 200 多条消息,报警被淹了。 这里不是微服务错,是拆分边界按“业务名词”切,而不是按“变更频率”和“事务边界”切。订单、库存、支付这三个词放在 PPT 上很清晰,放到代码里,改价格策略要同时改订单和支付,改库存锁定要同时改订单和库存,拆开就是分布式事务,合起来就是一个本地事务。 我们后来定了个土办法:一个模块如果每周变更超过 5 次,且和其他模块发布冲突每周超过 2 次,或者 CPU 峰值差异超过 4 倍,才考虑拆进程;否则先留在同一个进程里,但代码分层、数据库 schema 分开、API 走模块接口。这个阈值不神圣,但比拍脑袋强。

回退:7 步,6 周,从 7 个服务合到 3 个

图片

回退微服务不是把代码复制回一个 repo 就完事。我们按 7 步走,花了 6 周。 第 1 步,冻结新服务拆分,先别再“抽离”了。第 2 步,画数据写入矩阵:列出 7 个服务里谁写哪张表,发现订单库和库存库有 3 张表互相引用,支付库有 1 张冗余订单表。 第 3 步,选主库:把订单库作为写主,库存和支付通过 outbox 表 + Kafka 3.6 异步同步,outbox 表用 Postgres 16 的逻辑复制槽,消费端幂等键用 order_id + event_type + version。 第 4 步,合并 API:把 7 个服务的 HTTP 接口先收敛到 3 个 BFF,内部改成模块调用,减少网络跳数。第 5 步,双写窗口 7 天:旧服务继续写,新模块也写,跑对账脚本,差异超过 0.01% 就停。 第 6 步,灰度切流:按租户切,先切 5 个中小客户,再切 20%,最后全量,Kafka topic 保留 168 小时,消费者 offset 可以回滚。第 7 步,删旧服务,但保留 30 天镜像和数据库快照。 最后合并成 3 个服务:交易服务(订单+库存+支付)、履约服务(履约+通知)、报表 worker。p99 从 1.8s 降到 260ms,Kafka 积压从 270 万条到 0,发布冲突从 6 次/周降到 1 次/周,部署时间从 22 分钟降到 7 分钟。 代价也有:交易服务代码量从 1.8 万行涨到 4.6 万行,单测从 1200 个涨到 3100 个,构建时间从 3 分钟到 8 分钟。但我觉得值,因为故障半径小了,值班的人能睡整觉。

图片

对比和我的独立观点:架构不是分层,是分账

图片

现在很多人问“单体还是微服务”,我觉得这个问题问错了。应该问:你的团队有多少人?发布频率多高?事务边界清不清?故障能不能隔离? 我做了个对比,不一定对,但都是踩出来的。 | 维度 | 模块化单体 | 微服务 | 事件驱动 | |---|---|---|---| | 团队规模 | 8-15 人 | 30 人以上 | 不限,但要有人值班 | | 事务 | 本地事务 | 分布式事务/Saga | 最终一致 | | 部署 | 1 个单元 | N 个单元 | N 个消费者 | | 适合场景 | 订单+库存+支付 | 支付通道/风控/报表 | 通知/积分/审计 | | 主要成本 | 构建变慢 | 网络+一致性+值班 | 重复消费+顺序问题 | 模块化单体:12 人以下、2 周一个迭代、本地事务、部署 1 个单元,运维成本低,适合订单+库存+支付这种强一致场景。微服务:30 人以上、多团队独立发布、按合规或扩缩容隔离,适合支付通道、风控、报表这种独立能力。 事件驱动:适合通知、积分、审计、数据同步,但别拿它当分布式事务的万能药。服务网格:K8s 1.29 + Istio 1.20 能解决可观测和流量治理,但 12 人团队维护成本太高,我们试了 2 周,sidecar 内存多吃了 1.2GB,最后关了。 我的独立观点是:架构不是“分层”,是“分账”。每拆一个服务,就多一张网络账单、数据一致性账单、值班账单。团队规模不够,账单利息会吃光微服务的收益。 先做“可拆分的单体”,把模块边界、数据库 schema、事件契约画清楚,等变更频率和团队规模真的到了,再拆进程。别为了简历上的“高并发微服务”把公司拖进分布式事务的泥潭。 我们那个团队现在还在跑 3 个服务,老板问要不要再拆,我说先等交易服务单周变更超过 8 次、或者后端超过 15 人再说。这不是保守,是算账。

🏷️ 标签: