首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别再只堆 GPU 了!RAG 扛不住高并发,是因为你没懂这三件事

别再只堆 GPU 了!RAG 扛不住高并发,是因为你没懂这三件事

作者头像
java金融
发布2026-05-20 12:49:43
发布2026-05-20 12:49:43
860
举报
文章被收录于专栏:java金融java金融

凌晨两点,手机突然疯狂震动。

报警群里疯狂刷屏:“RAG 服务 P99 延迟超过 30s!”“GPU 利用率 100% 但请求全在排队!”“客服那边炸了,用户投诉进不来了!”

你揉着惺忪的睡眼打开监控面板,发现白天还顺滑得像德芙巧克力一样的 RAG 服务,此刻正像一辆抛锚在五环路上的破车,堵得水泄不通。

老板的消息紧接着弹出来:“怎么回事?平时不是挺稳的吗?今晚流量也没翻倍啊,赶紧保住链路!”

这是很多做 AI 落地同学都经历过的“至暗时刻”。RAG(检索增强生成)这东西,Demo 的时候跑得欢,真上线一碰流量洪峰,立马原形毕露。

这其实不是你运气不好,而是你从一开始就算错了账。

最近在字节跳动的面试场上,这道关于“RAG 扛量”的题目,刷掉了不少自以为懂架构的候选人。今天我们就把这道题拆开揉碎,聊聊怎么让 RAG 在流量洪峰中站得稳、扛得住。


01

面试现场:堆机器就能解决问题吗?

先还原一下那个让很多候选人卡壳的面试现场。

面试官推了推眼镜,抛出了那个经典的问题:

“你们现在的 RAG 服务,白天平稳运行在 200 QPS,没什么压力。但是一旦遇到活动或者晚高峰,流量瞬间冲到 800 QPS,服务就崩了。如果是你,你会怎么优化?”

坐在对面的候选人信心满满,毕竟这也是他之前在项目中遇到过的“实战经验”。

他脱口而出:“这个简单,加机器。横向扩容,再挂几台 GPU 服务器,或者把显卡换成 A100、H800,算力上去了,QPS 自然就扛住了。”

面试官没说话,只是轻轻敲了敲桌子,追了一句:

“加机器之前,你算过单卡每秒能处理多少 token 吗?你知道这 800 QPS 到了 LLM 那一层,相当于每秒要吞掉多少 token 吗?”

空气突然安静了三秒。

候选人愣住了。他习惯了用“请求数(QPS)”来衡量压力,却忘了在 LLM 这里,真正的货币是 Token,而不是请求次数

这道题看似在考扩容,实际上是在考你对 RAG 链路的成本结构有没有清晰的认识。

如果只是一刀切地堆 GPU,结果往往是:最贵的 GPU 闲着,因为瓶颈根本不在那儿;或者 GPU 满载了,但排队时间太长,用户早就把网页关了。

正确的解题思路,从来不是“堆硬件”,而是:先分段定位瓶颈、再用 Token 维度限流、最后上多级缓存


02

误区:为什么不能只看 QPS?

要理解为什么“堆 GPU”是下策,我们得先把 RAG 的链路拆开看。

很多人把 RAG 当成一个黑盒子,进一个请求,出一个答案。但在工程视角下,RAG 是一条由四个截然不同的“工种”组成的流水线:

  1. Embedding(文字转向量):这是“搬砖”的活,CPU 就能干,而且速度快得惊人。单节点几千 QPS 轻轻松松。
  2. 向量召回(找近邻):这是“查档案”的活,去 Milvus 或 Faiss 里找相似向量。单实例几百到上千 QPS 没太大压力。
  3. Rerank(候选重排):这是“精挑细选”的活,需要模型跑一遍,稍微慢点,单卡几十 QPS。
  4. LLM 生成(写答案):这是“大作家”的活,也是最贵的。生成 1k 个 token 可能需要 1~3 秒。单卡通常只能支撑个位数到十几个 QPS。

看到了吗?

这四段的扩容代价差了两个数量级!最贵的 LLM 段,比最便宜的 Embedding 段,成本高出 100 倍不止。

如果你只看入口的 800 QPS 就去扩容,那就像是“为了解决厕所排队慢,去扩建了停车场”。

更致命的误区在于限流。

很多同学在 Nginx 层面配置了 limit_req,比如限制每秒 100 个请求。但这在 RAG 场景下完全是掩耳盗铃。

因为请求和请求是不一样的。

  • 请求 A:“你好” -> 输入 2 token,输出 10 token。
  • 请求 B:“请帮我总结一下这份 50 页的财报,并分析其现金流风险” -> 输入 8000 token,输出 2000 token。

在 Nginx 眼里,A 和 B 都是“1 个请求”。但在 GPU 眼里,B 的代价是 A 的 500 倍。

如果你按请求数限流,要么把便宜请求挡在外面(浪费资源),要么放进来几个大请求直接把 GPU 队列撑爆(雪崩)。

结论很简单:限流必须按 Token 算,而且必须挡在最贵的 LLM 前面。


03

