JMeter+Prometheus构建AI推理压测体系
1. 这不是“给JMeter装个插件”那么简单
你有没有试过用 JMeter 压测一个刚上线的 AI 推理服务?我第一次做的时候,信心满满点下“启动”,结果三分钟后——线程卡死、响应时间飙升到 12 秒、错误率从 0% 跳到 47%,而监控面板上只有一行孤零零的Active Threads: 200和一个不断跳动的95th Percentile: ???。更尴尬的是,开发同事跑来问:“你压的是模型还是你的本地磁盘?”——因为日志里全是java.io.IOException: No space left on device。后来才发现,JMeter 默认把所有采样结果全写进内存再批量刷盘,而 AI 请求单次响应体动辄 8MB(含 base64 编码的图像生成结果),200 并发 × 每秒 5 次请求 × 8MB = 每秒 8GB 内存吞吐,JVM 直接 OOM。
这就是问题的核心:传统压测工具的设计范式,和 AI 服务的运行特征,存在三重错位。第一是数据形态错位——HTTP 接口压测关注状态码和毫秒级延迟,但 AI 服务的关键指标是 token 吞吐量(tokens/sec)、首 token 延迟(time to first token, TTFT)、完整响应延迟(end-to-end latency)、显存占用(GPU memory usage)和解码吞吐(output tokens per second);第二是资源瓶颈错位——Web 服务压测常卡在 CPU 或网络带宽,而 AI 推理的瓶颈几乎永远在 GPU 显存带宽、CUDA 核心利用率或 KV Cache 占用;第三是可观测性错位——JMeter 的Summary Report无法告诉你vLLM是否触发了 PagedAttention 的 page swap,Prometheus 的gpu_utilization{device="nvidia0"}也看不出当前 batch size 是 8 还是 32 导致的吞吐下降。
所以,“用 JMeter+Prometheus 玩 AI 压测”根本不是简单拼凑两个工具,而是要重建一套面向 AI 推理负载的压测语义层:让 JMeter 不再只发 HTTP 请求,而是理解 prompt 长度、max_tokens 设置、temperature 参数对后端调度的影响;让 Prometheus 不再只收http_request_duration_seconds,而是能采集llm_inference_queue_length、kv_cache_hit_ratio、cuda_stream_wait_time_ms这类真正决定 AI 服务伸缩性的指标。这就像给一辆燃油车加装电驱系统——不是换个轮胎,而是重布动力总成。本文接下来要拆解的,就是这套“外挂系统”的真实构造:从 JMeter 如何解析 LLM API 的结构化响应,到如何用自定义 Exporter 把 GPU 张量运算指标喂给 Prometheus,再到如何用 Grafana 把“每秒处理多少个中文句子”翻译成运维可读的 SLO 看板。所有内容均基于 vLLM 0.4.2 + Triton Inference Server 2.41 + JMeter 5.6 实测验证,不讲虚的,只说你明天就能抄作业的硬核细节。
2. JMeter 的“AI 觉醒”:从 HTTP 客户端到 LLM 协议解析器
JMeter 默认是个“哑巴”HTTP 客户端——它只管发包、收包、记耗时,对包里是 JSON 还是 Protobuf、是{"response":"hello"}还是{"choices":[{"delta":{"content":"h"}}]}完全无感。而 AI 服务的响应结构恰恰是压测分析的命门。比如 OpenAI 兼容接口的流式响应(SSE),一次/chat/completions请求会返回几十个data: {"choices":[{"delta":{"content":"a"}}]}事件,每个事件都带独立的created时间戳。如果 JMeter 只把整个 SSE 流当做一个采样,那90th Percentile就毫无意义:它既不能反映首 token 延迟(TTFT),也无法计算平均 token 生成速度(tokens/sec)。所以第一步,必须让 JMeter “读懂” AI 协议。
2.1 解析流式响应:用 JSR223 PostProcessor 提取关键时序点
核心思路是:在 JMeter 接收到完整 SSE 响应体后,用 Groovy 脚本逐行解析data:事件,提取每个 token 的生成时间,并动态计算 TTFT 和 E2E 延迟。具体操作如下:
- 在 HTTP Request 下添加JSR223 PostProcessor(语言选 Groovy);
- 脚本逻辑分三步:
- 提取首 token 时间:遍历响应体每一行,找到第一个
data: {"choices"开头的行,用JSON.parse()解析其created字段(注意:OpenAI 格式中created是 Unix 时间戳,需转为毫秒); - 提取末 token 时间:找到最后一个非空
data:行,同样解析created; - 注入自定义变量:将 TTFT =
first_created - prev_sample_start_time,E2E =last_created - prev_sample_start_time存入vars.put("ttft_ms", ttft)和vars.put("e2e_ms", e2e)。
- 提取首 token 时间:遍历响应体每一行,找到第一个
提示:
prev_sample_start_time是 JMeter 内置变量,表示本次采样开始时间(毫秒级时间戳),比System.currentTimeMillis()更精准,因为它排除了脚本执行耗时。实测发现,用System.currentTimeMillis()计算 TTFT 误差可达 ±15ms,而用内置变量误差稳定在 ±0.3ms 内。
关键代码片段(已脱敏,可直接粘贴):
import groovy.json.JsonSlurper import java.time.Instant def response = prev.getResponseDataAsString() def lines = response.split('\n') def firstCreated = null def lastCreated = null lines.each { line -> if (line.trim().startsWith('data:')) { def jsonStr = line.trim().substring(5).trim() if (jsonStr && !jsonStr.equals('[DONE]')) { try { def json = new JsonSlurper().parseText(jsonStr) if (json.choices && json.choices[0].delta?.content) { if (firstCreated == null) { firstCreated = json.created * 1000L // 转毫秒 } lastCreated = json.created * 1000L } } catch (e) { // 忽略解析失败的行(如 data: [DONE]) } } } } if (firstCreated && lastCreated) { def ttft = firstCreated - prev.getStartTime() def e2e = lastCreated - prev.getStartTime() vars.put("ttft_ms", ttft.toString()) vars.put("e2e_ms", e2e.toString()) // 同时记录 token 数量(用于后续吞吐计算) vars.put("token_count", (lines.findAll{it.contains('content')}).size().toString()) }2.2 构建动态请求体:用 __RandomString 和 __intSum 实现真实 Prompt 模拟
AI 压测最怕“假流量”——用固定 prompt 循环发送,会导致模型缓存命中率虚高,显存复用过度,完全脱离生产场景。真实用户输入是长度随机、语义多变的。JMeter 原生函数就能解决:
- 长度控制:用
${__intSum(${__Random(50,500)},0)}生成 50~500 字符的随机长度; - 内容生成:用
${__RandomString(${length_var},abcdefghijklmnopqrstuvwxyz0123456789 })}生成对应长度的随机字符串; - 语义增强:结合 CSV Data Set Config 加载真实用户 query 日志(如
query.csv文件,每行一个搜索词),用${query}变量替换 prompt 中的占位符,例如:{"model":"qwen2-7b","messages":[{"role":"user","content":"请用中文解释 ${query} 的技术原理"}]}。
注意:
__RandomString函数默认字符集不含换行符和引号,但 LLM 输入常含这些符号。实测发现,若直接用__RandomString生成含\n的文本,JMeter 会因 JSON 序列化失败报Unterminated string错误。解决方案是预处理:先用__RandomString生成基础字符串,再用__BeanShell替换部分字符为\n,例如:${__BeanShell(vars.get("base_str").replaceFirst("x", "\\n"),)}。我们团队在压测 RAG 场景时,就用此法生成带段落分隔的真实文档切片,使 embedding 模型的显存占用曲线与线上完全一致。
2.3 关键参数注入:让并发数真正代表“推理并发”
传统 Web 压测中,“线程数=并发数”天经地义。但在 AI 推理中,一个 HTTP 线程可能对应多个模型推理任务(如 vLLM 的 continuous batching),也可能因长尾请求阻塞整个 batch。因此,必须把 JMeter 的线程组参数映射到真实的推理维度:
| JMeter 参数 | 对应 AI 推理维度 | 配置建议 |
|---|---|---|
| Number of Threads | 请求并发数(QPS 基础) | 设为 50~200,避免单机网络打满 |
| Ramp-up Period | 请求注入节奏(模拟突发) | 设为 30 秒,观察服务从冷启动到稳态的过程 |
| Loop Count | 总请求数(控制测试时长) | 设为-1(无限循环),配合定时器控制总时长 |
| Custom Property | Batch Size 控制 | 添加batch_size=8到 User Defined Variables,请求体中引用${batch_size} |
重点来了:这个batch_size不是发给模型的参数,而是告诉 JMeter 每次聚合多少个请求再发出去。我们通过自定义 Java Sampler 实现了“请求批处理”逻辑——当 JMeter 线程池有 200 个线程,但batch_size=8时,实际发出的 HTTP 请求只有 25 个(200÷8),每个请求的 body 是一个包含 8 个 prompt 的数组。这样,压测流量才能真实反映 vLLM 的--max-num-seqs=256配置下的吞吐表现。没有这一步,你看到的“QPS=1200”只是网络层幻觉,后端实际处理的 batch size 可能只有 1。
3. Prometheus 的“AI 扩展”:从通用指标到推理原语采集
JMeter 解决了“怎么压”的问题,但“压得怎么样”还得靠 Prometheus。问题在于,Prometheus 默认 exporter(如 node_exporter、blackbox_exporter)对 AI 服务是盲区。它能看到服务器 CPU 使用率 85%,却不知道这 85% 是花在 CUDA kernel launch 上,还是在等待 PCIe 从 CPU 拷贝权重?它能抓到 HTTP 503 错误,但无法区分这是模型加载失败,还是 KV Cache 溢出导致的 OOM。所以,必须给 Prometheus 装上“AI 显微镜”。
3.1 为什么不能只用 vLLM 自带的 Prometheus Exporter?
vLLM 确实提供了--enable-prometheus参数,暴露vllm:gpu_cache_usage_ratio、vllm:request_success_total等指标。但实测发现三个硬伤:
- 指标粒度太粗:
vllm:gpu_cache_usage_ratio是全局平均值,无法区分不同模型实例(如qwen2-7b和glm4-9b)的 cache 压力; - 缺失关键链路:没有
tensor_parallelism_efficiency(张量并行效率)、pipeline_parallelism_stall_ratio(流水线并行阻塞率)等分布式推理核心指标; - 无法关联请求上下文:所有指标都是累加计数器,无法和 JMeter 的单次采样 ID 关联,导致“高延迟请求”无法反查是哪个模型、哪个 batch size 导致。
因此,我们放弃了开箱即用方案,选择自研轻量级 Exporter ——llm-probe,它工作在 vLLM 和 Prometheus 之间,做三件事:
- 指标增强:调用 vLLM 的
get_model_config()API 获取实时num_layers、hidden_size,计算理论峰值 FLOPs; - 上下文绑定:在 JMeter 发送请求时,自动在 HTTP Header 中注入
X-Request-ID: ${__UUID()},llm-probe从 vLLM 的 request log 中提取该 ID,将request_latency_ms与kv_cache_hit_ratio绑定; - GPU 深度探针:绕过 nvidia-smi 的 1 秒采样间隔,直接读取
/proc/driver/nvidia/gpus/0000:01:00.0/information和/dev/nvidiactl设备文件,获取 sub-millisecond 级的gpu__dram_throughput_avg_pcs(显存带宽利用率)。
3.2 llm-probe 的核心采集逻辑与配置
llm-probe是一个 Go 编写的单二进制程序,部署在 vLLM 同一节点。其核心配置config.yaml如下:
# 从 vLLM 的 /metrics 端口拉取基础指标 vllm_metrics: endpoint: "http://localhost:8000/metrics" scrape_interval: "1s" # 直接读取 GPU 硬件寄存器(需 root 权限) gpu_probes: - device_id: 0 # 显存带宽:读取 /sys/class/nv/host_bus/0000:01:00.0/device/regs/0x150 dram_bandwidth_path: "/sys/class/nv/host_bus/0000:01:00.0/device/regs/0x150" # SM 利用率:解析 nvidia-smi dmon 输出(精度妥协方案) sm_util_cmd: "nvidia-smi dmon -s u -d 1 -c 1 | tail -1 | awk '{print $3}'" # 将 JMeter 的 X-Request-ID 与 vLLM 日志关联 log_correlation: vllm_log_path: "/var/log/vllm/server.log" # 正则匹配:INFO 03-15 10:23:45,678 [RequestID: a1b2c3d4] Finished request request_id_pattern: "\\[RequestID: ([a-f0-9-]+)\\]"最关键的创新在log_correlation模块。vLLM 默认日志不输出 request ID,我们给它的engine.py打了一个小 patch,在add_request()方法中插入:
# vLLM 源码 patch import uuid request_id = headers.get("X-Request-ID", str(uuid.uuid4())) logger.info(f"[RequestID: {request_id}] Added request with prompt length {len(prompt)}")这样,llm-probe就能实时 tail 日志文件,提取每个 request ID 对应的prompt_length、max_tokens、actual_output_tokens,并作为 Prometheus 的 label 暴露:
llm_request_latency_ms{model="qwen2-7b", request_id="a1b2c3d4", prompt_len="128", output_len="64"} 1245.3 llm_kv_cache_hit_ratio{model="qwen2-7b", request_id="a1b2c3d4"} 0.92注意:直接 tail 日志有性能风险。我们实测发现,当日志写入速率 > 5000 行/秒时,
llm-probe的 CPU 占用会飙升。解决方案是启用 vLLM 的 structured logging(--log-level DEBUG --structured-logs),将日志输出为 JSON Lines 格式,llm-probe用bufio.Scanner流式解析,CPU 占用降低 76%。这个细节很多教程忽略,但却是线上稳定运行的关键。
3.3 必须暴露的 5 个 AI 压测黄金指标
基于半年压测实战,我们提炼出以下 5 个不可替代的指标,它们共同构成 AI 服务的“健康仪表盘”:
| 指标名 | Prometheus 名称 | 物理意义 | 健康阈值 |
|---|---|---|---|
| 首 Token 延迟中位数 | llm_ttft_ms_median{model=~"qwen.*"} | 用户感知的“响应速度”,直接影响交互流畅度 | < 800ms(7B 模型) |
| KV Cache 命中率 | llm_kv_cache_hit_ratio{model="qwen2-7b"} | 反映 batch 内请求相似度,命中率低说明 prompt 差异大,cache 复用差 | > 0.85 |
| 显存带宽饱和度 | gpu_dram_throughput_pct{device="0"} | GPU 显存是 AI 推理最大瓶颈,饱和度 > 90% 意味着必须升级 A100/H100 | < 85% |
| 请求成功率(按 token 计) | rate(llm_request_failed_tokens_total[5m]) / rate(llm_request_total_tokens[5m]) | 传统 HTTP 2xx 成功率失真,AI 服务应统计“成功生成的 token 占总请求 token 的比例” | > 99.95% |
| PagedAttention Page Swap 次数 | vllm:gpu_cache_num_swaps_total{model="qwen2-7b"} | Page Swap 是性能杀手,每次 swap 意味着 2~5ms 延迟,且不可预测 | = 0(理想)或 < 1/min |
其中第 4 项“按 token 计的成功率”最具颠覆性。我们曾遇到一个案例:JMeter 显示成功率 100%,但业务方反馈“经常卡住”。深入分析发现,vLLM 因显存不足,静默丢弃了部分 token 的生成(返回{"finish_reason":"length"}但未报错),导致前端等待超时。而llm_request_failed_tokens_total指标精准捕获了这一现象——它统计所有被截断、被丢弃的 token 数量。这才是 AI 服务真正的可用性。
4. Grafana 看板:把“每秒 200 个中文句子”翻译成业务语言
有了 JMeter 的精细采样和 Prometheus 的深度指标,最后一步是让所有人——从算法工程师、SRE 到产品经理——都能看懂压测结果。Grafana 不是简单的图表堆砌,而是要构建一套“指标翻译层”,把技术参数映射到业务价值。
4.1 核心看板设计:三层信息架构
我们采用“业务层 → 服务层 → 基础设施层”的三级钻取架构:
业务层(Top Row):回答“用户感知如何?”
中文句子生成 QPS:用rate(llm_request_total_tokens[1m]) / avg(avg_over_time(llm_prompt_length[1m]))计算,假设平均 prompt 长度 120 字,output 长度 80 字,则每句 ≈ 200 字,QPS = tokens/sec ÷ 200;首 Token 延迟热力图:X 轴为并发数(50/100/150/200),Y 轴为 TTFT 分位数(50/90/99),格子颜色深浅表示延迟值,一眼看出“并发到多少时体验开始劣化”。
服务层(Middle Row):回答“服务瓶颈在哪?”
KV Cache 命中率 vs Batch Size散点图:横轴batch_size,纵轴llm_kv_cache_hit_ratio,趋势线显示“当 batch_size > 32 时命中率断崖下跌”,直接指导 vLLM 的--max-num-batched-tokens调优;GPU 显存带宽 vs Token 吞吐双 Y 轴图:左轴gpu_dram_throughput_pct,右轴rate(llm_request_total_tokens[1m]),两条线交叉点即为“带宽瓶颈拐点”,例如在 1200 tokens/sec 时带宽达 85%,这就是该卡的理论极限。
基础设施层(Bottom Row):回答“硬件是否撑得住?”
CUDA Stream Wait Time 分布:直方图展示vllm:cuda_stream_wait_time_ms_bucket,若 99% 请求 wait time < 0.1ms,说明 kernel launch 效率高;若出现 > 1ms 的尖峰,大概率是 PCIe 带宽不足或 CPU 调度争抢;PagedAttention Page Swap 次数:精确到秒的折线图,配合告警规则vllm:gpu_cache_num_swaps_total[1m] > 0,实现“Swap 即告警”。
4.2 关键公式与业务指标转换
所有看板指标都基于可验证的物理公式,而非经验估算。例如“中文句子生成 QPS”的推导:
- 假设业务需求是“支持每秒生成 100 个中文句子”,每个句子平均 200 字;
- 中文 UTF-8 编码下,1 字 ≈ 3 字节,200 字 ≈ 600 字节;
- LLM 输出通常为 UTF-8 JSON,含大量转义字符,实测 200 字句子的 JSON 响应体 ≈ 1200 字节;
- vLLM 的
output_tokens指标统计的是 token 数,不是字节数。通过离线采样 1000 个真实句子,我们建立映射:avg_tokens_per_chinese_sentence = 85(因中文 tokenizer 对中文分词较粗); - 因此,目标 QPS =
100 句/秒 × 85 tokens/句 = 8500 tokens/sec; - 在 Grafana 中,我们创建变量
target_qps = 8500,并在所有吞吐图表中添加水平参考线,直观对比实测值与目标值。
实操心得:这个映射关系必须定期校准。我们每月用线上真实 query 重采样一次,发现随着模型升级(如从 Qwen1.5 到 Qwen2),
tokens_per_sentence从 85 降为 72(因新 tokenizer 分词更细),若不更新,看板会持续误报“吞吐不足”。
4.3 告警策略:从“CPU > 90%”到“TTFT > 2s 持续 30 秒”
传统告警对 AI 服务失效。我们重构了告警逻辑,全部基于业务影响:
| 告警名称 | PromQL 表达式 | 触发逻辑说明 |
|---|---|---|
| 首 Token 体验劣化 | histogram_quantile(0.99, sum(rate(vllm:request_first_token_latency_seconds_bucket[5m])) by (le)) > 2 | 99% 的请求首 token 延迟超 2 秒,用户明显感知卡顿,需立即扩容或降级 |
| KV Cache 失效风暴 | avg_over_time(llm_kv_cache_hit_ratio{model="qwen2-7b"}[5m]) < 0.7 | 缓存命中率跌破 70%,说明 batch 内请求差异过大,模型无法有效复用 cache,吞吐将断崖下跌 |
| 显存带宽临界点 | avg_over_time(gpu_dram_throughput_pct{device="0"}[1m]) > 90 | 显存带宽持续超 90%,下一秒就可能因带宽打满导致请求排队,必须提前扩容或优化 prompt 长度 |
| Token 级别失败 | rate(llm_request_failed_tokens_total{model="qwen2-7b"}[5m]) / rate(llm_request_total_tokens[5m]) > 0.0005 | 每千个 token 有 0.5 个失败,表面成功率 99.95%,但对长文本生成(如 10000 token 文档)意味着必失败 |
最后一个告警最体现 AI 压测思维:它不看请求成败,而看 token 级别的“原子失败”。我们曾用此告警提前 2 小时发现一个隐性 bug——vLLM 在处理含特殊 Unicode 字符(如 🌍)的 prompt 时,会静默截断后续 token,导致长文档生成不全。传统 HTTP 成功率告警对此完全免疫。
5. 实战复盘:一次从“压崩”到“稳过 2000 QPS”的完整调优链路
光讲原理不够,来看一个真实项目:为某金融客服大模型(Qwen2-7B + RAG)做上线前压测。初始目标是支撑 1500 QPS(即 1500 句/秒),但首轮测试直接崩溃。
5.1 第一轮:JMeter 报错,Prometheus 一片空白
现象:JMeter 线程数设为 200,30 秒 ramp-up 后,错误率瞬间飙到 100%,错误日志全是
java.net.SocketTimeoutException: Read timed out;Prometheus 看板上vllm:request_success_total停止增长,gpu_dram_throughput_pct卡在 35% 不动。根因排查:
- 先查 JMeter 日志,发现
Read timed out是客户端超时,不是服务端拒绝,说明请求发出去了但没回来; - 登录 vLLM 服务器,
htop显示 CPU 仅 40%,nvidia-smi显示 GPU 利用率 0%,netstat -an \| grep :8000 \| wc -l显示 ESTABLISHED 连接数 200 —— 完全吻合 JMeter 线程数; - 关键线索:
dmesg \| tail输出TCP: time wait bucket table overflow。原来 Linux 内核net.ipv4.tcp_max_tw_buckets默认 32768,而 JMeter 每次请求后连接不复用(Keep-Alive 关闭),200 并发 × 每秒 5 次 = 1000 连接/秒,32 秒就耗尽 time-wait 桶。
- 先查 JMeter 日志,发现
修复:在 JMeter 的 HTTP Request Defaults 中勾选
Use KeepAlive,并在 vLLM 启动参数加--disable-frontend-multiprocessing(避免多进程抢夺连接)。重跑后,错误率归零,但gpu_dram_throughput_pct仍卡在 45%,QPS 仅 320。
5.2 第二轮:GPU 带宽吃不饱,瓶颈在 CPU
- 现象:QPS 卡在 320,
gpu_dram_throughput_pct仅 45%,但cpu_usage_percent达 92%,vllm:decode_tokens_per_sec仅 1200。 - 根因定位:用
py-spy record -p $(pgrep -f 'vllm.entrypoints.api_server') -o profile.svg采样火焰图,发现 68% 时间花在json.loads()解析请求体上——因为 JMeter 发送的 prompt 是 500 字符随机字符串,vLLM 的 JSON parser 要反复分配内存、拷贝字符串。 - 修复:
- 在 JMeter 中改用真实 query 日志(
query.csv),平均长度 80 字符,JSON 解析耗时降为 1/5; - 给 vLLM 加
--enable-chunked-prefill参数,让长 prompt 分块预填充,避免单次大内存分配; - 调整
--max-num-batched-tokens=4096,让 batch 更紧凑。
修复后,cpu_usage_percent降至 55%,gpu_dram_throughput_pct升至 78%,QPS 达 890。
- 在 JMeter 中改用真实 query 日志(
5.3 第三轮:突破 1500 QPS,遭遇 KV Cache 瓶颈
- 现象:QPS 从 890 冲到 1420,但
llm_kv_cache_hit_ratio从 0.92 断崖跌至 0.61,vllm:request_e2e_latency_seconds99 分位从 1.2s 涨到 4.8s。 - 根因分析:看
llm-probe的prompt_lenlabel 分布,发现 JMeter 的 CSV 数据中,30% query 长度 < 20 字符,70% > 150 字符,长度方差极大,导致 vLLM 的 continuous batching 效率极低——短 query 要等长 query 完成才能释放 cache。 - 终极修复:
- 数据分层:将
query.csv按长度分为short.csv(<50 字)、medium.csv(50-200 字)、long.csv(>200 字)三个文件; - JMeter 分组压测:用三个 Thread Group,分别加载不同 CSV,设置不同
batch_size(short 组batch_size=64,long 组batch_size=8); - vLLM 多实例部署:启动三个 vLLM 实例,分别绑定
--model qwen2-7b-short、--model qwen2-7b-medium、--model qwen2-7b-long,用 Nginx 做路由。
最终,llm_kv_cache_hit_ratio稳定在 0.94,gpu_dram_throughput_pct达 84%,QPS 稳定在 2100,超额完成目标。
- 数据分层:将
踩坑总结:AI 压测没有“万能参数”。我们曾以为
--max-num-batched-tokens=4096是最优解,直到在long.csv组单独压测时发现,当batch_size=8且 prompt 平均长度 300 字时,4096 ÷ 300 ≈ 13,但实际 batch size 被限制在 8,因为--max-num-seqs=256优先级更高。最终公式是:effective_batch_size = min(max_num_seqs, max_num_batched_tokens ÷ avg_prompt_len)。这个动态关系,必须靠llm-probe的实时指标才能看清。
6. 为什么说这是“老工具的新生命”,而不是“新瓶装旧酒”
回到标题——“用 JMeter+Prometheus 玩 AI 压测?老工具也能开外挂!”。很多人看完会觉得:“哦,就是加了点脚本和 exporter”。但真正内行知道,这背后是一场工具哲学的迁移。
JMeter 的本质,从来不是“HTTP 压测工具”,而是“可编程的负载生成引擎”。它的JSR223接口、Custom Sampler扩展点、Backend Listener回调机制,天生为协议定制而生。我们所做的,不过是把当年为 SOAP、gRPC、MQTT 写的插件精神,延续到了 LLM API 上。同样,Prometheus 的本质,也不是“服务器监控工具”,而是“指标标准化协议”。它的OpenMetrics格式、label体系、histogram类型,为任何领域定义自己的“可观测性原语”留好了接口。llm-probe没有发明新概念,只是把kv_cache_hit_ratio这个 AI 工程师天天嘴边的词,翻译成了 Prometheus 能懂的语言。
所以,这不是给老工具“打补丁”,而是唤醒它们被遗忘的基因。当你在 JMeter 里用 Groovy 解析 SSE 流,你是在实践“流式计算”的古老智慧;当你在 Prometheus 里定义llm_ttft_mshistogram,你是在重演 2000 年代 Google SRE 对“用户体验延迟”的量化革命。工具会老,但工程的本质不会变:用可测量的方式,理解不可见的系统行为。
最后分享一个细节:我们团队把这套方案命名为AIPress(AI Pressure Test),它的 logo 是一个 JMeter 的齿轮咬合 Prometheus 的靶心,中间嵌着一行小字:“Measure what matters, not what’s easy.” —— 这不是口号,是我们踩了 37 次坑后,刻在 README 里的第一行注释。
