0.2 吞吐、延迟、成本的三方权衡
上一节:0.1 三类问题必须分开处理 | 下一节:0.3 延迟是分布,不是数字
一句话结论
「让性能变好」是一句没有意义的话。 任何优化都是在「吞吐量、延迟、资源成本」三者之间做取舍——你必须说清楚:在什么成本约束下,把哪个指标改善到什么程度。
一、用一个快递公司的故事理解三者关系
假设你开了一家快递站,每天要送 1000 个包裹:
| 策略 | 吞吐量 | 单个包裹的延迟 | 成本 |
|---|---|---|---|
| 一件一送:每来一个包裹就派一辆车 | 低(一天最多几十趟) | 极低(来了就走) | 极高(要几十辆车、几十个司机) |
| 攒够 100 件送一趟 | 高(10 趟送完) | 高(第一个包裹要等到第 100 个才出发) | 低(一辆车就够) |
看出来了吗?「吞吐高」和「延迟低」在这里是直接冲突的。
再看第三个维度:那辆车的载重、司机的数量、仓库的大小——这就是资源成本。想同时提高吞吐、降低延迟,唯一办法是加钱(更多车辆、更多司机)。
性能优化就是这么回事:
性能优化的本质,是在给定的成本预算下,重新分配「吞吐—延迟」这一对矛盾。
所以当有人说「我要性能更好」时,你必须追问:
「你能接受多大成本?你更在意「每秒处理更多」还是「每个请求更快」?」
二、三个指标的通俗定义与精确含义
2.1 吞吐量(Throughput)
- 通俗:单位时间能干完多少活。
- 精确:每秒完成的请求数(req/s、QPS、RPS)。注意分母是「成功完成」的请求数——返回 500 的错误请求不该算进吞吐。
2.2 延迟(Latency / Response Time)
- 通俗:一个请求从发出到收到结果,等了多久。
- 精确:必须说明是哪个点测的——客户端观测、网关观测、服务端 handler 内部,三者数值不同(第 1 章会展开)。
2.3 资源成本(Cost / Resource)
- 通俗:为了达到上面两个数字,花了多少钱。
- 精确:CPU 核数、内存、连接数、副本数、以及这些折算成的账单。
记住:延迟和吞吐是「结果」,成本是「代价」。只谈前两个而不谈代价的性能报告,是不完整的。
三、一个具体的数字例子:批量 vs 单条
假设你在写一个批量导入功能,要往数据库插 1000 行。
方案 A:逐条插入
每条 INSERT 往返一次数据库,耗时 5 ms
总耗时 = 1000 × 5 ms = 5000 ms(5 秒)
第 1 行的延迟 = 5 ms
吞吐 = 1000 / 5s = 200 行/秒
方案 B:批量插入(每 1000 行一次)
一次批量 INSERT 耗时 200 ms
总耗时 = 200 ms
第 1 行的延迟 = 200 ms(它必须等到整批拼好才能发出去)
吞吐 = 1000 / 0.2s = 5000 行/秒
对比:
| 总耗时 | 吞吐 | 第 1 行的延迟 | |
|---|---|---|---|
| 方案 A | 5000 ms | 200 行/秒 | 5 ms |
| 方案 B | 200 ms | 5000 行/秒 | 200 ms |
| 变化 | 快 25 倍 | 高 25 倍 | 慢 40 倍 |
注意最后一行:吞吐提升 25 倍的代价,是「单个请求的延迟变差了 40 倍」。
这在导入、导出、批处理场景是划算的(用户本来就要等整体完成)。但如果这是一个用户点一下要立刻看到结果的接口,方案 B 就是灾难。
这就是为什么「批量处理」这个优化手段不能无脑用——要先看清这个接口是「吞吐敏感」还是「延迟敏感」。
四、常见优化手段的「代价清单」
每一项优化都在消耗某样东西。下表请记住,第 7 章会展开:
| 优化手段 | 提升什么 | 牺牲什么 |
|---|---|---|
| 批处理 / 请求合并 | 吞吐 | 单请求延迟(要等攒批) |
| 加缓存 | 平均延迟、吞吐 | 一致性、内存成本、失效逻辑复杂度 |
| 加并发 / 加连接池 | 吞吐(到某点为止) | 下游压力、上下文切换、饱和风险 |
| 加大批量并行度 | 吞吐 | 尾延迟(最慢的那个决定整体) |
| 降低超时时间 | 快速失败、保护自己 | 成功率(本来能成功的变超时) |
| 重试 | 成功率 | 放大故障、下游压力、尾延迟恶化 |
| 减少功能/降级 | 全部 | 业务价值 |
最后一行很重要:「不做这个功能」往往是性价比最高的优化,但它不是技术决策,而是产品决策。
五、怎么把「取舍」变成可执行的目标
模糊的目标 → 可执行的目标,需要补三个要素:
| 模糊说法 | 缺什么 | 可执行的说法 |
|---|---|---|
| 「接口要快」 | 指标 + 阈值 | 「P99 < 100 ms」 |
| 「P99 < 100 ms」 | 负载前提 | 「在 500 QPS、100 万行数据下」 |
| 「500 QPS 下 P99 < 100 ms」 | 成本约束 + 测量点 | 「单实例 4C8G、无缓存、从客户端观测」 |
这三步做完,你才有一个可以被判定通过/失败的目标。第 1 章会把这件事做成标准表格(SLO 表 + 延迟预算表)。
六、本节小结
- 性能永远是三方的:吞吐量、延迟、资源成本,不可能同时最优。
- 优化的本质是「在成本约束下重新分配吞吐与延迟」。
- 批处理类优化会显著抬高单请求延迟——用之前先判断这个接口是吞吐敏感还是延迟敏感。
- 每种优化都有代价,把它写进报告,别只写收益。
- 「在 X 成本下,把 Y 指标改善到 Z」——这才是完整的目标陈述。
七、自测
- 产品经理说:「这个列表接口返回 1000 条太慢了,改成一次返回 10 条但支持翻页。」这个改动提升了什么、牺牲了什么?
- 你的服务在 500 QPS 时 P99 = 80 ms,团队想把 P99 降到 40 ms。除了改代码,还有哪两种「非代码」的方式?它们各有什么代价?
- 为什么「添加缓存」这条优化,有时会让 P99 变得更差?(提示:想想缓存未命中的那些请求)
- 提升:单次请求的响应体积变小 → 延迟降低、带宽降低、序列化成本降低,用户体验更快。牺牲:总请求数上升(翻页会发多次请求)、服务端总查询次数上升、若用户翻到很后面则总延迟反而更长(深分页问题)。本质上是用「更多次轻请求」换「单次重请求」,属于吞吐—延迟的重新分配。
- ① 加缓存——代价是一致性风险与内存成本;② 加机器/加副本——代价是钱,且如果瓶颈是共享数据库,加副本可能无效甚至更糟。还有第三种:降低功能复杂度(比如不做实时聚合),代价是业务价值。
- 因为缓存只加速「命中」的请求。如果命中率只有 60%,剩下 40% 未命中的请求不仅要查数据库,还要承受缓存层自己带来的开销(序列化、网络、锁)。结果:60% 的请求变快,40% 的请求略微变慢 → P50 变好,P99 可能变差。这正说明「只看平均值会做出错误决策」。