深度解析:RAG 扛量的四道防线

要想扛住 800 QPS 甚至更高,我们需要在 RAG 链路上建立四道防线。

判断 1:崩的几乎一定是 LLM 段

当系统报警时,不要先去查 Embedding 慢没慢,也不要先去查数据库锁没锁。

先去看 LLM 的排队队列长度。

因为 LLM 是计算密集型且资源最稀缺的环节。只要流量一涨,第一个饱和的肯定是它。一旦队列积压,延迟就会呈指数级上升。

所以,监控大盘上,最重要的那个指标不是 system_qps,而是 llm_queue_lengthllm_latency_p99

判断 2:限流要按 Token 算

怎么落地 Token 限流?

最简单的方案:在调用 LLM 之前,先估算一下这次请求要花多少 Token(Input Token + 预估 Output Token)。

然后去 Redis 里查一下这个用户的“剩余 Token 配额”。

  • 够扣?放行,扣减配额。
  • 不够扣?直接返回 429(Too Many Requests),或者让用户进排队队列。

这就叫“Token-based Rate Limiting”。

这就好比去自助餐厅,不是限制“进店的人数”,而是限制“每个人能吃多少克牛肉”。这样不管你是大胃王还是小鸟胃,餐厅的总成本都是可控的。

判断 3:Continuous Batching 是把双刃剑

现在主流的推理框架(如 vLLM)都支持 Continuous Batching(连续批处理)。

它的原理很简单:把多个请求塞进同一个 GPU 里一起跑,大家共享 GPU 的算力。这能显著提升吞吐量(Throughput),理论上能翻 3~5 倍。

但是,天下没有免费的午餐。

Continuous Batching 的代价是:你的请求要等队友

想象一下你在坐公交车。如果车上有一个大爷要在终点站才下车,那你哪怕只坐两站,也得陪着他一路堵过去。

在 Batching 里也是一样。如果同一个 Batch 里有一个请求要生成 4000 个 token,而你的请求只需要 200 个,你也得等着它把 KV Cache 释放掉,你的请求才能完成。

这就是典型的“长尾效应”。

怎么办?

  • 在线客服场景:用户对延迟极度敏感。这时候要盯紧 P99 延迟,甚至可能需要限制单个请求的最大 max_tokens,或者把长请求隔离到独立的 GPU 池子里去,别让它影响普通用户。
  • 离线批处理场景:比如晚上跑报表生成,这时候无脑开 Batching,吞吐量越高越好。

判断 4:缓存不是万能药,用错了是毒药

提到扛量,大家第一反应就是“加缓存”。

但在 RAG 里,缓存是个技术活。因为知识是会更新的,你缓存了旧答案,就是给用户埋雷。

我们可以把缓存分为三级,风险从低到高:

  1. Query -> Embedding 缓存
    • 存什么:用户问题对应的向量。
    • 风险:零。只要 Embedding 模型不变,这个向量永远是对的。
    • 建议:无脑上,命中率通常很高。
  2. 检索结果缓存
    • 存什么:向量检索 + Rerank 后的 TopK 文档片段。
    • 风险:中等。如果知识库更新了,旧结果就失效了。
    • 建议:缓存 Key 里必须带上“知识库版本号”。每次更新知识库,版本号 +1,旧缓存自然失效。
  3. Query -> Answer 缓存
    • 存什么:LLM 生成的最终答案。
    • 风险:极高。一旦命中,完全跳过 LLM,速度最快。但如果知识库改了,用户就会看到过时的回答。
    • 建议:只在“高频 + 答案稳定”的场景(如 FAQ)开启,且必须设置较短的 TTL,甚至要做二次校验。

04

实战:把理论变成代码

光说不练假把式。我们来看一个真实的改造案例。

场景设定

某大型券商上线了一款“智能投研助手”,帮助分析师快速查询研报、解析公告。

  • 平时:只有几十个核心用户在用,QPS 在 50 左右,系统稳如老狗。
  • 问题时刻:每天下午 3 点股市收盘后,或者美联储发布议息决议的当晚,几百个分析师同时涌入,询问“对XX板块的影响”。QPS 瞬间飙升到 600+。
  • 后果:LLM 排队超过 40 秒,前端大面积超时,分析师在群里骂娘。

改造第一步:Embedding 缓存先上

这是性价比最高的一步。

我们在 Redis 里加了一层缓存,Key 是 hash(query_text),Value 是 embedding 向量。

代码语言:javascript
复制
# 伪代码示例
def get_embedding(query):
    cache_key = f"emb:{hash(query)}"
    vec = redis.get(cache_key)
    if vec:
        return vec
    
    # 缓存未命中,调用模型
    vec = embedding_model.encode(query)
    redis.setex(cache_key, 86400, vec) # 缓存 24 小时
    return vec

效果:Embedding 段的 CPU 占用直接降了一半。因为分析师们问的问题高度重合(“今天的收盘价是多少”、“XX公司的财报出了吗”),Embedding 缓存命中率轻松达到了 60% 以上。

改造第二步:Token 限流挡在 LLM 门前

