你可能见过这种页面:数据明明不大,接口也没报错,可首页就是慢半拍。很多人第一反应是“服务器不行了”。其实有时候服务器不是算得慢,而是请求来回跑得太多。
这篇文章只讲一个核心思路:在高 RTT、客户端请求很碎的场景里,故意让服务端多干一点活,换更少的网络往返。常见手段就是批量 RPC、请求合并、服务端聚合。你读完后,至少能回答三个问题:它到底在优化什么、三种手段怎么选、代价会不会把服务端压垮。
先讲人话:这到底是在省什么?
人话版很简单:别让客户端自己跑五趟腿,改成它只去一次柜台,柜台里的工作人员把要办的事一次处理完。
术语版再说一遍:把原本分散在客户端侧的多次远程调用,改成更少次的请求;在服务端内部做批处理、合并或聚合,减少网络往返次数,也就是减少 RTT 的消耗。
RTT 是往返时间。你发出一个请求,再把响应拿回来,这一来一回要花多久,就是 RTT。
生活类比:你去办事,路上来回 15 分钟,窗口只办 1 分钟,那你真正浪费的不是窗口时间,而是反复跑腿的时间。
小案例:地铁里打开一个商品页,客户端要分别请求商品、库存、价格、优惠、推荐。每次 RTT 180ms,就算每个服务只算 20ms,用户也会觉得“怎么一直在转圈”。
先别急着写代码,先用这张判断图
开始
↓
用户感觉慢,先量客户端到服务端的 RTT
↓
RTT 明显高吗?
↓
页面或动作会拆成很多小请求吗?
↓
这些小请求能不能一起查、一起发、一起返回?
↓
服务端能承受更多 CPU / 内存 / 编排逻辑吗?
↓
能:优先考虑批量 RPC、请求合并、服务端聚合
不能:先看缓存、索引、并行化、数据裁剪
看完这张图,你下一步不是先改代码,而是先把一次页面加载到底发了多少请求数清楚。
三种手段,名字像一锅粥,其实分工很明确
1. 批量 RPC:同一件事,别一单一单办
人话版:本来你要去同一个窗口查 10 个人的信息,不如把 10 个名字一次交过去,让窗口一次查完。
术语版:把多个同类型、同目标服务的 RPC 调用打包成一次批量调用,常见形式是把单个 id 改成 ids[]。
小案例:推荐列表里有 20 个作者卡片。原来客户端或 BFF 要查 20 次作者资料,现在改成一次 batchGetAuthors(ids[]),网络只跑一趟。
它最适合的场景是:同一个服务、同一种查询、只是参数不同。
它的代价也很直白:一次请求更大,服务端需要做批处理、排序回填、部分失败处理。
2. 请求合并:时间很近的小请求,攒一攒再发
人话版:前台看到 10 个人连续来交同一种单子,不一定每来一个就跑一趟,可以先收齐一小摞再一起送。
术语版:在客户端、网关或者中间层里,把短时间窗口内的多个小请求合成一个更大的请求,或者把重复请求去重后只发一次。
小案例:一个页面上的多个组件都要读取同一份配置,结果各自发请求。合并后,网关只向配置服务发一次,其他请求直接复用结果。
它最适合的场景是:请求时间接近、内容相似、重复多。
它的代价是:要维护合并窗口、队列和回填关系,合并策略写不好,延迟反而会抖一下。
3. 服务端聚合:客户端少操心,服务端做总包
人话版:你装修房子时,不想自己分别找水电、木工、瓦工,就找一个总包。你只对接一次,总包去协调后面的工种。
术语版:由一个聚合层、BFF 或 API Gateway 统一向多个下游服务取数,再把结果拼装成一个更贴近页面或业务动作的响应。
小案例:首页需要用户信息、订单摘要、优惠券、推荐商品。客户端原来发 4 到 6 个请求,现在只调用一次 /home,聚合层并行访问多个下游后统一返回。
它最适合的场景是:一个页面或一个业务动作要拼多份数据。
它的代价是:聚合层会更忙,出问题时影响面也更大。
把三种手段放在一张表里
| 手段 | 解决的核心问题 | 最适合的场景 | 主要收益 | 主要代价 |
|---|---|---|---|---|
| 批量 RPC | 同类请求太多 | 同一服务、同一方法、不同参数 | 少很多次网络往返 | 单次请求更大,批处理更复杂 |
| 请求合并 | 短时间内小请求太碎或重复 | 组件各自请求、重复读配置、短窗口突发调用 | 降低请求数,减少重复工作 | 需要队列、窗口、去重和回填 |
| 服务端聚合 | 一个页面要拼很多来源的数据 | 首页、详情页、工作台、报表页 | 客户端简单,首屏或动作更完整 | 聚合层 CPU、内存和故障半径变大 |
如果你看到“同类多次查”,先想批量 RPC;如果你看到“短时间碎请求乱飞”,先想请求合并;如果你看到“一个页面要拼很多服务”,先想服务端聚合。
为什么它在高 RTT 场景里特别香?
因为总耗时里,真正吃亏的常常不是计算,而是来回跑的路费。
一个粗糙但很好用的心智模型是:
总耗时 ≈ 网络往返次数 × RTT + 服务端处理时间 + 排队时间
当 RTT 很低,比如机房内调用 2ms 到 5ms,你多几次往返可能还不算肉疼。可一旦用户在弱网、跨城、跨运营商环境里,RTT 上到 100ms、150ms、200ms,往返次数立刻变成大头。
再加上客户端请求碎片化,问题会更明显。
所谓请求碎片化,就是一个页面像拼乐高一样,每块组件都自己发一个接口,最后凑出来几十个小请求。代码看起来模块化,网络看起来像下小雨,性能看起来像被慢慢放气。
用一组数字感受一下
| 方案 | 网络往返次数 | 仅 RTT 成本 | 服务端处理示意 | 用户体感总耗时示意 |
|---|---|---|---|---|
| 5 个独立请求 | 5 次 | 5 × 180ms = 900ms | 每个 20ms,合计约 100ms | 常常接近 1 秒甚至更高 |
| 1 次聚合请求 | 1 次 | 180ms | 聚合 60ms,下游并行约 80ms | 大约 260ms 到 320ms |
| 1 次聚合 + 1 次批量 RPC | 1 次 | 180ms | 聚合 60ms,批量处理 90ms | 大约 300ms 左右 |
这张表的意思很直接:在高 RTT 下,哪怕服务端多算几十毫秒,只要少掉几次往返,整体通常还是赚的;你的下一步是先测 RTT 和请求数,而不是只盯 CPU。
来看一个能复现的走读例子
场景:一个移动端首页需要 5 类数据。
-
用户信息
-
最近订单
-
可用优惠券
-
推荐商品
-
推荐商品对应的店铺信息
没优化前,为什么慢?
-
客户端先请求用户信息。
-
再请求订单和优惠券。
-
再请求推荐商品。
-
拿到推荐商品后,又根据每个商品的店铺
id去查店铺信息。 -
页面所有模块都到齐后,首页才真正像个首页。
这里最浪费的不是服务端算得慢,而是客户端像跑腿小哥,一会儿去东楼,一会儿去西楼,鞋底都快磨平了。
优化后,可以怎么改?
-
客户端只发一次首页请求。
-
首页聚合层并行调用用户、订单、优惠券、推荐服务。
-
推荐服务返回商品后,聚合层把一批店铺
id用一次批量 RPC 查回来。 -
如果 10ms 内还有重复的配置请求,网关再做一次请求合并。
-
聚合层把最终结果一次性返回给客户端。
看一遍请求流
客户端
-> 首页聚合层:请求
/home
-> 用户服务:查用户信息
-> 订单服务:查最近订单
-> 优惠券服务:查可用券
-> 推荐服务:查推荐商品
-> 店铺服务:批量查询店铺信息
shopIds[]
-> 首页聚合层:拼装结果
-> 客户端:一次返回首页需要的数据
这段流程真正说明的是:客户端少跑腿,服务端多协调;你的下一步是检查下游是否能并行,以及批量接口是否有大小上限。
代价是不是只是服务器忙一点这么简单?
不是。这个思路好用,但它绝不是白嫖。
1. CPU 压力会上来
聚合、反序列化、结果拼装、去重、排序回填,都会吃 CPU。
以前 5 个请求分散在多个地方做的小动作,现在可能集中到一个聚合层一次做完。
2. 内存压力会上来
要做批量和聚合,就要先把请求攒起来、把结果暂存住、最后再统一返回。
批次越大、结果越胖、并发越高,内存越容易顶上去。
3. 尾延迟可能变坏
一批请求里只要混进一个特别慢的子请求,整批都可能被拖住。
这就像班车本来能一次拉很多人,但只要等一个迟到的人,全车都晚点。
4. 故障半径会放大
原来某个小接口挂了,也许只影响一个小组件。
改成聚合后,如果降级策略没设计好,整页都可能受影响。
5. 返回包可能变胖
你把请求次数省下来了,却可能把响应体做大了。
如果一口气塞太多暂时用不到的数据,网络账单省了一半,包裹重量又给你补回来了。
网络往返少了,机房不会自动送你一杯奶茶;所以优化前后一定要一起看少了几次往返和多了多少服务器负担。
新手最容易踩的 4 个坑
坑 1:看到慢就做大聚合
如果 RTT 根本不高,瓶颈其实在数据库慢查询或锁竞争,这时做聚合,收益可能很小,复杂度却立刻上去。
坑 2:批量接口越大越好
批 20 个也许合适,批 2000 个就可能把单次处理拖得很长。
批量不是越大越猛,合适的上限才是关键。
坑 3:把并行写成串行
服务端聚合最怕表面上只发一次请求,内部却一层一层串着调。
那就变成客户端不跑腿了,服务端自己开始绕远路。
坑 4:没有部分失败策略
聚合接口里一个下游超时了,是整页失败,还是返回核心数据加降级内容?
这个问题不提前定,线上会替你定,而且通常定得很难看。
什么时候该用哪一种?用这张决策表收口
| 你看到的现象 | 更优先考虑 | 为什么 |
|---|---|---|
| 同一服务被连续查很多次,只是参数不同 | 批量 RPC | 一次拿回多份结果,最直接减少往返 |
| 多个组件在短时间内发相似或重复请求 | 请求合并 | 能减少碎片化和重复调用 |
| 一个页面需要拼多个服务的数据 | 服务端聚合 | 客户端只请求一次,服务端内部并行协调 |
| RTT 不高,但服务端已经很热 | 先别急着做这套 | 服务端负担增加后,可能得不偿失 |
| 已经做了聚合,但包体和尾延迟暴涨 | 缩小聚合范围或设置批量上限 | 否则只是把慢从客户端搬到了服务端 |
这张表要传达的动作很明确:先看请求形态,再选手段,不要把三种方案当成一把万能锤。
新手落地,按这 5 步做最稳
-
量 RTT:先从真实客户端视角看 P95 或 P99 RTT,别只看机房里的调用延迟。 -
数请求:一次页面加载、一次按钮点击,到底会发多少请求,哪些是串行,哪些是重复。 -
选手段:同类多次查选批量 RPC,短时间碎请求选请求合并,多服务拼页选服务端聚合。 -
设边界:给批量大小、聚合超时、部分失败、返回字段做上限和规则。 -
做验证:同时比较请求次数、客户端 P95、服务端 CPU、内存、包体大小、错误率。
如果你只做前 3 步,项目可能看起来更快;如果你把第 4 步和第 5 步也做了,项目才更像真的可上线。
最后记住这几句就够用了
-
先
测量RTT 和请求数,再决定要不要用服务端负担换更少往返。 -
先
选择正确手段:同类调用多用批量 RPC,碎请求多用请求合并,拼页面多用服务端聚合。 -
先
限制批量大小、超时和响应体,别把一个优化写成新的性能事故。 -
先
验证客户端收益,再一起看服务端 CPU、内存和尾延迟。 -
先
检查是否真的在高 RTT、请求碎片化场景里,再上这套思路,命中场景它很香,没命中场景它也挺费钱。