AI 接口高并发 ≠ 秒杀高并发:为什么我把并发闸门挂在 LLM 调用汇聚点

AI 接口高并发 ≠ 秒杀高并发:为什么我把并发闸门挂在 LLM 调用汇聚点

主题:资源治理与流量防护

先说结论

给 AI 接口做限流,照抄秒杀那套 QPS 限流是不够的 ------ 因为它们的请求形态根本不同。三个关键判断:

判断 内容
AI 高并发 ≠ 秒杀高并发:秒杀是"大量快请求",AI 是"少量慢请求"
所以核心手段不是 QPS 限流,而是并发闸门
而且闸门必须挂在真实 LLM 调用的汇聚点 ,不是 Controller ------ 按请求限流 ≠ 按 LLM 调用限流

顺带讲一条更早的教训:内存硬边界 这件事,-Xmx 管不住。


一、先看两张表:秒杀 vs AI

维度 秒杀 AI
请求特征 大量快请求(毫秒级) 少量慢请求(SSE 流式,秒级~十几秒挂一个线程)
主要风险 瞬时 QPS 冲高 少量并发即占满框架异步执行器(默认并发 8,实测),拖垮其他接口
外部约束 无(内部资源) LLM API 共享配额(QPS 限制 + 按 token 收费)
应对 限流削峰(Sentinel QPS) 限流 + 并发闸门 + 频控 + 缓存 + 降级

两条铁约束

  1. 每个 SSE 请求 = 一个执行槽占用秒级 → 不设闸门,按默认并发 8 算,个位数用户同时流式对话就可能吃满执行器
  2. LLM API 是外部共享资源,不能无限并发调

QPS 限流解决的是"入口打爆"(瞬时大量请求快速失败),但解决不了"慢请求堆积" ------ QPS=5 意味着每秒进 5 个,每个挂 15 秒,稳态下也会有 75 个线程被 AI 占着。


二、三层防护设计

机制 挂点 超限行为
① 入口 QPS 限流 Sentinel,按接口轻重分 3 组:对话 5 / 推理 10 / 轻量 30 Controller 的 @SentinelResource + blockHandler 429 JSON / SSE error 事件
② 并发闸门(核心) SemaphoretryAcquire,满即抛自定义 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/search10×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 配置全丢 → 补数据卷

六、一句总结

限流管的是"入口",闸门管的是"占用"。 想清楚"限流对象是什么",才知道闸门该挂在哪 ------ 这是本方案里最值钱的一句话。


本文基于一个个人微服务电商演示项目的真实实施记录整理,涉及的环境信息已做脱敏处理。

相关推荐
大哥43091 小时前
Redis 主从下的库存一致性:我推翻了"付款前查库存"这个方案
后端
ikoala1 小时前
DeepSeek 官方仓库惊现 DeepSeek Harness 桌面端!
前端·javascript·后端
用户408527444142 小时前
一个个人开发者,怎么啃下 12 种工控协议?
后端
Bs_MoneyMagnet2 小时前
基于springboot+vue的旅游景区点评系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·spring·旅游
cpp_learner2 小时前
Qt 5.14.2 x86_64 静态编译 —— 从零搭建完整手册
后端
企业数字化笔记4 小时前
固定资产历史数据怎么导入系统?Excel模板、字段映射和数据校验
android·java·数据库·后端
西瓜太郎12344 小时前
request_id 如何串起请求、路由、计费和日志
后端·python
by组态4 小时前
Ricon组态系统通信配置指南
前端·后端·物联网
sbjdhjd4 小时前
ThinkPHP 5.0.10 缓存写入型 RCE 复盘:从手动搭建、换行绕过到源码单步验证 | 05
java·后端·安全·spring·网络安全·数据挖掘·php