KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

07 · 并发量级的心智模型:从「撑得住多少」倒推架构 — keel 龙骨

这一章回答:当别人说"我们上万并发"、或者你自己看着监控上的 QPS 发愁时,到底该盯哪个数字,以及你的系统现在站在哪一档。

这一章回答:当别人说"我们上万并发"、或者你自己看着监控上的 QPS 发愁时,到底该盯哪个数字,以及你的系统现在站在哪一档。

前六章讲的是"某个机制怎么用"。这一章补一个前置问题:同样一句"并发上来了",在 100、1000、10000、100000 这几个量级上,卡住你的根本不是同一个东西。分不清量级就选型,是后端最常见的浪费——把 Kafka 塞进只有千级 QPS 的系统,或者在十万长连接场景里纠结要不要用分布式锁。

一、问题现场:QPS 才 900,为什么先崩的是我

先建立一个具体现场(这是 AI 服务最常见的一类事故,不是假设):

先别往下读,猜一下:为什么"QPS 才 900"这个数字没能救你?

答案是:900 QPS × 平均 2.7s 的停留时间 ≈ 2430 个请求同时在途,而你的 4 个 worker 加上 20 条数据库连接,最多只能同时"招待"两三百个。多出来的两千多个请求全部堆在排队里。加两台机器只是把容量从 200 抬到 300,缺口还是两千多——所以延迟几乎没降。

这里的关键是:QPS 不是压力指标,在途并发数才是。QPS 低不代表系统轻松,如果每个请求占着资源不放,很低的 QPS 也能压垮你。AI 服务尤其如此,因为模型调用天然是"长任务"。

二、直觉模型:把服务想成一家餐厅

把你的服务想成一家餐厅,三个数字就都有了对应物:

系统里的量 餐厅里的量 含义
在途并发数 L 店里同时在吃的桌数 占着座位还没走
吞吐 QPS(λ) 每秒结账离开的桌数(翻台率) 单位时间完成多少
单请求耗时 W 一桌吃多久 占用时长

三者的关系是排队论里最基础的一条——Little's Law:

在途并发数 L = 吞吐 λ × 平均耗时 W

用餐厅来感受它为什么反直觉:

同样的"客流量/翻台率",慢餐需要的座位数是快餐的 18 倍。 换成系统语言:同样的 500 QPS,普通接口(0.2s)只需要 100 个并发位;换成 LLM 长任务(2s)就需要 1000 个。

AI 服务天然是火锅店。 这也是为什么"加机器没用"——你加的是"服务员",但座位被长任务占着不放。真正要动的是座位数(并发位、连接池、worker 并发度)和让客人别堵在店里(长任务队列化、限流准入)。

把整章的换算收进一张图。注意图右侧那条分支——它是这一章真正要你记住的东西:

flowchart TD
    A["DAU / 在线用户<br/>(10w 级,最外层)"] --> B["峰值 QPS λ<br/>受使用频次与高峰集中度影响"]
    B --> C{"单请求耗时 W"}
    C -->|"普通接口 0.2s"| D["在途并发 L = λ × 0.2"]
    C -->|"LLM 长任务 2s"| E["在途并发 L = λ × 2<br/>同样 QPS,10 倍"]
    D --> F{"L 与系统容量比?"}
    E --> F
    F -->|"L < 容量"| G["稳定:延迟 ≈ W"]
    F -->|"L > 容量"| H["排队爆炸:延迟无限增长<br/>见第六节实验"]
    H --> I["对策:削峰 + 限流准入<br/>+ 长任务离开在线槽位"]
    G --> J["量级定档:百 / 千 / 万 / 十万<br/>每档瓶颈不同(第五节)"]
    I --> J

三、精确定义:四个被混为一谈的数字

澄清下面四个数字,是这一章最值钱的部分。它们经常被当成一件事,量级能差出三四个数量级。

数字 精确定义 怎么观测 常见误用
在途并发数 同一时刻已接收、还没返回的请求数 服务端埋点(请求进入 +1、返回 -1),或用 L = λ × W 估算 被当成"用户数"或"线程池大小"
QPS / 吞吐 λ 每秒完成多少个请求 网关或 APM 的 RPS 被用来判断"压力大不大"——配合 W 才有意义
连接数 TCP 连接条数 ss -s、/proc/net/sockstat 把 keep-alive / SSE 的空闲连接当成并发
活跃用户 / DAU 最外层的人数 埋点、日志去重 直接除以某个常数就说"我们要支持 N 并发"

几个必须能脱口而出的换算:

① 十万 DAU 不等于十万并发。 10 万 DAU、每人每天 20 次请求、请求集中在 4 小时高峰、单次 0.3s:

峰值 QPS ≈ 100000 × 20 / (4 × 3600) ≈ 139 QPS
在途并发 ≈ 139 × 0.3 ≈ 42

