吞吐与延迟:一个厨房比喻讲透性能压测

写于 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%

读这张表的正确姿势:

  1. 吞吐都在 73~81 这个带子里,无论并发是 100、200 还是 300。这就是那堵 ~76 的墙。
  2. 同样的吞吐,延迟天差地别:100 真实 = 0.37s,200 猛灌 = 2.54s。差别全在"排队多深",不在厨房快慢。
  3. 真实节奏的用户极抗压: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 秒)——这是最差的体验,因为受害的是全体。

限流层干两件事:

  1. 公平(多租户): 不让某一个用户 / 失控 agent 把 76 这块蛋糕独吞,每人分一片 → 同样天花板下服务更多用户。
  2. 保命(更关键): 把超出的部分挡回去 / 限速,保住其余人的亚秒体验。从"大家一起烂"变成"只限住打猛的那个,其他人不受影响"。

诚实交底:限流不增加总容量(还是 ~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/stat CPU,确认真实 CPU%(不是 loadavg)。
  • 踩过的坑:worker 循环里漏了 think-sleep → 死 socket 上紧循环重连 → 单 IP 撞 per-IP 连接上限 → 造出 1000 万次假失败。修法 = think-sleep + 失败重连 + 多 IP。压测器自己的 bug 会造出假信号,数据要交叉验真。

Read more

把 Codex CLI 的登录态"搬"到一台新服务器

场景:你在一台老机器上早就登录好了 Codex CLI,现在开了台新服务器、装好了 codex,但它没登录。你不想在新机上重新走一遍 OAuth 网页授权(有时候服务器上根本打不开浏览器),只想把老机器上那份"已经登录好的身份"复制过去。 这篇讲的就是这个搬运动作的完整方法论——为什么能搬、怎么搬、有哪些坑。命令里所有隐私都用占位符,照着换成你自己的即可。 一、先理解一件事:Codex 的登录就是一个文件 这是整个操作的地基。Codex CLI(ChatGPT OAuth 登录模式下)的登录状态,不在什么系统钥匙串里,也不在环境变量里,就是家目录下一个单独的 JSON 文件: ~/.codex/auth.json 它长这样(字段名是真的,值我打码了): { "auth_mode": "

By ladydd

哨兵机制:让 Agent 一触即醒

0. 一句话点破本质 **让"等"发生在便宜的子进程里,让贵的 agent 只在有事时醒。**心跳解决"最迟多久必有人查岗",探针解决"事情一发生几乎立刻有人到场"——两个机制回答的是两个不同的问题,谁也替代不了谁。 1. 机制全貌:会自杀的轮询进程 + 宿主的"尸体通知" 我的实现只有两块积木: 积木一:一个有明确死法的后台循环 # 放行任务的同时,后台挂上(run_in_background) for i in $(seq 1 20); do 信号=$(ssh data "tmux capture-pane -t dna

By ladydd

Agent 心跳机制·设计与实现

0. 一句话点破本质 **心跳不是闹钟,是"带着完整世界快照的自我唤醒"。**闹钟只解决"什么时候醒";心跳真正要解决的是你点出的那个问题——醒来的那个瞬间,清楚自己是谁、任务到哪了、这一跳该干什么。我所有跑得好的心跳,提示词都写得像给一个失忆的陌生人看的;所有出过事的心跳,都是因为假设"我还记得"。 1. 第一性原理:为什么"醒来知道干啥"这么难 一个长期任务里的 agent 面临三重失忆: 1. 上下文会被压缩——多轮之后早期细节只剩摘要,心跳打进来时,那条心跳提示词可能是上下文里唯一高保真的任务描述 2. 世界在你睡着时变了——下属可能干完了、卡死了、跑偏了,你脑子里的"进度"从睡着那刻就开始过期 3. 任务本身会变—

By ladydd

我没手动映射 3000,公网为什么还能访问?一次 UPnP 误开孔复盘

写在前面:标题里的“自己打开”只是当时的主观感受。路由器没有失控,也不存在神秘穿透。真正发生的是:排障自动化从局域网主动调用了 UPnP AddPortMapping,路由器按协议新增了公网映射。 1. 原本的设计边界 家里的 Open WebUI 跑在一台 Ubuntu 主机的 Docker 中: 内网主机 192.168.x.x:3000 路由器上手动配置的入口是: 公网 TCP 13000 → 内网主机:3000 外部用户不直接访问家宽端口,而是先到云端 Caddy: 用户浏览器 → https://ai.example.com (云端 Caddy) → http://home.example.com:13000 (DDNS → 家宽公网

By ladydd
陕公网安备61011302002223号 | 陕ICP备2025083092号