第28章:RAGFlow 性能压测、容量规划与瓶颈定位
1 项目背景业务场景「云帆科技」的 CTO 在季度规划会上抛出一组数字“下季度公司要从 800 人扩到 2000 人还要把项目文档库也接进来——文档量从 500 份涨到 5000 份预计 Chunk 数从 10 万涨到 100 万。当前的部署能扛得住吗”运维小李没有答案——因为从来没做过压测。她不知道 API Server 单实例能承载多少并发聊天请求不知道 Task Executor 一天能解析多少份文档不知道 Infinity 在 100 万 Chunk 下的检索延迟是多少。更不知道整个系统的瓶颈在哪——是 CPU、内存、磁盘 IO、网络带宽、还是 LLM API 限流CTO 要求两周内完成一轮系统化压测输出容量规划报告明确在现有硬件下能撑多久、扩容需要加什么、加到多少。痛点没有压测和容量规划的后果扩容靠拍脑袋感觉慢了就加机器——不知道加 API Server 还是加 Worker也不知道加多少。瓶颈误判以为 CPU 是瓶颈加了 4 核后发现内存才是瓶颈——钱白花了。大促崩盘全员大会后 1000 人同时涌入问问题——系统直接 OOM没人知道并发上限。资源浪费为以防万一常年开着 4 个 Worker实际观察发现 1 个就够——50% 的资源浪费。压测揭示的典型现象 并发聊天用户数 1 10 50 100 200 P95延迟(ms) 800 1200 2500 8500 timeout LLM错误率(%) 0 0 2 15 45 Redis队列 0 3 12 85 320 结论系统在 50 并发内体验良好100 并发开始劣化200 并发崩溃 瓶颈LLM API 限流(100 req/s) Task Executor 单 Worker 积压2 项目设计小胖拿着计算器“大师CTO 说下季度要接 5000 份文档。我算了一下——每份文档平均 10 个 Chunk ÷ Worker 每小时解析 15 份 需要 333 小时…13 天这还没算问答的并发压力。你帮我算算到底要多少资源”大师“容量规划不是按文档数×时间的简单乘法——你要理解系统的吞吐瓶颈在哪。我们来拆 RAGFlow 的核心链路”RAGFlow 两大吞吐瓶颈链 1. 离线写入链路文档→解析→切片→Embedding→入库 瓶颈层: - Parser: 文本型 5页/秒, 扫描型 1页/10秒 - Embedding API: 通常 10-100 chunks/秒 - 文档引擎写入: Infinity ~5000 chunks/秒, ES ~2000 chunks/秒 → 最大瓶颈通常是 Embedding API 或 OCR(扫描件场景) 2. 在线查询链路问题→Embedding→检索→Rerank→LLM生成 瓶颈层: - Embedding: 50-200ms (本地) / 100-500ms (云端API) - 检索: 20-200ms (取决于索引大小) - Rerank: 500-3000ms (最大变数) - LLM生成: 1-10秒 (取决于模型和答案长度) → 最大瓶颈通常是 LLM API 限流 Rerank技术映射容量规划 高速公路设计——不是看一天能过多少辆车而是看早晚高峰每小时的通行能力。瓶颈就是最窄的那段路。小胖“那具体怎么压测用什么工具”大师“按分层递进的原则——先压组件、再压链路、最后压全链路”阶段压测对象工具目标L1 组件级LLM API 最大吞吐wrk/curl找到 API 限流阈值L1 组件级Infinity 查询延迟Python脚本不同数据量下的延迟曲线L2 链路级检索链路(无LLM)locust纯检索 QPS 上限L2 链路级解析链路批量上传脚本每小时解析吞吐L3 全链路端到端问答locust并发用户数与延迟关系小白“那瓶颈怎么定位跑完压测怎么知道问题出在哪里”大师“瓶颈定位四步法”步骤1观察现象 200 并发时 P95 延迟从 2s 飙升到 15s 步骤2分段计时 在代码中插入计时点将一次问答拆成 - 问题Embedding: 200ms - 检索: 450ms - Rerank: 1800ms ← 明显变慢 - LLM生成: 3200ms ← 也变慢 - 总计: 5650ms 步骤3看资源使用率 - CPU: 45% (不是瓶颈) - 内存: 72% (正常) - LLM API剩余额度: 5% (几乎耗尽) → 根因: LLM API 接近限流上限 步骤4验证假说 增加 LLM API 额度 → 重跑压测 → 延迟恢复正常 假说成立瓶颈是 LLM API 限流技术映射瓶颈定位 水管工检查堵点——不是看水龙头出水多大而是要一段段摸水管分段计时找到最窄的那一段瓶颈换粗管扩容/升配。3 项目实战环境准备目标用 locust 压测 RAGFlow 问答 API找出并发上限。# 安装压测工具pipinstalllocust分步实现步骤1编写问答压测脚本目标模拟真实用户提问行为。# locustfile.py - RAGFlow 问答压测fromlocustimportHttpUser,task,betweenimportrandom# 模拟真实用户问题池QUESTIONS[年假有几天,加班费怎么计算,出差住宿费标准是多少,试用期离职流程是什么,社保公积金缴纳比例,带薪休假多少天,P3职级的岗位津贴是多少,合同到期不续签怎么赔偿,远程办公需要什么条件,报销需要提供哪些材料,迟到一次罚多少钱,哺乳期有什么特殊政策,公司年会有哪些活动,如何申请调休,出差交通补助标准,员工体检如何预约,商业保险覆盖哪些内容,绩效奖金发放时间,离职需要提前多久通知,会议室如何预定,]classRAGFlowChatUser(HttpUser):模拟普通用户问答行为wait_timebetween(5,15)# 用户思考间隔 5-15 秒hosthttp://localhostdefon_start(self):登录获取 Tokenrespself.client.post(/api/v1/login,json{email:testyunfan.com,password:TestPass2024})self.tokenresp.json()[data][access_token]taskdefask_question(self):模拟一次问答请求questionrandom.choice(QUESTIONS)respself.client.post(/api/v1/chats/test_chat_id/sessions/test_session_id/messages,headers{Authorization:fBearer{self.token}},json{question:question,stream:False,},timeout30,namechat_question)# locust 会自动记录响应时间和成功率# 启动 locustlocust-flocustfile.py --web-port8089# 或命令行模式无 UIlocust-flocustfile.py\--headless\--users200\--spawn-rate10\--run-time 5m\--htmlreport.html步骤2分段计时诊断目标在代码中加入计时埋点精确定位慢点。# latency_profiler.py - 分段计时importtimefromcontextlibimportcontextmanagerclassLatencyProfiler:RAG 链路分段计时器def__init__(self):self.segments{}contextmanagerdefmeasure(self,name):starttime.time()yieldelapsed(time.time()-start)*1000self.segments.setdefault(name,[]).append(elapsed)defreport(self):print(\n 分段耗时报告 )forname,timesinself.segments.items():times.sort()p50times[len(times)//2]p95times[int(len(times)*0.95)]iflen(times)1elsep50 p99times[int(len(times)*0.99)]iflen(times)1elsep50 avgsum(times)/len(times)print(f{name:25}avg{avg:6.0f}ms P50{p50:6.0f}ms P95{p95:6.0f}ms P99{p99:6.0f}ms (n{len(times)}))# 使用示例profilerLatencyProfiler()contextmanagerdeftraced_chat(question,dataset_ids):withprofiler.measure(1.Question Embedding):query_vectorembed(question)withprofiler.measure(2.Vector Search):vector_resultsvector_search(query_vector,dataset_ids)withprofiler.measure(3.Keyword Search):keyword_resultsbm25_search(question,dataset_ids)withprofiler.measure(4.Fusion):fusedrrf_fusion(vector_results,keyword_results)withprofiler.measure(5.Rerank):rerankedrerank(fused)withprofiler.measure(6.LLM Generation):answerllm_generate(reranked,question)returnanswer# 跑完 1000 次请求后查看报告profiler.report()预期输出 分段耗时报告 1.Question Embedding avg 180ms P50 170ms P95 350ms P99 520ms (n1000) 2.Vector Search avg 85ms P50 70ms P95 180ms P99 320ms (n1000) 3.Keyword Search avg 35ms P50 28ms P95 80ms P99 150ms (n1000) 4.Fusion avg 3ms P50 2ms P95 8ms P99 15ms (n1000) 5.Rerank avg 1200ms P50 1050ms P95 2800ms P99 4500ms (n1000) ← 瓶颈 6.LLM Generation avg 2800ms P50 2200ms P95 6500ms P99 12000ms (n1000) ← 瓶颈步骤3容量规划计算器目标根据压测结果推算未来容量需求。# capacity_planner.pyclassCapacityPlanner:RAGFlow 容量规划计算器def__init__(self,benchmark_results): benchmark_results: 压测结果 { chat_qps_per_instance: 5, # 单 API Server QPS parse_docs_per_hour_per_worker: 15, # 单 Worker 每小时解析文档数 llm_api_qps_limit: 100, # LLM API QPS 上限 embedding_api_qps_limit: 200, infinity_p95_latency_ms: {10万: 50, 50万: 120, 100万: 250}, } self.benchbenchmark_resultsdefplan_for_scale(self,target_users,target_docs,avg_questions_per_user_per_day5):根据目标规模计算所需资源# 1. 问答容量peak_qpstarget_users*avg_questions_per_user_per_day/(8*3600)*3# 峰值 3× 均值api_instances_neededmax(1,int(peak_qps/self.bench[chat_qps_per_instance]0.5))# 2. 文档解析容量parse_hourstarget_docs/self.bench[parse_docs_per_hour_per_worker]workers_neededmax(1,int(target_docs/1000)1)# 每 1000 份文档 1 个 Worker# 3. LLM API 额度api_qps_neededpeak_qps*1.2# 留 20% 余量need_upgradeapi_qps_neededself.bench[llm_api_qps_limit]# 4. 文档引擎容量estimated_chunkstarget_docs*100# 每份文档约 100 个 Chunkifestimated_chunks100000:engine_typeInfinity 单实例elifestimated_chunks500000:engine_typeInfinity 单实例建议 2 副本else:engine_typeES 集群 (3 节点)# 5. 内存估算memory_gb(api_instances_needed*2# API Serverworkers_needed*4# Task Executor (含本地Embedding)8# MySQL Redis MinIO(8ifengine_type.startswith(ES)else2)# 文档引擎)return{峰值 QPS:round(peak_qps,1),API Server 实例数:api_instances_needed,Task Executor Worker 数:workers_needed,推荐文档引擎:engine_type,LLM API 需升级:need_upgrade,估算总内存(GB):memory_gb,估算解析总耗时(小时):round(parse_hours,1),}# 使用plannerCapacityPlanner({chat_qps_per_instance:5,parse_docs_per_hour_per_worker:15,llm_api_qps_limit:100,})planplanner.plan_for_scale(target_users2000,target_docs5000)fork,vinplan.items():print(f{k}:{v})步骤4瓶颈定位实战目标运行 200 并发压测根据分段计时报告和执行监控定位瓶颈。# 终端1运行压测locust-flocustfile.py--headless--users200--spawn-rate10--run-time 10m# 终端2监控资源每个组件独立监控watch-n2docker stats --no-stream --format table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}# 终端3监控 Redis 队列watch-n3docker exec ragflow-redis redis-cli XLEN ragflow_tasks# 终端4监控 LLM API 限流# 记录每次 LLM 调用的 HTTP 响应头 X-RateLimit-Remainingtail-f/var/log/ragflow/llm_ratelimit.log# 瓶颈分析报告defanalyze_bottleneck(profiler_report,resource_metrics):根据分段耗时 资源使用率判断瓶颈# 规则1Rerank 占比 40% → Rerank 是瓶颈ifprofiler_report[Rerank][p95]/profiler_report[total][p95]0.4:return{bottleneck:Rerank,recommendation:1. 启用GPU加速 2. 减少top_k 3. 换轻量Reranker}# 规则2LLM P95 8s 且资源充足 → LLM API 限流if(profiler_report[LLM Generation][p95]8000andresource_metrics[cpu]70andresource_metrics[memory]80):return{bottleneck:LLM API 限流,recommendation:1. 升级 API 套餐 2. 增加 API 并发额度 3. 切换到更快模型}# 规则3CPU 85% 且内存正常 → CPU 瓶颈ifresource_metrics[cpu]85andresource_metrics[memory]80:return{bottleneck:CPU,recommendation:1. 扩容 API Server 2. 增加 Worker 数 3. 优化代码}# 规则4队列积压 100 且 Worker 正常 → 解析吞吐瓶颈ifresource_metrics[redis_queue]100:return{bottleneck:Task Executor 吞吐不足,recommendation:1. 增加 Worker 数(WS) 2. 优化 Embedding API 并发}return{bottleneck:无明显瓶颈,recommendation:继续监控}步骤5输出容量规划报告目标生成向 CTO 汇报的容量规划文档。catcapacity_plan_report.mdEOF # RAGFlow 容量规划报告 ## 现状 - 用户数: 800 - 文档数: 500 - Chunk数: ~5万 - API Server: 1 实例 - Task Executor: 1 Worker - 文档引擎: Infinity 单实例 ## 压测结果摘要 | 指标 | 100并发 | 200并发 | 300并发 | |------|--------|--------|--------| | P50延迟 | 1.8s | 3.2s | 8.5s | | P95延迟 | 4.2s | 12s | timeout | | 成功率 | 99.5% | 92% | 68% | | 峰值QPS | 8 | 14 | 22 | ## 瓶颈分析 1. LLM API P95 延迟 6.5s占全链路 55% 2. Rerank P95 延迟 2.8s占全链路 24% 3. Task Executor 单 Worker 每天解析上限约 120 份文档 ## 扩容方案支持 2000 用户 5000 文档 | 组件 | 当前 | 目标 | 成本 | |------|------|------|------| | API Server | 1 | 3 | 0 (开源) | | Task Executor Worker | 1 | 3 | 0 (开源) | | LLM API 额度 | 100 QPS | 300 QPS | ~$500/月 | | 内存 | 16GB | 32GB | ~$50/月(云服务器) | | 文档引擎 | Infinity | Infinity (单实例) | 0 | EOF测试验证# test_perf.pyclassTestPerformance:deftest_chat_concurrent_50(self):验证 50 并发下成功率 99%resultsrun_locust_test(users50,duration2m)assertresults[success_rate]0.99deftest_p95_latency_under_5s_50concurrent(self):验证 50 并发下 P95 5sresultsrun_locust_test(users50,duration2m)assertresults[p95_latency_ms]5000deftest_no_queue_backlog_after_rest(self):验证空闲时队列长度归零time.sleep(60)# 等待积压任务处理完毕queue_lenredis.xlen(ragflow_tasks)assertqueue_len5完整代码清单路径说明column/chapter28/locustfile.py问答压测脚本column/chapter28/latency_profiler.py分段计时工具column/chapter28/capacity_planner.py容量规划计算器4 项目总结优点 缺点维度locust 分段计时k6wrk/ab商业 APM学习成本★★★ Python 易上手★★☆ JavaScript★★★ 命令行★★☆ 需要学习脚本灵活性★★★ 完全可定制★★☆ JS 脚本★☆☆ 参数化有限★★☆ 受平台限制分段计时★★★ 自定义埋点★★☆ 需配合插件★☆☆ 无★★★ 自动报告可视化★★☆ 基础 HTML★★★ 丰富★☆☆ 纯文本★★★ 企业级成本★★★ 免费★★★ 免费★★★ 免费★★☆ 按量/订阅适用场景上线前压测新版本或新规模目标发布前必做一轮全链路压测。容量规划根据压测结果推算未来 3/6/12 个月的资源需求。瓶颈定位通过分段计时快速定位慢点而不是盲目扩容。回归压测每次重大架构变更后跑一次基准压测确认无退化。LLM 成本优化通过压测找到最优的并发数——既不过度限流也不浪费 API 额度。不适用场景极小规模 50 人在用、 100 份文档——压测的收益小于成本。频繁变更的早期项目架构和代码每周大改——压测结果有效期只有几天。注意事项压测环境要与生产一致在开发机上跑压测测出来的数字不能代表生产——硬件、网络、LLM API 配额都不同。不要压测生产环境在生产上跑 200 并发压测 人为制造 DDoS。要么用独立压测环境要么在业务低峰期小流量压测。LLM API 费用一次 200 并发 10 分钟的压测可能调用 5000 次 LLM API费用可能在 $10-50。提前做准备。预热Warm-up压测前先跑 2 分钟低流量预热——否则第一个用户因冷启动被计入 P99 会造成数据偏差。分段计时的性能开销加入计时代码本身有微小开销 1ms但足以影响 P99 数据。生产环境建议采样而非全量。常见踩坑经验故障现象根因解决方法压测结果忽高忽低不稳定样本量太小或 LLM 服务在别处也有负载加长压测时间10分钟多次取中位数P99 极高但 P50 正常长尾延迟——少数请求触发了冷加载或 GC检查是否有资源争抢预热是否充分压到一半 LLM API 全 429触发 API 限流降低并发或提前申请提高限流额度locust 本身 CPU 100%locust 进程本身成了瓶颈用 locust --master --worker 分布式压测Infinity 在压测后查询变慢索引碎片累积压测后触发 optimize 操作思考题当前的压测只覆盖了均速到达的场景——每秒均匀增加用户。但真实场景有突发流量全员大会后 1000 人同时涌入。请设计一个阶梯式 脉冲式混合压测方案分别评估系统的稳态容量和突发抗冲击能力。压测发现 LLM API 是瓶颈但你发现不同的 LLM 模型延迟差异巨大GPT-4o: 3200ms, GPT-4o-mini: 800ms, DeepSeek: 450ms。如果预算不变如何在三种模型间划分流量比例使得在总预算内整体 P95 延迟最小请建立数学模型求解。答案提示见第29章末尾或附录 D。延伸阅读与资源10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析