同样这些用户,如果每人每天问的是 Agent(单次 3s),在途并发变成约 417。仍然是几百,不是十万。DAU 和在途并发之间隔着"使用频次""峰值集中度""单次耗时"三个衰减/放大系数,任何一个不估就直接换算,都会得出荒谬的结论。

② 一万长连接也不等于一万并发。 假设 1 万个 SSE 连接,平均 30s 才推一次消息、每次推送处理 50ms:

推送到达率 ≈ 10000 / 30 ≈ 333 次/秒
处理侧在途并发 ≈ 333 × 0.05 ≈ 17

这 1 万条连接在处理侧只对应约 17 个在途并发,但它们实打实占着 1 万个 fd、1 万个连接对象和对应的内存。长连接场景的瓶颈通常在"连接维持"而不是"计算并发",两者要分开算、分开扩容——这是实时推送类系统最容易搞错的地方(realtime-streaming-and-sse 那门课专门处理这一侧)。

③ 并发池大小不是并发数。 连接池 20 意思是"最多 20 条",不代表真的有 20 个在跑;反过来,在途并发超过池大小时,多出来的请求在排队,此时的延迟不再由处理速度决定,而由队列长度决定。

一句话收口:问"我们多少并发"之前,先问清楚是哪一个数字。 对方说十万,多半是 DAU 或连接数;你真正要为容量买单的是在途并发数。

四、一次完整运行:让数字自己说话

下面两组输出来自本机实测(环境见注释),不是经验值。

实验 1 · Little's Law 的放大效应(纯算术,与机器无关):

   目标 QPS       单请求耗时 W       在途并发 L  场景
      500          0.2s          100  普通接口
      500          2.0s         1000  LLM 长任务
     1000          0.2s          200  普通接口
     1000          2.0s         2000  LLM 长任务
     5000          0.2s         1000  普通接口
     5000          0.5s         2500  LLM 短调用
    10000          1.5s        15000  LLM 长任务

请盯住第 2 行和第 1 行:同样是 500 QPS,把单请求从 0.2s 换成 2s,在途并发从 100 变成 1000——整整十倍。这就是"AI 服务为什么特别吃并发位"的全部原因,不需要任何玄学。

实验 2 · IO 密集下,协程与"一请求一线程"的差别(每请求 sleep 0.5s 模拟一次模型调用):

   并发 N    asyncio 耗时        线程版耗时       需要几个 OS 线程
    100         0.51s        0.51s              100
    500         0.52s        0.57s              500
   1000         0.51s        0.66s             1000
   2000         0.52s        0.85s             2000

  同一批并发,线程版要额外付出的固定成本:
    线程数 N        创建耗时       RSS 增量
      100      0.014s      +0.0 MB
      500      0.071s      +0.2 MB
     1000      0.149s      +0.4 MB
     2000      0.311s      +0.9 MB
     4000      0.620s      +1.9 MB

怎么读这两张表:

环境说明(重要):以上实测在 Python 3.13.14 / 32 逻辑 CPU / Windows 开发机上跑出。生产 Linux 的绝对值(fork 成本、线程栈保留、调度行为)都不同。请把它当成"形状"来读,不要当成阈值抄走——这正是本课程的一贯纪律:这些数字是起点,不是答案。

五、量级的台阶:每一档卡住你的不是同一个东西

把在途并发数分成几档,每档的主要矛盾完全不同。这张表是这一章的骨架:

量级(在途并发) 主要瓶颈 最先崩的地方 该盯的指标
百级以内(< 100) 单条 SQL / 单次外部调用的质量 慢查询、无索引、无超时 P99、慢查询日志
千级(几百 ~ 2 千) 并发位与连接池上限、同步阻塞点 worker 打满、连接池耗尽、一个阻塞调用拖垮全站 在途并发数、池使用率、阻塞点耗时
万级(2 千 ~ 2 万) 单机资源上限:fd、内存/连接、调度 长任务占满并发位、队列无限增长、fd/端口耗尽 队列长度、fd 使用、事件循环延迟
十万级(> 10 万,多为长连接或极高 QPS) 单机根本不够:连接维持 + 无状态扩展 单机天花板、LB 与分片、全局限流 连接数、实例数、分片均衡度

注意这张表的单位是在途并发,不是用户数、不是连接数。十万级这一档通常有两种成因,处理方式完全不同:一是极高 QPS 的短请求(电商大促),二是海量长连接(推送/IM/IoT)。前者靠水平扩展 + 分片吞吐,后者靠连接网关 + 心跳/空闲治理——把这两种当成一种,是十万级最常见的误判。

几个"要核对的旋钮"(Linux 生产环境;它们是待校准的起点,不是推荐值):

旋钮 查看方式 为什么值得看
进程 fd 上限 ulimit -n 万级连接时先撞它,症状是 Too many open files
accept 队列 sysctl net.core.somaxconn 突发建连时队列溢出,表现为连接被拒
出向端口范围 sysctl net.ipv4.ip_local_port_range 网关/压测机作为客户端时,四元组耗尽会连不上游
进程线程数 /proc/<pid>/status 的 Threads 验证"一请求一线程"到底开了多少
连接池 size 应用配置 不是越大越好——见下一节实验

