吞吐与延迟:一个厨房比喻讲透性能压测
写于 2026-06-26。背景: MCP 服务 跑在 3 台 ClickHouse(每台 16 核 / 64G,1 分片 3 副本)上。
我们花了一整轮做公网压测,把这套系统的极限、天花板和杠杆全摸清了。这篇把"吞吐 / 延迟 / 排队"这三个最容易混的概念讲透,配我们自己的真实实测数据。
一句话结论
我们这套系统的吞吐天花板 ≈ 76 req/s。 往里塞再多并发(100、200、300、500),每秒"做完"的还是大约 76 个,多出来的全在排队。系统不会崩,只会让每个人等得更久。
天花板能不能抬?能,但不是靠多塞请求,而是靠三个硬杠杆(文末)。
一、两个最容易混的指标:吞吐 vs 延迟
| 指标 | 是什么 | 像什么 |
|---|---|---|
| 吞吐(req/s) | 每秒完成多少个请求,是个速率 | 厨房每分钟出多少盘菜 |
| 延迟(p50/p95) | 你这一个请求从发出到拿到结果花多久 | 你这一盘菜从下单到上桌花多久 |
这俩是两个独立的旋钮。最经典的误解就是把"76 req/s"当成"每个请求 1 秒内返回"——
76 req/s 的准确意思是:系统平均每秒能做完 76 个请求。它本身不保证每个都在 1 秒内返回。
返回快不快,是延迟决定的,跟吞吐是两回事。
二、厨房比喻(整套模型一次讲完)
把整个集群想成一个最快每分钟出 76 盘菜的厨房:
- 人少时(稀稀拉拉来): 厨房随到随做,你的菜 0.4 秒上桌——又快、又没满负荷。这时吞吐低(只来几个),但延迟极好。
- 人多猛灌时(200 个同时不停下单): 厨房还是拼命出菜、速率顶到 76 盘/分不变,但你的单子排在前面 190 单后头,你的菜要等 2.5 秒。厨房一秒没歇,可你就是等得久。
关键洞察:
厨房的最大出菜速率是固定的(=吞吐天花板)。你越往里塞人,塞进去的都只是变长的队,不是更多的出菜。
延迟 = 排队 + 做菜
延迟可以精确拆成两块:
延迟 = 排队时间 + 做菜时间
(queue wait) (service time)
- 做菜时间 ≈ 恒定:CPU 实际算这一个查询,大约 0.3~0.4 秒,基本不变。
- 排队时间 = 随你超过天花板的程度暴涨:没挤时 ≈ 0,猛灌时堆到 2 秒。
所以:
- 没挤 → 排队≈0 → 延迟 ≈ 只剩做菜(0.37s)
- 猛灌 → 排队吃掉大头 → 延迟 2.5s(其中 2 秒多都在排队,真正做菜还是那 0.4 秒)
三、真实实测数据(公网压测,2 个 Go 实例)
| 场景 | 吞吐(出菜速率) | 你的延迟 p50 | p95 | CH CPU |
|---|---|---|---|---|
| 100 真实 agent(带 1.5s 思考停顿) | 54 req/s | 0.37s | 0.73s | ~44% |
| 100 猛灌(一个完立马下一个) | 81 req/s | 1.15s | 2.72s | ~84% |
| 200 真实 agent | 76 req/s | 0.74s | 1.8s | ~44% |
| 200 猛灌 | 73 req/s | 2.54s | 6.17s | ~84% |
| 300 猛灌 | 73 req/s | — | — | ~85% |
读这张表的正确姿势:
- 吞吐都在 73~81 这个带子里,无论并发是 100、200 还是 300。这就是那堵 ~76 的墙。
- 同样的吞吐,延迟天差地别:100 真实 = 0.37s,200 猛灌 = 2.54s。差别全在"排队多深",不在厨房快慢。
- 真实节奏的用户极抗压:200 个带思考停顿的真 agent,吞吐 76、延迟还在亚秒、CPU 才 44%。系统对"真实流量"非常从容——所以监控看着一直很平静,不是没压上,是真实流量根本压不满。
四、一条小学公式把它们拴在一起:Little 定律
同时在排队 / 在做的请求数 = 吞吐 × 延迟
代入我们的数:
76/s × 0.4s ≈ 30 个在飞 → 队浅,人人亚秒。76/s × 2.5s ≈ 190 个在飞 → 队里堆了 190 个,所以每人等 2.5s。
吞吐是固定的天花板,延迟随队深线性涨。 你往系统里塞的并发,超过天花板的部分,百分百变成队列深度,而队列深度直接等比例变成延迟。
五、"请求"不等于"用户"——别被 76 这个数吓到
76 req/s 是请求数,不是用户数。一个用户一次会话会打很多个请求,而且中间有大量停顿(等模型想、等人看结果)。
- 如果一个真实用户平均每 5 秒才打一下,那 76 req/s 能同时托住约 380 个这样的用户(76 × 5)。
- 所以 "76" 听起来小,但因为真实用户是断续打的,实际能服务的人远不止 76。
六、为什么要做限流层
现在系统一旦总量超过 ~76,反应是所有人一起排队变慢(p95 飙到 6 秒)——这是最差的体验,因为受害的是全体。
限流层干两件事:
- 公平(多租户): 不让某一个用户 / 失控 agent 把 76 这块蛋糕独吞,每人分一片 → 同样天花板下服务更多用户。
- 保命(更关键): 把超出的部分挡回去 / 限速,保住其余人的亚秒体验。从"大家一起烂"变成"只限住打猛的那个,其他人不受影响"。
诚实交底:限流不增加总容量(还是 ~76),它是把容量公平分配 + 守住低延迟区。它管的就是"每个用户每秒能打几下"——一限,76 这块蛋糕就能分给更多人,而且保证大家都待在"亚秒、没排队"的舒服区。
形态(两层):
- 全局准入闸(保命): 限总在飞请求数(比如 ≤ 120),超了直接
429 + Retry-After,让规矩的客户端退避。守住"绝不进入 p95 6s 的烂区"。 - 每 key 配额(公平 + 变现): 按 API key 做 token bucket,免费档每秒几下、付费档放宽。商业产品的限流层本质就是计费层。
七、天花板能不能抬?三个硬杠杆
76 是当前硬件 + 当前查询组合的天花板,不是宇宙常数。能挪它的只有三个:
| 杠杆 | 效果 | 代价 |
|---|---|---|
| 升 CPU(16→32 核 ×3) | 48→96 核 ≈ +100% → ~140-150 req/s | 最值,零拓扑改动 |
| 加第 4 副本(16 核) | 48→64 核 ≈ +33% → ~100 req/s,近线性 | 顺带 HA 更强,不用重分片 |
| 结果缓存(Redis) | 有效吞吐 = 76 / (1 − 命中率),命中率 80% → ~380 | "流氓层",省下的是 CPU |
注意:加内存不在这张表里。 我们 3 台内存一直很空(每台只占 2-3G,剩 ~55G 是 page cache 在兜热数据)——但那是"假空闲":卡的是 CPU 算力,不是内存容量。空闲内存换不出 QPS,唯一途径是上面那个结果缓存(把结果存内存里、直接跳过 CPU 计算)。要花钱,花在 CPU 上。
也别用分片(sharding)。 分片是把数据切开、每个查询扇出到多个分片——它降的是"单条大扫描"的延迟。我们不是扫描瓶颈(数据全在内存、CPU:disk = 20:1),分片只会加扇出协调开销 + 巨大的重分片运维成本。要并发就加副本,不要加分片。
附:压测方法论(怎么测出这些数)
- 闭环 N 个常驻 worker:每个 worker 打一下 → 等(think)→ 再打,循环。
think=1.5= 模拟真人 agent(看完结果再问);think=0= "一个完立马下一个" = 极限猛灌。 - 从服务器端双 IP 打公网(data + ops-110 各 100 路),比从本机可信。
- 同步密采 3 台 CH 的
/proc/statCPU,确认真实 CPU%(不是 loadavg)。 - 踩过的坑:worker 循环里漏了 think-sleep → 死 socket 上紧循环重连 → 单 IP 撞 per-IP 连接上限 → 造出 1000 万次假失败。修法 = think-sleep + 失败重连 + 多 IP。压测器自己的 bug 会造出假信号,数据要交叉验真。
陕公网安备61011302002223号