AI 接口高并发 ≠ 秒杀高并发:为什么我把并发闸门挂在 LLM 调用汇聚点
主题:资源治理与流量防护
先说结论
给 AI 接口做限流,照抄秒杀那套 QPS 限流是不够的 ------ 因为它们的请求形态根本不同。三个关键判断:
| 判断 | 内容 |
|---|---|
| ① | AI 高并发 ≠ 秒杀高并发:秒杀是"大量快请求",AI 是"少量慢请求" |
| ② | 所以核心手段不是 QPS 限流,而是并发闸门 |
| ③ | 而且闸门必须挂在真实 LLM 调用的汇聚点 ,不是 Controller ------ 按请求限流 ≠ 按 LLM 调用限流 |
顺带讲一条更早的教训:内存硬边界 这件事,-Xmx 管不住。
一、先看两张表:秒杀 vs AI
| 维度 | 秒杀 | AI |
|---|---|---|
| 请求特征 | 大量快请求(毫秒级) | 少量慢请求(SSE 流式,秒级~十几秒挂一个线程) |
| 主要风险 | 瞬时 QPS 冲高 | 少量并发即占满框架异步执行器(默认并发 8,实测),拖垮其他接口 |
| 外部约束 | 无(内部资源) | LLM API 共享配额(QPS 限制 + 按 token 收费) |
| 应对 | 限流削峰(Sentinel QPS) | 限流 + 并发闸门 + 频控 + 缓存 + 降级 |
两条铁约束:
- 每个 SSE 请求 = 一个执行槽占用秒级 → 不设闸门,按默认并发 8 算,个位数用户同时流式对话就可能吃满执行器
- LLM API 是外部共享资源,不能无限并发调
QPS 限流解决的是"入口打爆"(瞬时大量请求快速失败),但解决不了"慢请求堆积" ------ QPS=5 意味着每秒进 5 个,每个挂 15 秒,稳态下也会有 75 个线程被 AI 占着。
二、三层防护设计
| 层 | 机制 | 挂点 | 超限行为 |
|---|---|---|---|
| ① 入口 QPS 限流 | Sentinel,按接口轻重分 3 组:对话 5 / 推理 10 / 轻量 30 | Controller 的 @SentinelResource + blockHandler |
429 JSON / SSE error 事件 |
| ② 并发闸门(核心) | Semaphore,tryAcquire,满即抛自定义 AiBusyException |
所有真实 LLM 调用的汇聚点(客户端 chat 方法 + SSE 流式) | 走服务内既有降级路径 → 繁忙永不 500 |
| ③ 每用户频控 | Redis INCR + TTL,60 秒窗口,阈值 10 次/分 | 各重接口入口 | 429「操作太频繁」 |
⭐ 为什么闸门挂"LLM 调用汇聚点"而不是 Controller
这是本方案最值得讲的一个设计决策:
- 一次
/ai/search内部可能调 2 次 LLM (意图提取 + 重排);一次/ai/chat内部是意图提取 + 流式生成各一次 - 所以 按请求限流 ≠ 按 LLM 调用限流
- 闸门挂在客户端 chat 方法 + 流式方法上后,统计的是真实并发 LLM 调用数(外部 API 的真实占用),与 token 预算检查同层、语义一致
- Controller 层的
@SentinelResource只解决"入口打爆"(快速失败);闸门解决"慢请求堆积"(限并发)------ 两层互补,不是重复
typescript
AiController(@SentinelResource QPS 限流 + 频控)
└→ service(Search / Ask / Compare / Chat)
└→ DeepSeekAiClient.chat/chatWithModel/doChat ← Semaphore.acquire("chat")
└→ ChatServiceImpl.streamDeepSeek ← Semaphore.acquire("stream")
│ 闸门满 → AiBusyException
├→ service 内 try-catch → 既有降级(Search→纯 ES / Ask→busy VO / SSE→error 事件)
└→ Controller 直抛 → AiBusyExceptionAdvice → 429 JSON
关键设计 :闸门满时抛的异常走服务内既有的降级路径 ------ 所以繁忙是降级,不是 500。
三、两个部署踩的坑(都很有代表性)
| # | 坑 | 现象 → 根因 → 解法 |
|---|---|---|
| ① | pom 缺 sentinel-datasource-nacos |
启动崩 ClassNotFoundException: NacosDataSource → yml 配了 Nacos datasource 但 pom 只加了 starter → 补依赖。教训:新增 datasource 配置必须同步加依赖,缺了启动即崩(不是运行时才崩) |
| ② | blockHandler 签名缺原参数 | 首测 30 并发出现 20×500 (应该是 429)→ blockHandler 只写了 (BlockException e),而 Sentinel 反射要求 原方法全部参数 + BlockException ,找不到匹配 → FlowException 落到全局 Throwable → 500 → 改为每接口专属签名(如 searchBlock(SearchDTO, BlockException))。教训:500 而不是 429,就是"blockHandler 没接住"的典型信号 |
实测验证 :30 并发 /ai/search → 10×200 + 20×429(精确命中 QPS=10,无 500);单用户连打 15 次 → 全 429。
四、更早的一条教训:内存硬边界
⭐ -Xmx 是 JVM 自限,mem_limit 是容器硬限
两者是"自限 + 硬限"两层 ------ 只设 -Xmx 挡不住线程栈/堆外内存/agent 失控。
当时的现实:服务器 4C16G,available 仅 2.0G、无 Swap、21 个容器 mem_limit 全为 0 → 任何进程失控都能直接吃满宿主被 OOM Kill(历史上真杀过 ES)。
四项优化:
| 项 | 改动 | 收益/风险 |
|---|---|---|
① 全容器 mem_limit |
按当前 RSS +30% 余量设置 | docker update --memory 零重启即时生效;recreate 后靠 compose 持久化 |
| ② Nacos 堆 1g→512m | 加 JVM_XMS/XMX/XMN |
省 ~480M(最大单点);重启有注册抖动,微服务自动重连 |
| ③ Sentinel 显式限堆 | JAVA_OPTS: -Xms128m -Xmx256m |
消除默认 ~3.5G 堆上限隐患 |
| ④ Swap 2G | fallocate + mkswap + swapon + fstab |
仅防瞬时峰值 OOM;SSD 慢,不能依赖 |
预期效果:available 2.1G → ~2.7G,OOM 失控风险基本消除。
⭐ 一个"知道什么时候该停"的判断
8-04 JVM 调优后,某个业务服务容器的 RSS 仍然 965M。用 smaps 实测分解:
ini
堆 ~250M + Metaspace ~50M + 线程栈 350 线程 × 1M = 350M
+ DirectMemory 64M + CodeCache 37M + SkyWalking agent
线程栈是调优后的隐形大头。 但我没有继续压 -Xss256k ------ 收益每服务 ~200-300M,但 Dubbo 序列化/复杂 SQL 可能超栈 → 偶发 StackOverflow,排查极其困难。
结论:JVM 层已到合理下限,剩余靠 mem_limit + Swap 做安全网,不继续压榨。 这体现的是"知道什么时候该停"的工程判断。
五、踩坑清单
| 认知 / 坑 | 说明 |
|---|---|
| ⭐ 规则权威源只能有一个 | Sentinel 的 FlowRuleManager 是全局唯一 + 整体替换 ,不是叠加 ------ 配了 Nacos datasource 后,Nacos 空配置会擦空本地代码规则(限流静默失效);代码规则只对"Nacos 宕机/不可达"有效 |
| ⭐ "生效" ≠ "可见" | SCA 的 spring.cloud.sentinel.eager 默认 false → CommandCenter 首次流量才启动 → Dashboard 看不到规则/监控,但限流实际在工作 。生产必须 eager: true |
| ⭐ 繁忙要降级,不要 500 | 闸门满 → 走既有降级路径(返回降级结果),而不是把异常抛给用户 |
| ⚠️ compose 未同步 | 改了本地 compose 但服务器是旧的 → 心跳连错地址。部署前必须同步 compose |
| ⚠️ 规则内容的双重 URL 编码 | python quote() + curl --data-urlencode 各编一次 → 规则变成 %5B%0A... → 改用 curl 的 content@file(自动只编一次) |
| ⚠️ 中间件必须挂数据卷 | 当时 Nacos 是唯一没挂 volumes 的中间件 → 重建容器 derby 配置全丢 → 补数据卷 |
六、一句总结
限流管的是"入口",闸门管的是"占用"。 想清楚"限流对象是什么",才知道闸门该挂在哪 ------ 这是本方案里最值钱的一句话。
本文基于一个个人微服务电商演示项目的真实实施记录整理,涉及的环境信息已做脱敏处理。