“hug_face#2”是我在一个内部项目里给 AI 后端基准测试起的代号。起因很简单我们团队基于 Hugging Face 上的开源大模型搭了一个前后端分离的 AI 平台前端是 Vue后端是 Spring Boot模型推理服务单独部署在 GPU 机器上。功能还没全部上线老板就追问“能撑住多少人同时用、接口会不会卡”。我手里只有一堆模型推理的跑分数据却答不上业务层面的吞吐量于是就有了这个测试项目。如果你也在做类似的 AI 后端开发或者在为“RAG 服务”“AI Agent 接口”“大模型对话服务”做上线前的容量验证这篇就是我踩完坑后的实操总结。不会堆理论更多是“当时我怎么测、用什么测、测完发现什么、怎么改”。1. 基准测试到底测什么先把维度拆明白很多人在给 AI 后端做测试时第一反应是“拿脚本打一下接口看挂不挂”。这不算全错但容易漏掉关键点大模型服务的性能瓶颈通常不是单纯并发量而是“并发用户数、输入输出 token 长度、显存、排队策略”这几个因素叠加的结果。如果不分层理解测试跑完也看不懂数据。1.1 模型推理层与业务 API 层必须分开测我们当时的系统大概长这样Vue 前端发起请求Spring Boot 后端做用户鉴权、记录聊天历史然后把 Prompt 整理好转发给独立的模型推理服务推理服务返回流式结果后端再透传给前端。这中间有一条完整的链路但性能压力主要集中在两个节点模型推理层直接调用 Hugging Face 模型比如 llama、qwen 或者 embedding 模型。它接收 token计算生成结果是 GPU 消耗最大的地方。业务 API 层Spring Boot 这一侧的鉴权、限流、DB 读写、Redis 缓存、RAG 检索。虽然一般是 IO 密集但高并发下的线程池和数据库连接池也可能先打成瓶颈。测试时如果把两层混在一起测遇到“接口变慢”会很懵分不清到底是模型计算慢还是后端框架卡住或者是网络有了损耗。我建议先对模型推理服务单独做一次基准测试拿到一个“能力上限”再对业务 API 做整体测试分析损耗在哪里。比如模型服务每秒能处理 20 个请求但业务 API 只有 10 个那么慢的原因大概率不在 GPU而在后端处理环节。1.2 关键指标不只是“请求每秒多少次”对话类 AI 接口和普通 REST 接口差异很大。普通接口看的是响应时间、每秒请求数AI 生成接口还要考虑一个请求会输出多个 token消耗的算力是动态变化的。我的测试报告里至少包含这几项指标含义重要程度RPS / QPS每秒完成的请求数高TTFT (Time To First Token)从发送请求到收到第一个 token 的时间高影响用户体验TPOT (Time Per Output Token)每个输出 token 的平均生成耗时高直接决定生成速度Token 级别吞吐量每秒可生成的 token 总数中高计算资源效率P99 延迟99% 请求的响应时间高错误率 / 超时率请求失败占比高GPU 利用率 / 显存占用推理服务器的资源消耗中比如你测得模型服务 QPS 为 15但 P99 延迟是 6 秒TTFT 已超过 3 秒那这个结果往往意味着并发已经顶到上限用户已经明显觉得“转圈圈”。反过来如果 QPS 很高但 TPOT 很差那可能是批量凑得猛单个用户反而变慢。这些都是普通后端测试工具默认不关心的东西需要自己定义并打印。1.3 先定目标再启动压测没有目标的压测就像开车不看导航。我开始测试前会和业务方一起定几个数字预期同时在线人数、单用户每次会话的平均输入 token 长度、平均输出 token 长度、交互频率。举个例子假设聊天平台高峰期同时在线 2000 人每人每 10 秒发一条消息平均输入 500 token输出 1000 token那么预期 QPS 2000 / 10 200每个请求平均消耗的生成计算量 1000 token模型服务每分钟生成 token 数 200 * 1000 * 60 ≈ 1200 万 token如果单张 GPU 每秒只能生成大约 50 个 token这只是个粗糙假设那就需要 400 秒才能处理完一分钟的负载明显不够。这种粗算非常适合在跑压测之前做一轮“纸面评估”避免拿着不切实际的目标乱调参。2. 测试环境搭建与工具选型环境搭不好后面所有数据都没有参考价值。我用了两台机器一台装有 GPU 的服务器专门部署模型推理服务一台普通的云主机部署 Spring Boot 后端服务和 Nginx压测机单独用一台客户端机器避免压测工具本身消耗目标机资源。2.1 模型服务部署vLLM 是当前性价比很高的选择当时我在 Hugging Face 上选了一个 7B 参数的对话模型直接用 Transformers 跑过一版但并发一高就会发现显存占用太高、GPU 空闲多。后来切到 vLLM它用 PagedAttention 管理显存还能做 continuous batching同样是单卡吞吐量有明显提升。部署命令大概是这样python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --host 0.0.0.0 \ --port 8000 \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.85 \ --max-model-len 8192 \ --served-model-name qwen2.5-7b这里有几个关键参数--tensor-parallel-size 1如果是单卡就用 1多卡可以设成卡数vLLM 会把模型切分到多卡并行计算。--gpu-memory-utilization 0.85告诉 vLLM 最多用 85% 的显存剩余留给 CUDA context 和应急缓冲。设成 0.95 虽然能用更多显存但遇到请求波动容易直接 OOM。--max-model-len 8192最大上下文长度。这个值越大KV Cache 占显存越多并发能力会下降。要根据业务实际需要设置不要无脑拉满。如果不习惯 vLLM也可以用 Text Generation InferenceTGI或者 Ollama。TGI 和 vLLM 在功能和性能上各有千秋Ollama 适合快速验证但生产性能一般。我最后选择 vLLM 主要是它暴露了 OpenAI 兼容的/v1/chat/completions接口写压测脚本非常顺手而且和 Spring Boot 后端对接也很简单。2.2 业务后端用若依框架改造成本很低业务后端我直接基于 RuoYi若依的前后端分离版本改的。若依自带了用户鉴权、菜单权限、字典管理这些功能在做真实 AI 产品时基本都要用到。我在它的基础上加了几个模块对话管理接口负责记录聊天会话和消息文档知识库接口用于上传文档、切片、向量化模型调用模块通过 HTTP 调用 vLLM 的接口使用 Spring Boot 异步线程把流式输出返回给前端。它默认整合了 Spring Boot 和 Vue天然满足前后端分离的场景。但要注意若依默认的线程池和 Tomcat 配置偏保守后面压测时会直接打穿需要手动调整。2.3 压测工具与监控工具的选择压测工具我试过 Apache Benchmark、wrk、Locust、k6。简短对比工具优点缺点适用场景Apache Benchmark简单一条命令跑完无法模拟复杂场景、不支持流式响应分析快速验证接口连通性wrk性能高单机可打高并发脚本能力有限高并发纯 API 压测Locust用 Python 写脚本自定义程度高协程性能较 k6 稍弱复杂业务流模拟k6脚本采用 JS性能强断言和整理报告方便流式响应需要 WebSocket 或自定义处理长期压力测试最后我用 k6 做主压测工具。它支持比较复杂的场景编排、参数化、自定义指标而且它能在压测过程中实时输出延迟水位团队里其他人也能看懂。监控工具没走太重的方案。GPU 那一侧用nvidia-smi dmon -s pucmet -f dmon.csv持续记录 GPU 利用率、显存、温度。业务后端则接了 Prometheus Grafana 来做监控Spring Boot 暴露 micrometer 指标Nginx 记录请求日志再写入 Prometheus。这样跑完压测我能同时看到模型服务的 GPU 时间线、业务 API 的响应时间分布、系统负载变化方便定位瓶颈。3. 动手压测从零跑一份 AI 后端测试报告环境准备好后我按“单接口压测 - 混合场景压测 - 稳定性压测”的顺序推进。那样如果最后出了问题能知道是哪一步暴露的。3.1 编写 k6 压测脚本对话接口的请求体是 OpenAI 兼容格式vLLM 支持流式返回。但 k6 官方 HTTP API 默认收到的完整响应所以我在脚本里单独调用了流式接口并手动读取 chunk。这里给一个简化版方便你照搬import http from k6/http; import { check, sleep } from k6; import { Trend, Rate } from k6/metrics; const ttfTrend new Trend(ttft_ms, true); const tpotTrend new Trend(tpot_ms, true); const errorRate new Rate(request_error); const API_URL __ENV.API_URL || http://10.0.0.5:8080/api/chat/completions; const payload JSON.stringify({ messages: [ { role: user, content: 请给我写一段关于明天的计划安排包含上午、下午和晚上的具体事项约200字。 } ], stream: false, max_tokens: 512, temperature: 0.7 }); const params { headers: { Content-Type: application/json, Authorization: Bearer test-token } }; export const options { scenarios: { chat_load: { executor: constant-vus, vus: 10, duration: 2m, }, }, }; export default function () { const start Date.now(); const res http.post(API_URL, payload, params); const latency Date.now() - start; if (res.status ! 200) { errorRate.add(true); check(res, { status is 200: (r) r.status 200 }); return; } const body res.json(); const choices body body.choices body.choices[0]; if (!choices) { errorRate.add(true); return; } const usage body.usage || {}; const outputTokens usage.completion_tokens || 1; const inputTokens usage.prompt_tokens || 0; ttfTrend.add(0); // 非流式场景拿不到首个 token 时间这里只记录完整延迟 const tpot outputTokens 0 ? latency / outputTokens : 0; tpotTrend.add(tpot); check(res, { has output content: (r) (r.json(choices[0].message.content) || ).length 0, }); sleep(1); }注意上面的ttfTrend.add(0)只是示意。如果想测 TTFT需要调用stream: true接口并读第一个 chunk用 WebSocket 或自定义重定向接口的方式做这里为了演示方便没有展开。真实测试时我单独写了一个脚本攻击stream: true接口用INIT时间标记第一个 chunk 到达的时间。3.2 设计三种测试场景我分别做了三个场景场景一单用户低并发基准vus 从 1 开始每次增加 5持续 30 秒。这个场景用于找出“理想延迟”。比如单用户 P95 延迟是 800ms那么在后续测试中一旦 P95 超过 2 倍就说明并发已经造成明显排队。场景二稳定递增探索最大吞吐vus 从 10、20、30、50、80、120 逐级递加每一级跑 60 秒。观察 RPS 和错误率。当坡度之后 RPS 不再增长甚至下降就说明系统到了饱和点。场景三混合场景模拟真实业务模拟多类请求混合对话生成 70%、知识库检索 20%、向量化接口 10%。每条请求的 prompt 从一个池子里随机取池子里的文本长度按业务分布设置30% 短文本小于 200 token50% 中等文本500-900 token20% 长文本2000 token 以上。这种随机性才能暴露缓存和排队策略的问题。3.3 执行与记录压测脚本放到一个目录里用命令行跑k6 run chat.js --vus 20 --duration 5m --out csvresult_20vus.csv然后我每隔一段时间去看 nvidia-smi dmon 记录。跑完后把 k6 的 CSV 结果和 GPU dmon CSV 放进 Python 里做一些合并和透视分析。一个常见误区是只看 k6 的 Average RPS忽略“有多少请求被模型服务排队”。我习惯同时看两个图RPS 曲线和 GPU 利用率曲线。如果 GPU 利用率已经常驻 95% 以上而 RPS 上不去说明计算真的到顶如果 GPU 利用率只有 60%RPS 却平了那问题在业务后端或者网络继续加并发只会增加排队不会提升产出。4. 从测试结果定位瓶颈常见问题与排查实录这一节是我个人认为最有价值的部分。测试过程中遇到的坑非常多但我挑几个几乎人人都会碰到的。4.1 GPU 显存 OOM第一次压测没过两分钟vLLM 直接抛CUDA out of memory。原因很简单我设置了--max-model-len 16384并且--gpu-memory-utilization 0.9默认并发又是无限瞬间十几个长 prompt 请求进到显存占满。解决办法是治理并发和排队而不是换个更大的卡在 vLLM API 服务启动参数中调整--max-num-seqs限制同时生成的序列数比如 8。降低--max-model-len到业务可接受的长度例如 8192。给 Spring Boot 对接 vLLM 的调用方加信号量或线程池限制避免请求疯狂涌入。后端调用模型时设置超时和重试防止一个慢请求占着资源不放。改动后的压测结果从“并发 20 就挂”变成“并发 80 也能跑稳”显存占用稳定在 85% 以内。4.2 Spring Boot 线程池与数据库连接池被打满去掉 GPU OOM 后我又发现业务接口在并发 60 左右 RPS 不再上升反而开始大量超时。看 CPU 和 JVM 线程状态发现 Tomcat 的工作线程打满数据库连接池默认只有 10 条大量线程等待 DB 连接。针对若依这类基于 Spring Boot 的项目我调整了配置server: tomcat: threads: max: 200 min-spare: 20 accept-count: 200 max-connections: 10000 spring: datasource: hikari: maximum-pool-size: 40 minimum-idle: 10 connection-timeout: 30000但注意线程池和连接池并不是越大越好。队列太长会让用户等待很久反而不如快速拒绝。我结合测试数据最终把 Tomcat 最大线程设为 200、Hikari 连接池设为 40足够达到业务目标 200 QPS 且 P99 稳定在 1.5 秒内。4.3 Nginx 反向代理成了“隐形瓶颈”底下层的框架都通透了可整体压测时又发现延迟从模型服务的 500ms 变成前端感知的 2 秒。查了 Nginx 访问日志发现大量请求出现upstream timed out。原因是我只配置了默认的 worker 进程和 keepalive每个 worker 的并发连接能力有限再叠加流式响应接口代理等待时间特别长。Nginx 端我做了这些调整worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 4096; use epoll; } http { upstream model_api { server 10.0.0.2:8000; keepalive 32; } server { listen 80; proxy_http_version 1.1; proxy_set_header Connection ; proxy_read_timeout 300s; proxy_buffering off; } }worker_processes auto让 Nginx 跟随 CPU 核数。worker_connections 4096提高单 worker 并发连接数。proxy_http_version 1.1配合Connection 开启 upstream 长连接减少握手。proxy_buffering off对流式接口很重要否则 Nginx 会缓冲整个响应SSE 的流式效果会失效也会感觉响应特别慢。改完后业务 API 层测试的数据终于和模型服务层测试的数据能对上了。4.4 文件上传接口也不能漏测别让知识库上传拖垮全局我们项目里有知识库功能用户会通过网页上传 PDF、Word、TXT 到后端再进行切片和向量化。这类接口平时没人注意但一旦上传高峰期整个后端线程池都可能被上传操作占满。热词里面提到的“上传漏洞”和“正则限制了很多后缀”我在实际加固时也遇到类似背景。坦率说文件上传这个点是安全测试和性能测试的交界。我在基准测试里单独加了一个“上传 解析”的场景同时分享几个比较稳妥的加固手段这些也是我在失败中总结出来的在后端严格限制文件后缀和 MIME 类型用白名单而不是黑名单。比如只允许pdf,doc,docx,txt,md其余后缀一律拒绝。对上传大小做上限比如 20MB。大文件解析很吃 CPU 和内存容易造成 JVM GC 压力。如果服务器用的是 Apache就配置静态目录排除脚本执行权限如果用的是 Nginx可以通过location规则禁止上传目录解析 PHP、JSP 之类的脚本。这些属于服务器基础加固不能只依赖后端代码。上传文件最好落到独立存储服务或独立目录不要和前后端代码混在一起避免权限扩散。这些配置限制定稿后我再一次压测上传接口确认高并发下不会出现文件被重复写入、目录权限错误、连接池被占用等副作用。之后才敢让测试工资上传接口的性能。4.5 RAG 检索接口的隐藏延迟知识库场景里向量检索和 LLM 生成通常是一前一后串联的。我们用的 Embedding 接口是从 Hugging Face 上下载的 embedding 模型初版测试时发现 RAG 请求的延迟超过 8 秒主要原因是把 Embedding 和对话生成放在同一个 GPU 上两者抢占资源。后来把 Embedding 模型单独部署到一个 CPU 服务或用轻量级的 GPU 部署和对话模型分开延迟一下就降下来了。这个属于架构层面的拆分选择。基准测试要多做几次“组件拆分测试”观察每种组合的资源竞争情况不要一句话就决定一起部署。5. 压测完成后的一些优化思路跑完测试不能只留一份报告不能忘了根据数据做调整。我这里给几个通用的优化动作我们在项目里实际验证过有效。5.1 开启流式响应优化首字延迟感知如果业务允许尽量让后端直接透传 vLLM 的流式响应而不是等生成完才返回整个 JSON。我上线前做了一轮改造把接口改成 SSE前端逐字显示用户感知到的延迟瞬间从“等了 5 秒才出字”变成“0.5 秒开始出字”。这样即便 P95 完整响应时间不变TTFT 大幅下降用户体验会好很多。5.2 用 Redis 缓存重复 Prompt 的结果对话场景里有大量“相似问题”。我在业务后端加了 Redis 缓存层对完全相同的 prompt、temperature、max_tokens 组合的结果做短时间缓存。压测结果显示重复请求占比 30% 时同等 RPS 下 GPU 的负载降低了约 20%。不过要注意流式结果缓存起来要额外处理连接关系简单场景下可以先缓存最终完整结果再做一次 SSE 模拟发送。5.3 为模型服务增加排队和限流人怕出名接口怕挤。我在 Spring Boot 后端加了简单的令牌桶限流每个用户按等级分配每分钟的请求配额模型服务端 vLLM 的--max-num-seqs控制同时运行的序列数。多级限流的好处是即使前端被恶意刷量模型服务也不至于被打到 OOM。5.4 按测试数据制定扩缩容策略大模型推理部署不是越多 GPU 越好。根据压测数据我画了一条“最大 QPS vs GPU 数量”的曲线然后配合 Kubernetes 的 HPA 或手动扩容脚本让模型服务在高峰期自动横向扩展。对于单卡无法支撑的并发再考虑在 Spring Boot 层做多个 vLLM 实例的负载均衡。这样既保证稳定性也能控制成本。这些坑我都踩了一遍现在回头看基准测试最怕的不是测得慢而是不敢测、不知道测哪些维度。哪怕你暂时没有一个完整的“hug_face#2”项目环境也可以先把指标约定清楚、把工具链拉通等模型和业务接口就绪后直接套用。我个人最满意的一版测试脚本是一键输出 TTFB、TTFT、TPOT、RPS、错误率五合一报告的 Python 脚本但比脚本更重要的是你会不会分析这些数据背后的资源消耗和排队关系。希望这份经验能让你在给 AI 后端做基准测试时少走几步弯路。