软件架构模式:把“我的页面“拆成 5 个微服务后,一次渲染要并发等 5 次 RPC
title: "软件架构模式:把'我的页面'拆成 5 个微服务后,一次渲染要并发等 5 次 RPC"
tags: [软件架构, 微服务, serverless, 单体架构, 服务拆分]
categories: [后端, 架构]
我们曾经是"微服务原教旨主义者":用户中心按功能切成 profile、auth、preference、address、points 五个服务,理由是"每个服务独立部署、独立扩容"。直到"我的"页面上线,前端一次渲染并发调这 5 个服务,任何一个慢 200ms,整页就慢 200ms;大促时 points 服务抖动,连带把个人主页一起拖垮。那次我们才明白,架构模式没有"先进落后",只有"贴合不贴合你的团队规模和调用关系"。
这篇把单体、SOA、微服务、Serverless 的真实代价摆出来,重点讲那个"拆过头"的坑,以及 Serverless 冷启动在什么场景下会反咬你一口。
事故现场:一次渲染打 5 个服务
"我的"页面要展示昵称、登录状态、偏好设置、收货地址、积分。我们按"服务拆分"把它们分到了 5 个微服务,前端(BFF 层)这么聚合:
// 错误示范:强耦合的"我的页面"被拆成 5 个服务并发调用,脆弱且慢
public MyPageVO buildMyPage(long userId) {
CompletableFuture<Profile> p1 = svc.profileAsync(userId); // 昵称
CompletableFuture<Auth> p2 = svc.authAsync(userId); // 登录态
CompletableFuture<Pref> p3 = svc.prefAsync(userId); // 偏好
CompletableFuture<Address> p4 = svc.addressAsync(userId); // 地址
CompletableFuture<Points> p5 = svc.pointsAsync(userId); // 积分
CompletableFuture.allOf(p1, p2, p3, p4, p5).join(); // 全部等到最慢的那个
return new MyPageVO(p1.join(), p2.join(), p3.join(), p4.join(), p5.join());
}
逐行看:第 3-7 行发起 5 个异步调用;第 8 行 allOf(...).join() 的意思是"等最慢的那个完成"。这 5 个服务里,profile/auth/pref/address 的数据其实来自同一个用户库、同一个用户生命周期,彼此强耦合——用户改了昵称,profile 和 pref 大概率一起变。把它们拆开只换来"部署独立",代价是每次渲染多 5 次网络往返、多 5 个故障点。points 服务一旦抖动,整页 P99 被它拽高。
正确的切分:按"演进速度"而非"功能名词"
我们后来重切了一刀:profile、auth、pref、address 这 4 个数据同源、变更同步、调用同频的,合并回一个"用户服务";只有 points(积分,有独立的活动运营节奏、独立的数据量、独立扩容诉求)单独留成服务。聚合层从 5 调变 2 调:
// 正确示范:强耦合的合并成一个服务,只把"演进速度不同"的留作独立服务
public MyPageVO buildMyPage(long userId) {
// 一次本地/同机房调用拿齐昵称、登录态、偏好、地址
UserAggregate u = userService.loadAggregate(userId);
// 只有积分是真正独立演进的服务,单独调
Points pts = pointsClient.get(userId);
return new MyPageVO(u, pts); // 2 次调用,故障点减半
}
逐行看:第 4 行 userService.loadAggregate 在合并后的用户服务里一次查出 4 块数据(甚至可以是一次 SQL 多表 JOIN 或同库多次查,都在一个进程内,无网络往返);第 6 行只单独调积分服务。网络往返从 5 次降到 2 次,最慢依赖从"5 个里最慢"变成"积分服务一个",整页 P99 明显下降。"我的页面"这种读多写少、数据强耦合的场景,单体/聚合服务明显比拆碎了更划算。
Serverless 的冷启动坑:同步调用被拖垮
另一个被"模式光环"坑到的地方是 Serverless。我们把图片缩略图处理写成函数(以 AWS Lambda Java 运行时为例),期望"按需执行、不用养机器"。但图片上传是同步链路的一环——用户传完头像要立刻看到缩略图:
// Serverless 函数(Java 运行时)做缩略图,冷启动代价高
public class ThumbnailHandler implements RequestHandler<S3Event, String> {
// 静态初始化里加载了 ImageIO + 缩略图库,冷启动时一次性加载 ~1.2s
private static final Thumbnailer THUMB = new Thumbnailer();
@Override
public String handleRequest(S3Event event, Context ctx) {
String key = event.getRecords().get(0).getS3().getObject().getKey();
return THUMB.resize(key, 200, 200); // 业务逻辑本身只要 80ms
}
}
逐行看:第 4 行静态初始化 Thumbnailer 在冷启动时加载,JVM + 图片库初始化约 1.2 秒;第 8 行真正的缩略图逻辑只要 80ms。问题在于这个函数是"同步被上传链路调用"的——用户传完图,上传服务同步 invoke 这个函数等结果。函数长时间没流量被回收,下次调用先经历 1.2 秒冷启动,上传接口的 P99 直接从 200ms 飙到 1.5 秒。Serverless 适合异步、可延迟的任务(比如把缩略图改成"上传完成后发消息,函数异步消费"),不适合卡在同步主链路里。
SOA 的 ESB 陷阱:一个总线成了全公司的单点
聊架构模式绕不开 SOA,因为很多人是从"单体直接跳微服务"踩坑后,才发现中间还有 SOA 这层。我们早年在 SOA 阶段用企业服务总线(ESB)把所有系统打通:订单、库存、用户、支付都通过 ESB 转发报文。听起来解耦了,实际上 ESB 成了全公司的同步单点。
一次大促,ESB 上某个 XSLT 报文转换规则写得重(一个订单报文要过 6 次 XPath 抽取再拼装),单条转换 30ms,峰值 8000 TPS 时 ESB 节点的 8 核 CPU 打满,所有走总线的交易一起变慢。更糟的是 ESB 是同步转发,前面订单系统超时了,请求还在 ESB 队列里排队,雪崩顺着总线传到库存、支付。那次之后我们把"重转换"挪到各业务系统内部做,ESB 只做轻量路由,并给总线加了异步队列隔离。
SOA 的教训和微服务的教训是同一句话的反面:总线式集中治理在流量小的时候很省心,流量一大就变成瓶颈;而微服务式去中心化把治理成本摊到每个服务,灵活但运维重。选 SOA 还是微服务,本质是在"集中治理的瓶颈风险"和"去中心化的运维成本"之间 trade-off,不是谁淘汰谁。
四种模式到底怎么选
| 模式 | 适用团队/规模 | 拆得越细越好? | 典型代价 |
|---|---|---|---|
| 单体 | 小团队(<10 人)、单一业务 | 适合早期,别急着拆 | 代码膨胀、部署耦合 |
| SOA | 多系统集成的企业 | 按企业总线解耦 | ESB 易成瓶颈、治理重 |
| 微服务 | 中大型、团队按域划分 | 否,按"演进速度"拆 | 网络延迟、分布式事务、运维复杂 |
| Serverless | 事件驱动、流量波动大、异步为主 | 函数粒度看场景 | 冷启动、调试难、 vendor 绑定 |
我们现在的判断是:微服务不是"把服务拆小",是"按业务域和演进速度划边界"。一个服务该不该独立,看三件事——①数据是否同源同生命周期;②发布节奏是否不同;③是否需要独立扩缩容。三条里只满足一条,就先别拆。
复盘真实数字
- "我的页面"拆 5 服务时,渲染 P99 为 420ms,其中网络往返占 约 210ms;大促时 points 服务抖动,整页错误率一度 4.7%。
- 合并回 2 调用后,渲染 P99 降到 190ms,网络往返占比降到 约 60ms,整页错误率和 points 服务解耦,points 抖动不再影响主页。
- 缩略图函数冷启动 1.2s,导致上传接口 P99 从 200ms 飙到 1.5s;改成异步消费后,上传接口 P99 回到 210ms,缩略图在 1 秒内最终一致生成,用户无感。
- 我们审计了一遍服务边界,把 23 个微服务合并成 14 个,运维工单(跨服务链路排查)同比下降 约 40%。
- SOA 阶段那次 ESB 瓶颈,单条报文转换 30ms,峰值 8000 TPS 把 8 核 ESB 节点 CPU 打满,连带拖垮订单/库存/支付三条链路;把重转换下沉到业务系统、ESB 改纯路由 + 异步队列后,总线 CPU 峰值从 100% 降到 35%,交易超时率归零。
- 这轮拆分合并下来,单机到微服务的演进我们前后推翻了两次边界,团队从"盲目拆"变成"按数据生命周期和发布节奏划界",新需求平均交付周期反而缩短了一天半。
我的取舍判断
第一,别用"功能名词"拆服务,用"演进速度"拆。 profile/auth/pref/address 拆成 5 个,是教科书式的过度拆分;它们同一份数据、同一次变更、同一节奏,拆开只增加网络跳数和故障点。真正值得独立的是 points 这种"有自己运营活动、自己数据量曲线"的。
第二,Serverless 是给异步任务用的,别塞进同步主链路。 它的冷启动在 Java 运行时尤其明显(JVM 启动 + 依赖加载),放进"用户传完图立刻看结果"这种同步路径就是给 P99 埋雷。改成"事件触发、异步消费"才发挥它的弹性长处。
第三,架构模式要回头看,别一次定终身。 我们从单体到微服务拆过头,再合并,不是走回头路,是让边界贴合真实调用关系。合并不等于"架构失败",拆也不等于"先进"。我更倾向于每季度审计一次服务边界,数据同源的该合就合。
第四,单体不是"临时方案",是多数项目的最优解。 我们见过太多团队一上来就微服务,结果 3 个人维护 8 个服务,一半精力花在搭链路追踪和网关上。我的经验线:团队不到 10 人、业务域没清晰分裂前,单体(顶多加模块化分包)的迭代速度远超微服务。等真出现"某模块要独立扩容"或"发布节奏明显冲突"的信号,再拆不迟。架构跟着组织和业务走,别反过来。
思考题
你现在负责的服务里,有没有"数据同源、却拆成两个服务、每次调用都要跨一次 RPC"的情况?如果把它合并,你预计会省掉哪些代价、又会失去哪些好处?欢迎评论区说说你的取舍。
架构没有银弹,只有取舍。这一轮 Batch 7 的 5 篇(配置中心 / Session / 存储 / 混沌 / 架构)就到这里,下一批我们收尾 Round 3 的 Batch 8。
更多推荐

所有评论(0)