ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LLM高并发限流实战:从QPS到并发闸门的架构演进

LLM高并发限流实战:从QPS到并发闸门的架构演进 1. 从一个真实的线上事故说起去年冬天我们团队负责的一个智能客服系统上线了第三周流量突然涨了三倍。本来以为是个好消息结果监控大盘上 LLM 调用成功率从 99.2% 直接掉到 71%P99 延迟从 2.3 秒飙到 18 秒用户端开始大面积报“回复超时”。运维同学第一反应是“并发太高了赶紧上 Sentinel 限流”于是我们照着秒杀系统的老套路在网关层配了个 QPS200 的流控规则。结果呢问题不但没解决反而更糟了。因为 LLM 调用和秒杀接口有一个本质区别秒杀接口是“快进快出”LLM 调用是“慢进慢出”。一个秒杀请求可能 20 毫秒就返回了一个 LLM 请求动辄 3 到 15 秒。你在网关层限流 200 QPS意味着同一时刻可能有 200 × 8 1600 个请求正压在 LLM 服务上线程池瞬间被打满后面的请求全部排队超时。那次事故之后我把整个限流架构推倒重来核心思路就一句话并发闸门不能挂在入口要挂在 LLM 调用的汇聚点上。这篇文章就把这套方案的来龙去脉、参数计算、代码实现和踩过的坑完整地分享出来。2. 为什么 AI 接口高并发和秒杀高并发根本不是一回事2.1 请求生命周期的本质差异先看一张对比表这是我在做方案设计时画的第一张图把两类场景的核心特征列清楚后面所有决策都基于这张表。维度秒杀接口LLM 调用接口单请求耗时10~50 ms2~20 s资源占用CPU 瞬时、DB 行锁长连接、GPU/显存、Token 配额并发模型短连接、快速释放长连接、持有时间长瓶颈位置DB 库存行模型推理队列、上游配额失败代价超卖、少卖用户等待后失败、Token 白烧限流目标保护 DB保护推理资源 控制成本典型 QPS数万数十到数百这张表里最关键的一行是“单请求耗时”。秒杀场景下一个请求在系统里停留的时间极短所以“QPS 限流”和“并发数限流”几乎是等价的——QPS 200 就意味着瞬时并发大概也就 200 × 0.02 4 个。但 LLM 场景下QPS 200 意味着瞬时并发可能高达 1600 个这两个数字差了 400 倍。我见过太多团队直接把秒杀的 Sentinel 规则复制过来配个 QPS 阈值就上线结果就是限流形同虚设。因为QPS 是速率并发是存量对于长耗时请求你必须限的是存量不是速率。2.2 一个生活化的类比你可以把秒杀想象成高速公路收费站车流很快通过你控制的是“每分钟放多少辆车进来”只要入口速率控制住收费站里面永远不会堵。而 LLM 调用更像是一个只有 10 个停车位的洗车房每辆车要洗 8 分钟你如果只控制“每分钟进来 20 辆车”那洗车房门口瞬间就排了 160 辆车队伍直接堵到马路上。正确的做法是控制“洗车房里同时最多停 10 辆车”洗好一辆放一辆进来。这就是“并发闸门”和“QPS 限流”的本质区别。并发闸门管的是存量QPS 限流管的是流量。对于 LLM 这种长耗时、重资源的调用必须用并发闸门。2.3 为什么闸门要挂在“汇聚点”那为什么不能在每个业务入口各挂一个闸门非要挂到 LLM 调用的汇聚点原因有三个。第一LLM 调用往往有多个入口智能客服、内容生成、代码助手、数据分析每个业务线都可能调 LLM。你在每个入口各配一个闸门阈值怎么分分少了浪费分多了打爆。第二同一个业务可能调多个模型有的走 GPT 类接口有的走本地部署模型有的走国产大模型每个模型的承载能力不同入口层根本感知不到。第三重试和异步任务会绕过入口限流很多团队有失败重试、定时批处理任务这些流量不走用户入口但一样会打到 LLM 服务上。所以正确的做法是在所有 LLM 调用的必经之路上做一个统一的汇聚层把并发闸门挂在这里。不管流量从哪来、走哪个模型、是同步还是异步只要最终要调 LLM就必须先过这道闸门。3. 并发闸门方案的整体设计与选型考量3.1 架构分层设计我把整个链路拆成四层从外到内依次是接入层网关负责鉴权、路由、基础 QPS 限流防刷业务层各个业务服务负责业务逻辑编排LLM 汇聚层统一封装所有模型调用并发闸门就挂在这里模型层实际的 LLM 服务包括云端 API 和本地推理关键设计原则是接入层的 QPS 限流只防恶意刷量真正的资源保护放在 LLM 汇聚层的并发闸门。两层各司其职不要混用。3.2 为什么选 Sentinel 做并发闸门市面上做限流的组件不少我最终选 Sentinel理由很实在第一Sentinel 原生支持并发数流控。它的FlowRule里有个grade参数设为RuleConstant.FLOW_GRADE_THREAD就是按并发线程数限流这正是我需要的。很多轻量级限流库只支持 QPS做不了并发控制。第二支持热点参数限流和熔断降级。LLM 调用经常出现某个模型特别慢、某个租户特别占资源的情况Sentinel 的热点参数限流可以针对特定参数做精细化控制。第三规则动态推送。通过 Nacos 或 Apollo 配置中心可以实时调整并发阈值不用重启服务。LLM 上游配额经常变这个能力很关键。第四生态成熟。Spring Cloud Gateway、Dubbo、WebFlux 都有现成适配接入成本低。注意Sentinel 的并发线程数流控是基于“当前正在处理的请求数”统计的它统计的是进入资源后、退出资源前的线程数。所以你必须确保 LLM 调用被正确包裹在SphU.entry()和entry.exit()之间否则统计会失真。3.3 阈值到底怎么算这是最多人问的问题并发阈值设多少合适我的计算方法分三步。第一步测单请求平均耗时。假设你的 LLM 调用 P50 是 4 秒P99 是 12 秒。第二步确定目标吞吐。假设你希望系统稳定支撑 50 QPS。第三步用利特尔法则反推并发数并发数 QPS × 平均耗时 50 × 4 200。但这个 200 是理论值实际要打折。因为 LLM 调用耗时波动大P99 是 P50 的 3 倍如果按 P50 算遇到长尾请求时并发会瞬间超标。我的经验是按 P75 到 P90 的耗时来算再留 20% 余量。假设 P90 是 6 秒那并发阈值 50 × 6 × 0.8 ≈ 240。还有一个约束是上游配额。如果云端 API 给你的并发上限是 100那你的闸门阈值绝对不能超过 100否则请求会全部撞到上游的 429 错误。这时候要么降级到备用模型要么直接快速失败。参数取值说明目标 QPS50业务期望吞吐P50 耗时4 s中位数P90 耗时6 s用于计算安全系数0.8留余量计算并发24050×6×0.8上游配额100硬约束最终阈值100取小值这张表就是我当时实际算的最终阈值被上游配额卡死在 100。这也说明一个道理并发闸门的阈值不是你想设多少就设多少它受限于最窄的那个瓶颈。4. 核心实现把闸门挂在 LLM 调用汇聚点4.1 汇聚层的代码骨架先看汇聚层的核心结构。我用一个LlmGateway类统一封装所有模型调用所有业务都通过它来调 LLM。public class LlmGateway { private final MapString, LlmClient clients; public LlmResponse call(LlmRequest request) { String resourceName llm: request.getModel(); Entry entry null; try { entry SphU.entry(resourceName, EntryType.OUT); LlmClient client clients.get(request.getModel()); return client.invoke(request); } catch (BlockException e) { throw new LlmThrottledException(LLM 并发已达上限请稍后重试, e); } finally { if (entry ! null) { entry.exit(); } } } }这段代码有几个关键点。第一资源名按模型区分llm:gpt-4和llm:local-7b是两个独立的资源各自有独立的并发阈值。第二EntryType.OUT表示这是出站调用语义上更准确。第三finally里必须exit()否则并发数只增不减闸门会永久卡死。4.2 规则配置与动态推送规则配置我走的是 Nacos 动态数据源这样改阈值不用重启。Configuration public class SentinelConfig { PostConstruct public void init() throws Exception { ReadableDataSourceString, ListFlowRule flowRuleDataSource new NacosDataSource( nacosAddr, groupId, dataId, source - JSON.parseObject(source, new TypeReferenceListFlowRule() {}) ); FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); } }Nacos 里的规则内容长这样[ { resource: llm:gpt-4, grade: 1, count: 100, strategy: 0, controlBehavior: 0 }, { resource: llm:local-7b, grade: 1, count: 40, strategy: 0, controlBehavior: 0 } ]这里grade: 1就是FLOW_GRADE_THREAD按并发线程数限流。controlBehavior: 0是快速失败超过阈值直接抛BlockException不排队。为什么选快速失败而不是排队等待因为 LLM 调用本来就慢排队只会让用户等更久还不如快速失败让业务层走降级逻辑。4.3 业务层的降级处理闸门挡住请求后业务层不能直接把错误抛给用户要有降级策略。我一般准备三套降级到小模型GPT-4 被限流时自动切到本地 7B 模型虽然质量差一点但至少能返回返回缓存结果如果是重复问题直接返回历史缓存友好提示实在没有降级方案返回“当前咨询人数较多请稍后再试”public LlmResponse callWithFallback(LlmRequest request) { try { return llmGateway.call(request); } catch (LlmThrottledException e) { if (request.isAllowFallback()) { return llmGateway.call(request.toFallbackModel()); } return LlmResponse.busy(); } }实操心得降级模型的选择很讲究。不要选一个质量差太多的模型否则用户会觉得“怎么突然变笨了”。我一般选同系列的小一号模型比如 GPT-4 降级到 GPT-3.5或者 70B 降级到 13B体验落差相对可控。5. 实操过程中的关键细节与踩坑记录5.1 流式响应下的并发统计陷阱LLM 调用很多是流式返回SSE这里有个大坑。如果你在流式响应开始时就entry.exit()那并发统计只算了“建立连接”的时间实际模型还在生成 Token资源还在占用统计严重偏低。正确做法是在整个流式响应结束后才exit()。用 Reactor 的话要挂在doFinally上public FluxString stream(LlmRequest request) { return Flux.defer(() - { Entry entry null; try { entry SphU.entry(llm: request.getModel(), EntryType.OUT); return client.stream(request) .doFinally(signal - entry.exit()); } catch (BlockException e) { return Flux.error(new LlmThrottledException(限流, e)); } }); }这个坑我踩过当时并发阈值设了 100实际压测发现能扛 500 并发还以为是 Sentinel 不准查了半天才发现是流式响应提前释放了。5.2 重试流量必须计入闸门很多 HTTP 客户端自带重试比如 Feign 的Retryer、OkHttp 的retryOnConnectionFailure。这些重试如果发生在SphU.entry()内部会被正确统计但如果重试逻辑在汇聚层外面就会绕过闸门。我的做法是把重试逻辑收进汇聚层确保每次实际调用都过一次闸门。同时给重试加个上限最多 2 次避免重试风暴。5.3 异步任务的并发隔离定时批处理、消息队列消费这类异步任务如果和在线业务共用同一个闸门会出现“批处理把在线业务挤死”的情况。解决办法是按调用来源做资源隔离用不同的资源名llm:gpt-4:online给在线业务llm:gpt-4:batch给批处理然后给 batch 设一个较小的阈值比如 online 的 20%。这样即使批处理跑满也只占用 20% 的配额在线业务不受影响。5.4 常见问题速查表现象可能原因排查方向解决方案并发数只增不减entry.exit() 未执行检查异常路径确保 finally 中 exit限流不生效资源名不匹配打印实际资源名统一资源命名规范流式响应统计偏低提前 exit检查 doFinally响应结束后再 exit重试绕过闸门重试在闸门外检查客户端配置重试收进汇聚层批处理挤死在线共用资源检查资源名按来源隔离资源阈值改了不生效规则未推送检查 Nacos 连接确认数据源注册成功这张表是我运维半年攒下来的基本覆盖了 90% 的线上问题。建议你接入时对照着排查一遍。6. 压测验证与效果对比6.1 压测方案设计方案上线前我做了三轮压测。第一轮基准压测不加任何限流看系统极限在哪里。第二轮QPS 限流压测用秒杀那套方案验证它为什么不行。第三轮并发闸门压测验证新方案的效果。压测工具用的 JMeter模拟 200 个并发用户持续 10 分钟LLM 调用用 Mock 服务模拟固定 4 秒延迟。6.2 三轮压测结果对比方案成功率P99 延迟上游 429 错误资源占用无限流68%22 s大量打满QPS 限流 20074%18 s较多打满并发闸门 10099.1%6.5 s极少稳定数据很说明问题。无限流时系统被自己的流量打爆成功率只有 68%。QPS 限流 200 看起来限制了入口但因为请求耗时长实际并发还是压到了 1600效果有限。并发闸门 100 把实际并发死死控制在 100成功率 99.1%P99 延迟 6.5 秒正好是单请求耗时的 1.6 倍符合预期。6.3 成本对比还有一个容易被忽略的收益是成本。无限流时大量请求打到上游后超时失败但 Token 已经消耗了钱白烧。我统计过限流前每天因为超时浪费的 Token 成本大概占总成本的 15%。上了并发闸门后这部分浪费降到 2% 以内。一个月下来省的钱够买好几张显卡了。实操心得压测时一定要用真实的长耗时场景不要用 Mock 的 100ms 延迟。我见过有团队用快速 Mock 压测结果上线后完全不是一回事。LLM 的耗时特性是方案设计的核心依据压测必须还原。7. 一些延伸思考与后续优化方向7.1 从并发闸门到自适应限流固定阈值的并发闸门有个问题阈值是静态的但 LLM 服务的实际承载能力是动态的。上游可能因为负载高而变慢本地 GPU 可能因为显存碎片而性能下降。这时候固定阈值要么太保守浪费资源要么太激进打爆服务。后续我打算引入自适应限流根据实时的响应时间和错误率动态调整阈值。Sentinel 本身有SystemRule做系统自适应保护但针对 LLM 场景还需要定制。思路是当 P99 延迟超过阈值时自动降低并发上限当延迟恢复正常时缓慢提升。这样系统能自己找到最优工作点。7.2 按 Token 数限流并发数限流有个粒度问题一个请求可能只生成 10 个 Token也可能生成 4000 个 Token但它们占用的并发数都是 1。对于按 Token 计费的上游这会导致成本不可控。更精细的做法是按 Token 数做限流比如“每秒最多消耗 10000 个 Token”。这需要在请求前后统计 Token 数实现复杂度更高但对成本控制更精准。我目前是在并发闸门基础上加了一层 Token 预算控制作为第二道防线。7.3 多模型统一调度现在每个模型一个闸门业务层要自己决定调哪个模型。未来我想做一个统一调度层业务只说“我要一个高质量回复”调度层根据当前各模型的负载、配额、成本自动选择最合适的模型。这样业务层不用关心模型细节调度层也能做全局最优的资源分配。这套并发闸门的方案从那次事故到现在跑了半年多中间又迭代了三四版目前支撑着我们所有 AI 业务的稳定运行。核心思想其实就一句话LLM 调用是长耗时重资源操作必须用并发闸门而不是 QPS 限流而且闸门要挂在所有调用的汇聚点上。理解了这一点剩下的都是工程细节。
返回列表