我们在 LLM Gateway 层加了一个令牌桶算法。

不再限制“每秒多少个请求”,而是限制“每个用户每分钟多少 Token”。

代码语言:javascript
复制
# 伪代码示例
def check_rate_limit(user_id, input_tokens, estimated_output_tokens):
    total_tokens = input_tokens + estimated_output_tokens
    key = f"limit:{user_id}"
    
    # Redis 令牌桶扣减逻辑
    current = redis.decrby(key, total_tokens)
    if current < 0:
        # 配额不足,回滚并拒绝
        redis.incrby(key, total_tokens)
        raise RateLimitExceeded("Token 配额不足,请稍后再试")
    return True

效果:那些喜欢把几百页 PDF 扔进来的分析师,发几个请求后就会触发限流。这保护了 GPU 队列不会被几个“大胃王”撑爆。LLM 的排队长度从 200+ 降到了 20 以内。

改造第三步:检索结果缓存带版本号

我们给知识库里的每份研报都打了一个 etl_version

缓存 Key 设计为:retrieval:{hash(query)}:{kb_version}

每次 ETL 任务跑完,更新知识库时,全局版本号自动 +1。所有旧的检索结果缓存瞬间失效。

效果:向量库的压力减少了 30%,而且不用担心分析师看到昨天的旧数据。

改造第四步:FAQ 白名单全链路缓存

对于像“交易时间”、“休市日历”这种万年不变的问题,我们直接开启了 Query -> Answer 缓存。

但为了安全,我们维护了一个“意图白名单”,只有识别出是 FAQ 类型的 Query,才会走这层缓存。

最终数据

经过这一套组合拳,在同样的 600 QPS 流量洪峰下:

  • P99 延迟:从 40s 降到了 1.5s。
  • 超时率:从 15% 降到了 0.1%。
  • LLM 调用量:实际下降了 40%(大量请求被缓存拦截)。
  • 成本没有增加一块 GPU,靠架构优化硬扛住了流量。

05

面试追问:还能再深一点吗?

如果面试官觉得你答得不错,通常会继续追问三个细节。如果你能接住,那基本就是 Offer 预定了。

追问 1:为什么 Continuous Batching 会增加长尾延迟?

回答要点: 因为 Batching 机制要求同一个 Batch 内的所有请求“同生共死”。只要有一个人还在生成,GPU 的计算资源就被占用,其他人即使生完了也得等着。当请求长度方差很大(有的短,有的巨长)时,P99 延迟就会被那个最长的请求拖垮。

追问 2:怎么解决 Batching 的长尾问题?

回答要点

  • 策略 A:分池。把长文本任务和短文本任务路由到不同的 GPU 集群。
  • 策略 B:强切片。限制 max_tokens,强制截断,或者让大模型学会“分多次生成”。
  • 策略 C:监控。实时监控 P99/P50 的比值,如果超过 5 倍,说明长尾严重,需要扩容或调整策略。

追问 3:Query -> Answer 缓存怎么保证一致性?

回答要点

  • 版本号:Key 里必须包含 KB 版本。
  • TTL:根据数据时效性设置,比如新闻类 5 分钟,政策类 1 小时,百科类 24 小时。
  • 主动失效:如果后台有管理员手动修改了某个文档,要通过消息队列主动清除相关的缓存 Key。

06

总结:RAG 工程师的生存法则

RAG 落地,算法决定了上限,但工程决定了下限。

千万别以为 RAG 就是调个 API、接个向量库那么简单。当流量上来的时候,它就是一个复杂的分布式系统工程问题。

记住这张防崩清单

  • [ ] 分段监控:Embedding、召回、Rerank、LLM,各段的 QPS 和延迟要分开看。
  • [ ] Token 限流:不要用 Nginx 的 RPS 限流,要在 LLM Gateway 层做 Token 维度的限流。
  • [ ] 缓存分级:Embedding 缓存优先上,结果缓存看版本,全链路缓存必须小心。
  • [ ] Batching 调优:关注 P99 延迟,别被吞吐量指标迷惑了双眼。
  • [ ] 算力预估:上线前一定要算账:1 张卡每秒能吐多少 Token?你的业务峰值需要多少 Token?

下次再遇到“白天 200 QPS 稳,晚上 800 QPS 崩”的问题,别急着找老板要钱买显卡。

先看看你的 Redis 缓存热不热,再看看你的限流器是不是在裸奔。

很多时候,救命的方案不是加钱,而是加脑子。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 java金融 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 01
  • 02
  • 03
    • 判断 1:崩的几乎一定是 LLM 段
    • 判断 2:限流要按 Token 算
    • 判断 3:Continuous Batching 是把双刃剑
    • 判断 4:缓存不是万能药,用错了是毒药
  • 04
    • 场景设定
    • 改造第一步:Embedding 缓存先上
    • 改造第二步:Token 限流挡在 LLM 门前
    • 改造第三步:检索结果缓存带版本号
    • 改造第四步:FAQ 白名单全链路缓存
  • 05
  • 06
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档