把 12 个微服务合并回 4 个之后,P99 从 680ms 降到 470ms:过度拆分的 5 笔账
title: 把 12 个微服务合并回 4 个之后P99 从 680ms 降到 470ms过度拆分的 5 笔账tags: [微服务, 单体架构, SOA, Serverless, 架构演进]category: 后端新来的同事第一周问了个问题查一个订单详情要调几个服务我打开链路追踪给他看那张图网关 → 订单服务 → 用户服务 → 会员等级服务 → 权益服务 → 商品服务 → 库存服务 → 价格服务 → 优惠券服务 → 物流服务 → 售后服务 → 评价服务。12 跳其中 5 跳是串行的7 跳能并行。P99 是 680ms其中真正执行业务逻辑的时间加起来不到 90ms剩下的全在网络往返、序列化、线程切换和等待上。他说了句这不就是把方法调用改成了 HTTP 调用吗。这句话不客气但准确。那之后我们花了七个月把这套东西合并回 4 个服务P99 降到 470ms故障率降了一半团队的迭代速度反而快了。这篇把这个过程里算清的五笔账写下来。先承认当初拆成 12 个理由在当时是成立的2022 年那次拆分不是拍脑袋。当时的单体应用有 47 万行代码一次全量构建 18 分钟部署要停机 6 分钟20 多个人共用一个代码仓库每次发布前的合并冲突能开半天会。任何一个模块的内存泄漏都能把整个应用拖垮——我们真的遇到过报表导出功能的一个 List 没释放导致下单接口全线 OOM。拆分之后这些问题确实解决了单服务构建 2 分钟独立部署故障隔离。问题在于拆分的粒度。我们当时按领域名词拆看到会员等级这个概念就拆一个服务看到权益又拆一个。结果是会员等级服务只有 3 张表、8 个接口代码量 4000 行日常唯一的调用方就是权益服务而权益服务的唯一调用方是订单服务。三个服务串成一条线中间隔着两次 HTTP却从来没有第二个调用方。这就是典型的分布式单体物理上分开了逻辑上还是一坨改一个需求要同时发三个服务还得排好顺序。第一笔账跨服务调用的固定成本一次进程内方法调用大约 1-10 纳秒。一次同机房的 HTTP 调用我们实测的分解是这样的环节耗时P50说明服务发现 负载均衡0.02ms本地缓存命中时请求序列化Jackson0.3ms平均 2KB 报文网络往返同可用区0.4ms不含服务端处理服务端反序列化0.3ms服务端业务逻辑3-8ms真正干活的部分响应序列化 反序列化0.5ms客户端线程切换 / 回调0.1ms异步框架下固定开销小计约 1.6ms不含业务逻辑1.6ms 看起来不多但要乘以调用次数而且这是 P50。P99 的分布完全不一样——任何一跳发生 GC、连接池等待、DNS 重解析都能贡献几十毫秒。12 跳串下来只要每跳有 1% 的概率慢 50ms整条链路慢的概率就是 1-0.99^12 11.4%。这就是为什么微服务的 P99 天然比单体差。我们那条链路上5 跳串行的固定开销是 8ms但 P99 贡献了 210ms。第二笔账数据一致性的复杂度单体里下单扣库存是一个本地事务Transactional(rollbackFor Exception.class) public Order createOrder(OrderRequest req) { // 三张表在同一个数据库同一个事务要么全成要么全滚 inventoryMapper.deduct(req.getSkuId(), req.getQty()); Order order orderMapper.insert(buildOrder(req)); couponMapper.markUsed(req.getCouponId(), order.getId()); return order; }拆成三个服务之后这段代码变成了public Order createOrder(OrderRequest req) { String txId IdGen.next(); // 第一步预扣库存TCC 的 Try 阶段冻结但不真扣 InventoryFreezeResult freeze inventoryClient.freeze( new FreezeRequest(txId, req.getSkuId(), req.getQty())); if (!freeze.isSuccess()) { throw new BizException(库存不足); } try { // 第二步锁定优惠券同样是可撤销的中间态 couponClient.lock(new LockRequest(txId, req.getCouponId())); } catch (Exception e) { // 补偿撤销库存冻结。注意这里的 catch 必须捕获所有异常 // 包括超时——超时意味着不知道成没成功也必须补偿 safeCancel(() - inventoryClient.unfreeze(txId)); throw e; } Order order; try { order orderMapper.insert(buildOrder(req, txId)); } catch (Exception e) { // 补偿两个顺序无所谓但每个都要保证幂等 safeCancel(() - couponClient.unlock(txId)); safeCancel(() - inventoryClient.unfreeze(txId)); throw e; } // 第三步确认Confirm 阶段。这一步的失败最难处理—— // 订单已经落库了但库存还是冻结态。只能靠异步补偿任务重试 // 而重试期间用户看到的订单状态是处理中 asyncConfirm(txId, order.getId()); return order; } private void safeCancel(Runnable action) { try { action.run(); } catch (Exception e) { // 补偿失败只能记账交给对账任务兜底。 // 这张 compensation_failure 表我们线上一天能积几十条 compensationLogMapper.insert(CompensationLog.of(action, e)); log.error(compensation failed, recorded for retry, e); } }从 5 行变成 40 行还要额外维护冻结记录表、补偿失败表、对账任务、超时清理任务、幂等去重表。我粗略统计过这套分布式事务的配套代码有 2300 行是原来那 5 行业务逻辑的 460 倍。更要命的是它引入的新故障模式。我们上线后半年内跟分布式事务相关的线上问题有 14 起悬挂事务Cancel 先于 Try 到达3 起、空回滚 2 起、幂等失效导致重复扣减 4 起、补偿任务把已完成的订单又回滚了 1 起、对账脚本自身的 bug 4 起。第三笔账本地缓存失效单体里我们大量使用 Caffeine 做本地缓存商品基础信息的命中率能到 96%。拆分之后商品服务变成独立进程订单服务想缓存商品信息就得自己维护一份而失效通知要通过 MQ 广播。Component public class ProductLocalCache { private final CacheLong, ProductVO cache Caffeine.newBuilder() .maximumSize(50_000) // TTL 不能设太长本地缓存 MQ 失效的组合里 // MQ 消息丢失是必然会发生的TTL 是最后的兜底 .expireAfterWrite(Duration.ofMinutes(5)) // refreshAfterWrite 让过期时只有一个线程去回源 // 其他线程先拿旧值避免缓存击穿 .refreshAfterWrite(Duration.ofMinutes(2)) .recordStats() .build(this::loadFromRemote); private ProductVO loadFromRemote(Long productId) { // 这里必须做超时和降级商品服务挂了不能让订单服务跟着挂 try { return productClient.getById(productId); } catch (Exception e) { // 返回 null 会让 Caffeine 不缓存下次继续打远程 // 高峰期这会变成雪崩。返回一个降级对象更安全 log.warn(load product {} failed, use degraded, productId, e); return ProductVO.degraded(productId); } } /** * 商品服务发出变更消息后各个消费方清本地缓存。 * 广播模式消费每个实例都要收到——这一点在 RocketMQ 里 * 要显式设置 MessageModel.BROADCASTING默认是集群模式只有一个实例收到 * 我们上线第一周就栽在这个默认值上导致 7 个实例里只有 1 个清了缓存 */ RocketMQMessageListener(topic product-change, consumerGroup order-service-product-cache, messageModel MessageModel.BROADCASTING) public void onProductChange(ProductChangeEvent event) { cache.invalidate(event.getProductId()); } }这段代码本身不复杂问题是它要在每个需要商品信息的服务里复制一份。我们有 6 个服务需要商品信息就有 6 份几乎一样的缓存代码6 个消费组6 套监控指标。任何一处的失效逻辑写错就是一次数据不一致事故。而在单体里这就是一个 Bean。第四笔账排查成本单体时代排查一个订单金额算错了的问题流程是看日志找到那次请求从入口跟到出口全在一个日志文件里20 分钟能定位。12 个服务之后同样的问题要拿 traceId 去链路追踪系统查调用链找到可疑的那一跳去那个服务的日志系统按 traceId 过滤前提是它正确透传了 traceId发现是它的下游返回了错误数据再往下一跳……我们统计过2023 年跨服务问题的平均定位时间是 2 小时 40 分钟比单体时代长了 7 倍。链路追踪能缓解但解决不了因为追踪只告诉你哪一跳慢/错了不告诉你为什么。而为什么往往需要看那个服务的内部状态那就得找到对应的负责人——如果那人在休假等着吧。第五笔账组织成本这一笔最容易被忽略也最贵。12 个服务每个都需要一套 CI/CD 流水线、一套监控告警、一份容量规划、一个值班责任人、一份接口文档、一套压测脚本。这些东西的边际成本不是零加起来是实实在在的人力。我们算过一笔账维护一个微服务的固定运维成本大约是每月 0.3 人日不含业务开发12 个服务就是 3.6 人日/月一年 43 人日。而当时团队只有 9 个人。更隐蔽的是决策成本。一个需求要改 3 个服务就要 3 个人对齐排 3 次发布做 3 次回归。康威定律说组织结构决定架构反过来也成立过细的架构会强行制造出不必要的协作边界。我们怎么合并的按变更频率而不是按领域名词合并的原则是看它们是不是总是一起改。我们扒了两年的 Git 提交记录统计每两个服务在同一个需求里同时被修改的频率服务对共同变更次数独立变更次数耦合度会员等级 ↔ 权益473 / 50.85订单 ↔ 售后3188 / 120.24商品 ↔ 价格526 / 40.84库存 ↔ 订单961 / 880.06优惠券 ↔ 价格3814 / 60.65耦合度 共同变更 / (共同变更 独立变更之和)。这个数字超过 0.6 的服务对基本可以判定不该分开——它们没有独立演进的能力只是被物理分开了。最终的合并方案会员等级 权益 → 会员域服务商品 价格 优惠券 → 商品域服务订单 售后 评价 → 交易域服务库存、物流、用户各自保留它们的耦合度都低于 0.2而且有多个独立调用方。12 个变成 4 个加上保留的 3 个实际是 7 个但核心链路上只剩 4 跳。合并不是简单地把代码复制到一起。我们保留了模块边界合并后的服务内部按原服务划分 Maven 模块模块之间只能通过定义好的接口调用用 ArchUnit 写规则在 CI 里强制检查。AnalyzeClasses(packages com.example.merchandise) public class ModuleBoundaryTest { /** * 价格模块不能直接访问商品模块的 Mapper 层只能走 Service 接口。 * 这条规则的意义在于将来如果需要重新拆开边界还在 * 不至于合并两年后代码烂成一团再也拆不动 */ ArchTest static final ArchRule price_module_should_not_touch_product_dao noClasses().that().resideInAPackage(..price..) .should().accessClassesThat() .resideInAPackage(..product.dao..) .because(跨模块访问必须走 Service 接口保留将来拆分的可能); /** * 各模块的领域对象不能互相引用跨模块传递只能用 DTO。 * 这条比上面那条更容易被违反——写代码时随手 import 一个实体类太顺手了 */ ArchTest static final ArchRule domain_objects_should_not_cross_module slices().matching(com.example.merchandise.(*)..) .should().notDependOnEachOther() .ignoreDependency( resideInAPackage(..price..), resideInAPackage(..common.dto..)); }四种架构模式的适用边界模式团队规模部署单元数据一致性主要成本什么时候选它单体1-15 人1 个本地事务构建慢、故障不隔离业务未定型、快速验证阶段模块化单体10-40 人1 个本地事务需要纪律维持模块边界业务清晰但团队不大我认为这是最被低估的选项SOA / 粗粒度服务30-100 人5-15 个少量分布式事务服务治理、接口版本管理有明确的独立演进诉求微服务100 人以上几十到上百大量最终一致运维、排查、组织协调团队多到必须并行发布Serverless不限函数级强依赖外部状态冷启动、可观测性弱、供应商绑定流量波峰波谷极端、事件驱动型任务我把模块化单体单独列出来是因为它在国内讨论得太少。它保留了单体的部署简单和本地事务同时用模块边界和构建期检查获得了大部分的关注点分离收益。代价是需要纪律——但需要纪律的架构总比需要 43 人日/年运维成本的架构划算。Serverless 那一行我要多说一句。我们在两个场景上用了函数计算图片处理和数据导出。这两个场景的共同点是事件触发、执行时间短、并发波动极大。图片处理的日常 QPS 是 5但运营做活动时能到 800用常驻服务就得按 800 备容量。换成函数计算之后这块成本降了 74%。但我们没有把任何核心链路放上去——冷启动 300ms 到 2 秒的不确定性在下单链路上是不可接受的。我的判断拆分的触发条件应该是组织问题不是技术问题。这个模块太复杂了不是拆分理由那是重构理由。真正的拆分理由只有一个两拨人需要独立发布且合在一起会互相阻塞。不要按名词拆按变更频率拆。领域驱动设计里的限界上下文是个好概念但实践中很多人把它简化成了按业务名词分类。真正的边界应该用数据说话——把 Git 历史扒出来算耦合度比开三天架构评审会有用。合并回去不丢人。这一点我想强调。技术圈有种氛围好像架构只能往更先进的方向走合并服务等于承认失败。但架构本来就该跟着业务和团队规模双向调整。我们那次合并最大的阻力不是技术是有人觉得这是在开倒车。先把模块边界立住再考虑要不要拆成进程。边界清晰的单体随时可以拆边界模糊的微服务永远合不回去也拆不明白。复盘几个数字合并前核心链路 12 跳5 串 7 并P99 680ms其中业务逻辑 90ms。合并后核心链路 4 跳P99 470ms降幅 31%。业务逻辑耗时没变省下的全是网络和序列化。分布式事务相关代码从 2300 行降到 610 行库存和订单之间仍需要跨服务事务但只剩一处。跨服务问题平均定位时间从 2 小时 40 分降到 55 分钟。运维固定成本从 3.6 人日/月降到 1.5 人日/月。服务器成本降了 22%合并消除了大量为每个服务至少 2 个实例保证高可用而付出的冗余。合并耗时 7 个月分 4 个阶段灰度期间发生 P2 事故 1 次会员权益合并时漏迁了一张配置表影响 40 分钟。留个问题如果你现在接手一个 30 人团队、40 个微服务、核心链路 15 跳的系统第一步会做什么我的答案是先花两周把调用拓扑和 Git 耦合度算出来不动代码。但我见过另一种做法直接冻结新服务创建所有新需求只能加到现有服务里靠不再恶化换时间。两种做法你更倾向哪一种为什么

相关新闻

我在CSDN踩过的10个技术坑:血泪经验与避坑指南

我在CSDN踩过的10个技术坑:血泪经验与避坑指南

已为您生成一篇关于“CSDN技术踩坑经验”的详细文章大纲,包含引言、10个具体坑点的详细规划以及总结。您可以根据此大纲,使用“续写”、“扩写”等指令来填充具体内容。一、 引言:为什么分享这些“坑”?在CSDN平台进行技术创作与交…

2026/8/9 2:05:40 阅读更多 →
第一次混沌演练把生产打挂 23 分钟:稳态定错、半径失控,4 个换来的规矩

第一次混沌演练把生产打挂 23 分钟:稳态定错、半径失控,4 个换来的规矩

title: 第一次混沌演练把生产打挂 23 分钟:稳态定错、半径失控,4 个换来的规矩 tags: [混沌工程, 故障注入, ChaosBlade, Chaos Mesh, 稳定性] category: 后端那天的演练计划写得很漂亮:给订单服务调用库存服务的链路注入 500ms 延迟&#xf…

2026/8/9 2:05:40 阅读更多 →
2024年杭州集团网站建设指南:从战略规划到技术落地的全解析

2024年杭州集团网站建设指南:从战略规划到技术落地的全解析

说实话,每次听到“杭州集团网站建设”这个短语,我脑海里浮现的画面总是伴随着钱塘江畔的潮湿空气和写字楼里彻夜不眠的灯光。杭州,这座被誉为“数字之都”的城市,不仅有着西湖的温婉,更有着互联网浪潮的汹涌澎湃。在这里,做网站不仅仅是放几个图片、写几段文字那么简单,…

2026/8/9 2:05:40 阅读更多 →

最新新闻

Java HashMap核心机制与性能优化全解析

Java HashMap核心机制与性能优化全解析

1. HashMap 核心机制解析HashMap 作为 Java 集合框架中最常用的数据结构之一&#xff0c;其底层实现经历了从 JDK7 的数组链表到 JDK8 的数组链表/红黑树的演进。我们先看一个典型初始化示例&#xff1a;Map<String, Integer> map new HashMap<>(16, 0.75f);1.1 哈…

2026/8/10 4:55:29 阅读更多 →
VMware Workstation Pro 虚拟机安装配置全攻略:从避坑到高效使用

VMware Workstation Pro 虚拟机安装配置全攻略:从避坑到高效使用

你肯定遇到过这样的场景&#xff1a;想学一门新技术&#xff0c;比如 Linux 命令&#xff0c;但不敢在自己的主力电脑上瞎折腾&#xff1b;或者需要测试一个软件&#xff0c;又怕它把系统搞乱。这时候&#xff0c;一个独立、干净、随时可以重置的“沙盒”环境就显得无比重要。虚…

2026/8/10 4:55:29 阅读更多 →
企业网络安全攻防演练实战指南与案例分析

企业网络安全攻防演练实战指南与案例分析

1. 攻防演练的本质与价值现代企业安全体系建设中&#xff0c;攻防演练已成为检验防御能力的黄金标准。这种红蓝对抗模式最早可追溯到军事领域的"战争游戏"概念&#xff0c;如今已演变为网络安全领域的常态化实践。我参与过数十次不同规模的企业级攻防演练&#xff0c…

2026/8/10 4:55:29 阅读更多 →
数据驱动陷阱:指标化管理如何扼杀工程师创造力与技术创新

数据驱动陷阱:指标化管理如何扼杀工程师创造力与技术创新

1. 项目概述&#xff1a;当“数据驱动”变成“指标驱动”最近在圈子里&#xff0c;Meta AI 内部关于“指标化”管理引发的一系列问题&#xff0c;成了不少技术管理者私下讨论的热点。这事儿听起来像是一个遥远大厂的管理风波&#xff0c;但仔细琢磨&#xff0c;它精准地戳中了几…

2026/8/10 4:55:29 阅读更多 →
深度解析applera1n:iOS激活锁绕过技术的终极实战指南

深度解析applera1n:iOS激活锁绕过技术的终极实战指南

深度解析applera1n&#xff1a;iOS激活锁绕过技术的终极实战指南 【免费下载链接】applera1n icloud bypass for ios 15-16 项目地址: https://gitcode.com/gh_mirrors/ap/applera1n 你是否曾经面对一台被Apple ID锁定的iPhone而束手无策&#xff1f;或者从二手市场购买…

2026/8/10 4:55:29 阅读更多 →
AI应用安全实战:从网络风险到防御框架

AI应用安全实战:从网络风险到防御框架

如果你最近关注AI新闻&#xff0c;可能会注意到一个看似矛盾的现象&#xff1a;一方面&#xff0c;OpenAI的GPT-4o、o1模型更新不断&#xff0c;API价格战打得火热&#xff1b;另一方面&#xff0c;关于其下一代旗舰模型GPT-6和备受瞩目的多模态AI助手“Astra”的消息却突然变得…

2026/8/10 4:54:29 阅读更多 →

日新闻

GraphQL-CSS API全解析:useGqlCSS、GqlCSS组件与getStyles实用指南

GraphQL-CSS API全解析:useGqlCSS、GqlCSS组件与getStyles实用指南

GraphQL-CSS API全解析&#xff1a;useGqlCSS、GqlCSS组件与getStyles实用指南 【免费下载链接】graphql-css A blazing fast CSS-in-GQL™ library. 项目地址: https://gitcode.com/gh_mirrors/gr/graphql-css GraphQL-CSS是一个基于GraphQL的CSS-in-GQL™库&#xff0…

2026/8/10 0:00:02 阅读更多 →
告别语言障碍:KISS Translator 双语翻译插件终极指南

告别语言障碍:KISS Translator 双语翻译插件终极指南

告别语言障碍&#xff1a;KISS Translator 双语翻译插件终极指南 【免费下载链接】kiss-translator A simple, open source bilingual translation extension & Greasemonkey script (一个简约、开源的 双语对照翻译扩展 & 油猴脚本) 项目地址: https://gitcode.com/…

2026/8/10 0:00:02 阅读更多 →
BepInEx配置管理器:游戏插件配置的终极可视化解决方案

BepInEx配置管理器:游戏插件配置的终极可视化解决方案

BepInEx配置管理器&#xff1a;游戏插件配置的终极可视化解决方案 【免费下载链接】BepInEx.ConfigurationManager Plugin configuration manager for BepInEx 项目地址: https://gitcode.com/gh_mirrors/be/BepInEx.ConfigurationManager 你是否曾经因为游戏插件的复杂…

2026/8/10 0:00:02 阅读更多 →

周新闻

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑&#xff1a;baidupankey如何智能破解百度网盘资源锁 【免费下载链接】baidupankey 在线查询网盘提取码&#xff08;维护中 rm repo&#xff09; 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 你是否曾经在深夜寻找一份重要资料&#x…

2026/8/10 1:05:29 阅读更多 →
如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片&#xff1a;Python开源工具完整指南 【免费下载链接】chinese_license_plate_generator 中国车牌生成器 项目地址: https://gitcode.com/gh_mirrors/ch/chinese_license_plate_generator 中国车牌生成器是一个基于Python的开源项目&#xff0c…

2026/8/10 1:05:29 阅读更多 →
收藏!小白程序员轻松入门大模型,从Harness工程开始实践

收藏!小白程序员轻松入门大模型,从Harness工程开始实践

文章强调学习大模型不应只关注模型本身&#xff0c;而应重视模型外的系统搭建&#xff0c;即Harness。提出AgentModelHarness的实用公式&#xff0c;详细介绍Harness的四个层次&#xff1a;持久化层、执行层、控制层和观察与验证层。文章还探讨了上下文工程、工具设计、AGENTS.…

2026/8/10 1:05:29 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速&#xff1a;macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/9 17:05:02 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南&#xff1a;3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗&#xff1f;ncmdump解密工具帮你轻松解决这个困…

2026/8/10 1:05:29 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片&#xff1a;为英语学习 App 打造桌面级学习助手适用平台&#xff1a;HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0&#xff08;API 26 Beta&#xff09;新增了 AgentCard 智能体卡片能力&#xff0c;这是继 HMAF&#xff08;鸿蒙智能体框架&#x…

2026/8/9 17:05:02 阅读更多 →