六、长任务为什么把量级放大:排队爆炸

AI 场景的长任务有双重代价:占座久(Little's Law 直接放大在途并发),而且占着硬资源(模型并发额度、内存、有时还占着数据库连接)。

实验 3 演示第二件事有多致命——一旦到达率超过池的处理能力,延迟不是"变慢",而是无限增长:

池容量 W=20,单任务 2.0s -> 理论吞吐上限 = 10.0 个/秒
到达率 λ=15/s  ->  超过容量,必然积压
投放 150 个任务,全部完成耗时 16.8s
  最快完成的任务等了 2.0s,中位 9.2s,最慢 16.8s
  最慢那个纯排队等了 14.8s,而任务本身只要 2.0s

读法:池的吞吐上限是 W / 单任务耗时 = 20 / 2 = 10 个/秒,而到达率是 15 个/秒——每秒净积压 5 个。于是最后一批任务的等待时间(14.8s)是任务本身耗时(2.0s)的七倍多,而且只要到达率不降,队列会一直涨下去。

这解释了开篇那个现场为什么"加机器没用":只要 λ > 容量,队列单调增长;加机器把容量从 10 抬到 12,只是把爆炸时间推后。在这种状态下,第一优先级不是扩容,而是做三件事:

  1. 削峰:把长任务投递到队列,由独立 worker 按容量消费(05 章的领地);
  2. 准入限流:到不了就快速失败,而不是让队列无限长(03 章的领地);
  3. 让长任务离开在线请求的槽位:在线接口只负责"接单 + 返回任务 ID",不陪着等模型。

一句话:长任务的水位不是靠加机器降下去的,是靠"不让它占在线槽位 + 不接受超出容量的活"降下去的。

七、动手:可观察结果

产出 判断标准
一张量级换算表 写出你的系统:目标峰值 QPS、实测 P50/P99、算出在途并发需求 = QPS × P99;再对比当前并发容量,缺口是多少
一次加机器 vs 削峰对比 在到达率超过容量的状态下,分别用"加一倍 worker"和"加队列 + 限流"两种方式压测,记录 P99 与队列长度的变化——你会看到只有后者能让延迟收敛
一份我现在在第几档判定 用第五节的表给自己的系统定档,并写下"这一档的主要瓶颈是什么";说不出瓶颈,说明还没测到点上

完成标志:你能用一句话回答"我们系统现在在途并发多少、容量多少、缺口多少",并且这三个数字都有观测来源。

八、故障注入

注入方式 观察什么 期望的正确行为
人为把单请求耗时拉到 3s(在接口里 sleep),QPS 保持不变 在途并发是否按 L = λ × W 线性放大;池是否被打满 监控能直接看到在途并发上升,而不是只看到"接口变慢"
关掉限流,让到达率略高于池容量(如容量 10/s、λ=12/s) 队列长度是否单调增长、P99 是否发散 有背压/限流,队列封顶并快速失败;无保护则应当复现实验 3 的发散
把数据库连接池从 20 调到 200 DB 侧连接数与 P99 的变化 很可能更差(连接越多,上下文切换与锁竞争越重)——证明"池不是越大越好"
起 5000 个空闲 SSE 长连接,不推消息 fd、内存、处理侧在途并发 确认"连接数几千,处理侧并发接近 0",把两者分开计量

九、生产边界(替身 → 替换点)

教学替身 真实替换点 注意
asyncio.sleep 模拟模型调用 真实模型 SDK 调用 真实调用有连接复用、超时、重试、限流额度,不只是"慢"
本机 semaphore 模拟并发池 生产 worker / 队列消费者并发度 真实池还要考虑实例数 × 每实例并发度
单机进程池 多实例 + 队列 + 独立 worker 跨实例后需要分布式限流与全局队列水位
本机实测数字 你的目标环境实测 本机是 Windows 开发机,Linux 生产值必须重新测

十、自测题

  1. 两个系统 QPS 都是 500,A 的单请求 0.2s、B 的单请求 2.0s,谁需要的并发容量更大?大多少?为什么?
  2. "我们 10 万 DAU"能不能直接推出并发数?中间隔着哪三个系数?缺一个会怎样?
  3. 1 万个 SSE 长连接,处理侧在途并发可能只有几十——为什么?这两者各自消耗什么资源?
  4. 为什么"到达率略高于池容量"会比"到达率远超容量"更危险(更隐蔽)?队列会怎样变化?
  5. 你的系统现在在途并发多少?容量多少?这两个数字你分别是从哪个指标读出来的?如果读不出来,缺的是什么埋点?
  6. 加机器在什么时候有用、在什么时候没用?用 Little's Law 和排队论各解释一遍。

现在能解释什么

读完这一章,你应该能:

进入 keel 阅读