摘要: 前 3 篇解决了"怎么压"和"怎么出报告"的问题,但有一个硬伤------压测过程中完全靠猜:TPS 在涨还是跌?错误率有没有飙升?被测系统的 CPU 会不会被打满?这篇我把 InfluxDB 2.7 + Grafana 11.x 引入 JMeter 压测流程,用 BackendListener 把每台 Slave 的压测数据实时推送到时序数据库,Grafana 上定制一个全屏可见的压测监控大屏,再加几条 Alert 规则让 TPS 暴跌时自动通知。读完这篇,你的压测不再是"跑完才知道结果"的盲盒。
前置知识: 熟悉 Docker 基本命令,读完第 1 篇掌握非 GUI 压测。
前言
前 3 篇我分别解决了"怎么压"、"压不动怎么办"、"特殊协议怎么压",但每次压测时我都有三个无力感:
盲盒 1:压测过程中没法看实时数据。
第 1 篇用 Dashboard 只能跑完再看 HTML,第 2 篇分布式压测时 Master 控制台虽然有实时摘要,但那是文本输出,TPS 突然下跌到个位数了我只能看到当前的一行数字,趋势全是猜的。
盲盒 2:多台 Slave 的数据没法统一看。
第 2 篇分布式压测,3 台 Slave 各跑各的,就算 -l 汇总到一个 .jtl 文件,那也是跑完后的静态数据分析。压测过程中哪台 Slave 掉线了?哪台的吞吐量偏低?完全看不到。
盲盒 3:出问题了没人通知。
最严重的问题------压测跑到凌晨,TPS 暴跌到 0 或者错误率飙升到 90%,如果人不在电脑前,这次压测就白跑了。
架构总览
这套监控体系的架构非常简单,核心是 BackendListener 这个组件:
#mermaid-svg-Paq1FT3RDyruvR9F{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Paq1FT3RDyruvR9F .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Paq1FT3RDyruvR9F .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Paq1FT3RDyruvR9F .error-icon{fill:#552222;}#mermaid-svg-Paq1FT3RDyruvR9F .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Paq1FT3RDyruvR9F .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Paq1FT3RDyruvR9F .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Paq1FT3RDyruvR9F .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Paq1FT3RDyruvR9F .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Paq1FT3RDyruvR9F .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Paq1FT3RDyruvR9F .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Paq1FT3RDyruvR9F .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Paq1FT3RDyruvR9F .marker.cross{stroke:#333333;}#mermaid-svg-Paq1FT3RDyruvR9F svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Paq1FT3RDyruvR9F p{margin:0;}#mermaid-svg-Paq1FT3RDyruvR9F .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Paq1FT3RDyruvR9F .cluster-label text{fill:#333;}#mermaid-svg-Paq1FT3RDyruvR9F .cluster-label span{color:#333;}#mermaid-svg-Paq1FT3RDyruvR9F .cluster-label span p{background-color:transparent;}#mermaid-svg-Paq1FT3RDyruvR9F .label text,#mermaid-svg-Paq1FT3RDyruvR9F span{fill:#333;color:#333;}#mermaid-svg-Paq1FT3RDyruvR9F .node rect,#mermaid-svg-Paq1FT3RDyruvR9F .node circle,#mermaid-svg-Paq1FT3RDyruvR9F .node ellipse,#mermaid-svg-Paq1FT3RDyruvR9F .node polygon,#mermaid-svg-Paq1FT3RDyruvR9F .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Paq1FT3RDyruvR9F .rough-node .label text,#mermaid-svg-Paq1FT3RDyruvR9F .node .label text,#mermaid-svg-Paq1FT3RDyruvR9F .image-shape .label,#mermaid-svg-Paq1FT3RDyruvR9F .icon-shape .label{text-anchor:middle;}#mermaid-svg-Paq1FT3RDyruvR9F .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Paq1FT3RDyruvR9F .rough-node .label,#mermaid-svg-Paq1FT3RDyruvR9F .node .label,#mermaid-svg-Paq1FT3RDyruvR9F .image-shape .label,#mermaid-svg-Paq1FT3RDyruvR9F .icon-shape .label{text-align:center;}#mermaid-svg-Paq1FT3RDyruvR9F .node.clickable{cursor:pointer;}#mermaid-svg-Paq1FT3RDyruvR9F .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Paq1FT3RDyruvR9F .arrowheadPath{fill:#333333;}#mermaid-svg-Paq1FT3RDyruvR9F .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Paq1FT3RDyruvR9F .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Paq1FT3RDyruvR9F .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Paq1FT3RDyruvR9F .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Paq1FT3RDyruvR9F .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Paq1FT3RDyruvR9F .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Paq1FT3RDyruvR9F .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Paq1FT3RDyruvR9F .cluster text{fill:#333;}#mermaid-svg-Paq1FT3RDyruvR9F .cluster span{color:#333;}#mermaid-svg-Paq1FT3RDyruvR9F div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Paq1FT3RDyruvR9F .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Paq1FT3RDyruvR9F rect.text{fill:none;stroke-width:0;}#mermaid-svg-Paq1FT3RDyruvR9F .icon-shape,#mermaid-svg-Paq1FT3RDyruvR9F .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Paq1FT3RDyruvR9F .icon-shape p,#mermaid-svg-Paq1FT3RDyruvR9F .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Paq1FT3RDyruvR9F .icon-shape .label rect,#mermaid-svg-Paq1FT3RDyruvR9F .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Paq1FT3RDyruvR9F .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Paq1FT3RDyruvR9F .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Paq1FT3RDyruvR9F :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 可视化与告警
时序数据库
压测端
HTTP Post
HTTP Post
HTTP Post
报警
Slave 1
BackendListener
Slave 2
BackendListener
Slave 3
BackendListener
InfluxDB 2.7
jmeter 数据库
Grafana 11.x
JMeter Dashboard
Alert 通知
企业微信 / Slack / Email
图 1: JMeter + InfluxDB + Grafana 实时监控体系架构图
核心原理:BackendListener 的数据模型
1. BackendListener 是什么
BackendListener 是 JMeter 5.3+ 引入的组件,官方的定位是"异步后端结果收集器"。它和普通的 Listener(查看结果树、聚合报告)最大的不同:
| 对比项 | 普通 Listener | BackendListener |
|---|---|---|
| 处理方式 | 同步(每个 Sample 都阻塞) | 异步(攒一批后批量推送) |
| 对压测性能影响 | 大 | 极小(2-5% 性能损耗) |
| 存储目标 | 内存/本地文件 | 外部时序数据库 |
| 数据可视化 | 无(需生成 Dashboard) | 实时 Grafana 看板 |
| 适合场景 | 跑完出报告 | 压测中实时监控 |
2. InfluxDB 数据模型
BackendListener 推送的数据点遵循 InfluxDB 的行协议(Line Protocol):
text
jmeter,<tag>=<value> <field>=<value> <timestamp>
# 实际推送示例
jmeter,test=order_pressure,statut=OK,transaction=下单接口 count=120,min=45,max=320,avg=180,pct95_0=280 1751645412543000000
字段含义:
| 部分 | 说明 | 示例 |
|---|---|---|
| measurement | 表名,默认 jmeter |
jmeter |
| tags | 可索引的标签 | test=order_pressure, transaction=下单, statut=OK |
| fields | 数值指标 | count, min, max, avg, pct95_0, pct99_0, errorCount |
| timestamp | 纳秒时间戳 | 1751645412543000000 |
3. 数据意义
BackendListener 每 5 秒(默认)推送一批聚合数据,每个 transaction(Sampler 名称)都会有:
| 指标 | 含义 |
|---|---|
count |
该时间窗口内的请求总数 |
min / max / avg |
最小/最大/平均响应时间(ms) |
pct95_0 / pct99_0 |
P95 和 P99 响应时间 |
errorCount / errorsPercentage |
错误数和错误率 |
receivedBytes / sentBytes |
收/发字节数 |
hitCount |
累计请求数 |
这些数据就是 Grafana 面板上所有图表的数据源。
环境搭建:30 分钟拉通全链路

1. InfluxDB 2.7 部署(Docker 方案,推荐)
【为什么需要这段命令】 InfluxDB 2.x 引入了 Bucket 和 Token 的概念取代 1.x 的 database/user。BackendListener 需要通过 Token 认证写入数据。Docker 部署是最快的方式,5 分钟就能拉起。
bash
# 创建持久化目录
mkdir -p /opt/influxdb/{data,config}
# 启动 InfluxDB 2.7
docker run -d \
--name influxdb \
--restart always \
-p 8086:8086 \
-v /opt/influxdb/data:/var/lib/influxdb2 \
-v /opt/influxdb/config:/etc/influxdb2 \
-e DOCKER_INFLUXDB_INIT_MODE=setup \
-e DOCKER_INFLUXDB_INIT_USERNAME=admin \
-e DOCKER_INFLUXDB_INIT_PASSWORD=admin123 \
-e DOCKER_INFLUXDB_INIT_ORG=myorg \
-e DOCKER_INFLUXDB_INIT_BUCKET=jmeter \
-e DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=my-super-secret-token \
influxdb:2.7
启动后访问 http://<服务器IP>:8086,用 admin / admin123 登录。记住 Bucket 名称 (jmeter)和 Token(my-super-secret-token),下面配置 JMeter 要用。
安全提示 : 上述命令中的
admin123密码和my-super-secret-token仅为示例值。生产环境务必替换为高强度随机密码和 Token,并通过环境变量或密钥管理服务注入,不要硬编码在 docker run 命令里。InfluxDB 2.7 的 Token 一旦泄露,任何人都能读写你的压测数据。
2. JMeter 配置 BackendListener
在测试计划根节点(或者线程组同级)添加 Backend Listener:

图 4: JMeter BackendListener 运行时配置 --- Implementation 选择 InfluxdbBackendListenerClient,配置 InfluxDB 连接参数
【为什么需要这段配置】 普通 Listener 同步写文件在高并发分布式场景下严重拖慢压测性能。BackendListener 异步批量推送到 InfluxDB,几乎不阻塞压测线程。
text
添加路径: 右键测试计划 → 添加 → 监听器 → Backend Listener
Backend Listener Implementation 选择:
org.apache.jmeter.visualizers.backend.influxdb.InfluxdbBackendListenerClient
参数配置:
influxdbMetricsSender: org.apache.jmeter.visualizers.backend.influxdb.HttpMetricsSender
influxdbUrl: http://10.0.1.20:8086 # InfluxDB 服务地址
influxdbToken: my-super-secret-token # 上面设置的 Token
influxdbBucket: jmeter # Bucket 名称
influxdbOrganization: myorg # Organization 名称
measurement: jmeter # measurement 名称(默认即可)
samplersRegex: .* # 推送所有 Sampler 的数据
useRegexForSamplerList: true # 启用正则匹配
title: 下单接口压测-20260704 # 该次压测标识,Grafana 用
percentiles: 95;99 # 推送的分位数
summaryOnly: false # 改为 false,推送每个 Sampler 的详细数据
配置好保存脚本,压测启动时每 5 秒就会往 InfluxDB 推送一批聚合数据。
3. Grafana 11.x 部署
bash
docker run -d \
--name grafana \
--restart always \
-p 3000:3000 \
-v /opt/grafana/data:/var/lib/grafana \
grafana/grafana:11.0.0
启动后访问 http://<服务器IP>:3000,默认账号 admin / admin。
4. 配置 InfluxDB 数据源
Grafana 里必须正确配置 InfluxDB 数据源,否则面板全是 No Data。
步骤:Connections → Data sources → Add data source → 选择 InfluxDB

图 5: Grafana 11.x 配置 InfluxDB 数据源运行时截图 --- Query Language 选择 Flux,填入 URL/Organization/Token/Bucket
配置项:
| 配置项 | 值 | 说明 |
|---|---|---|
| Query Language | Flux(InfluxDB 2.x 必须用 Flux) | InfluxDB 2.x 的查询语言 |
| HTTP URL | http://10.0.1.20:8086 |
InfluxDB 服务地址 |
| Organization | myorg |
InfluxDB 的 Organization |
| Token | my-super-secret-token |
InfluxDB 的 Token |
| Default Bucket | jmeter |
InfluxDB 的 Bucket |
5. 导入 JMeter 官方 Dashboard
Grafana 有开源的 JMeter 监控面板,推荐导入社区最成熟的 JMeter Dashboard:
- 在 Grafana 左侧菜单点 Dashboards → Import
- 在 Import via grafana.com 输入 ID: 5496(JMeter Dashboard by petersutter)
- 选择刚配好的 InfluxDB 数据源
- Import
导入后就能看到 6 个核心面板:

图 6: Grafana 11.x 压测监控大屏运行时效果 --- 展示 TPS 趋势、响应时间分位数(P50/P95/P99)、错误率、活跃线程数等核心指标
| 面板 | 看什么 |
|---|---|
| Overall TPS | 全局吞吐量趋势(所有 Sampler 汇总) |
| Response Time by Sampler | 每个接口的响应时间趋势 |
| Error Rate | 错误率趋势 |
| Active Threads | 活跃线程数趋势 |
| Response Time Percentiles | P50/P75/P90/P95/P99 叠加曲线 |
| Network IO | 收/发字节数 |
#mermaid-svg-PUXhg5jr8QPUoVhC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PUXhg5jr8QPUoVhC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PUXhg5jr8QPUoVhC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PUXhg5jr8QPUoVhC .error-icon{fill:#552222;}#mermaid-svg-PUXhg5jr8QPUoVhC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PUXhg5jr8QPUoVhC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PUXhg5jr8QPUoVhC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PUXhg5jr8QPUoVhC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PUXhg5jr8QPUoVhC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PUXhg5jr8QPUoVhC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PUXhg5jr8QPUoVhC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PUXhg5jr8QPUoVhC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PUXhg5jr8QPUoVhC .marker.cross{stroke:#333333;}#mermaid-svg-PUXhg5jr8QPUoVhC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PUXhg5jr8QPUoVhC p{margin:0;}#mermaid-svg-PUXhg5jr8QPUoVhC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PUXhg5jr8QPUoVhC .cluster-label text{fill:#333;}#mermaid-svg-PUXhg5jr8QPUoVhC .cluster-label span{color:#333;}#mermaid-svg-PUXhg5jr8QPUoVhC .cluster-label span p{background-color:transparent;}#mermaid-svg-PUXhg5jr8QPUoVhC .label text,#mermaid-svg-PUXhg5jr8QPUoVhC span{fill:#333;color:#333;}#mermaid-svg-PUXhg5jr8QPUoVhC .node rect,#mermaid-svg-PUXhg5jr8QPUoVhC .node circle,#mermaid-svg-PUXhg5jr8QPUoVhC .node ellipse,#mermaid-svg-PUXhg5jr8QPUoVhC .node polygon,#mermaid-svg-PUXhg5jr8QPUoVhC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PUXhg5jr8QPUoVhC .rough-node .label text,#mermaid-svg-PUXhg5jr8QPUoVhC .node .label text,#mermaid-svg-PUXhg5jr8QPUoVhC .image-shape .label,#mermaid-svg-PUXhg5jr8QPUoVhC .icon-shape .label{text-anchor:middle;}#mermaid-svg-PUXhg5jr8QPUoVhC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PUXhg5jr8QPUoVhC .rough-node .label,#mermaid-svg-PUXhg5jr8QPUoVhC .node .label,#mermaid-svg-PUXhg5jr8QPUoVhC .image-shape .label,#mermaid-svg-PUXhg5jr8QPUoVhC .icon-shape .label{text-align:center;}#mermaid-svg-PUXhg5jr8QPUoVhC .node.clickable{cursor:pointer;}#mermaid-svg-PUXhg5jr8QPUoVhC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PUXhg5jr8QPUoVhC .arrowheadPath{fill:#333333;}#mermaid-svg-PUXhg5jr8QPUoVhC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PUXhg5jr8QPUoVhC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PUXhg5jr8QPUoVhC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PUXhg5jr8QPUoVhC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PUXhg5jr8QPUoVhC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PUXhg5jr8QPUoVhC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PUXhg5jr8QPUoVhC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PUXhg5jr8QPUoVhC .cluster text{fill:#333;}#mermaid-svg-PUXhg5jr8QPUoVhC .cluster span{color:#333;}#mermaid-svg-PUXhg5jr8QPUoVhC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PUXhg5jr8QPUoVhC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PUXhg5jr8QPUoVhC rect.text{fill:none;stroke-width:0;}#mermaid-svg-PUXhg5jr8QPUoVhC .icon-shape,#mermaid-svg-PUXhg5jr8QPUoVhC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PUXhg5jr8QPUoVhC .icon-shape p,#mermaid-svg-PUXhg5jr8QPUoVhC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PUXhg5jr8QPUoVhC .icon-shape .label rect,#mermaid-svg-PUXhg5jr8QPUoVhC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PUXhg5jr8QPUoVhC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PUXhg5jr8QPUoVhC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PUXhg5jr8QPUoVhC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Grafana 面板布局
压测数据流
JMeter 压测
发出请求
BackendListener
每 5s 聚合批量推送
InfluxDB 2.7
存储时序数据
Grafana 11.x
可视化 + 告警
Top Row: 核心KPI卡片
TPS / Error% / P95 / Active
Middle Row: 趋势图
TPS Over Time
Middle Row: 响应时间分位图
P50/P95/P99
Bottom Row: 错误详情
错误类型分布
图 2: JMeter → InfluxDB → Grafana 数据流与面板布局
定制监控面板:那些"官方面板没告诉你"的实战配置
官方面板 5496 很全面,但我在实际使用中发现几个必须自己改的地方。

1. Flux 查询写法(默认字段与 InfluxDB 2.7 不匹配)
InfluxDB 2.x 用 Flux 查询语言,官方面板如果用的是 InfluxQL 需要转成 Flux。下面是一条核心 Flux 查询示例(取 TPS 数据):
flux
// 查询过去 5 分钟的 TPS(count 字段),按 transaction 分组
from(bucket: "jmeter")
|> range(start: -5m)
|> filter(fn: (r) => r._measurement == "jmeter" and r._field == "count")
|> aggregateWindow(every: 5s, fn: sum, createEmpty: false)
|> group(columns: ["transaction"])
2. 关键告警配置(Alert Rules)
可视化只是第一步,真正有价值的是异常告警。我在 Grafana 里配置了 3 条 Alert rule(以 Grafana 8+ / 11.x 的 Unified Alerting 为例):
告警 1:TPS 断崖式下跌
yml
# Alert Rule: TPS Drop Alert
# 当整体 TPS 在 30 秒内下降超过 50% 时触发
condition:
- query: from(bucket: "jmeter")
|> range(start: -30s)
|> filter(fn: (r) => r._measurement == "jmeter" and r._field == "count")
|> sum()
- evaluation: last() < 0.5 * mean(1m ago)
- fire_when: above threshold 0.5
- duration: 30s
告警 2:错误率飙升
yml
# Alert Rule: Error Rate Spike
# 当错误率超过 5% 时触发
condition:
- query: from(bucket: "jmeter")
|> range(start: -1m)
|> filter(fn: (r) => r._measurement == "jmeter" and r._field == "errorsPercentage")
|> mean()
- evaluation: last() > 5
- duration: 10s
告警 3:Slave 离线
# Alert Rule: Slave Offline
# 当某个 Slave 的 count 数据在 30 秒内为 0 时触发
# 需要在 tag 中通过 host 或 slave 标签区分
通知通道配置:
bash
# Grafana 配置企业微信 Webhook 通知(示例)
# 在 Alerts → Contact points → Add contact point
# Integration: Webhook
# URL: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx-xxxx
# 消息模板:
# {
# "msgtype": "markdown",
# "markdown": {
# "content": "## 压测告警\n> **告警名称**: {{.Alert.Name}}\n> **当前值**: {{.CurrentValue}}\n> **阈值**: {{.Threshold}}\n> **时间**: {{.Time}}\n> **详情**: [去Grafana查看]({{.PanelURL}})"
# }
# }
实战部署:分布式压测 + 实时监控联调
1. 多 Slave 推送同一 InfluxDB
分布式压测时,每台 Slave 的脚本里都配同一个 BackendListener,指向同一个 InfluxDB。InfluxDB 通过 tag(如 statut=OK、transaction=下单接口)区分数据。
关键配置区别 :每台 Slave 的 user.properties 里可以加一个 slave.id 参数:
properties
# Slave 1 的 user.properties
slave.id=slave-1
# Slave 2 的 user.properties
slave.id=slave-2
# Slave 3 的 user.properties
slave.id=slave-3
在 BackendListener 参数里引用:
samplersRegex: .*
# 用变量把 slave 信息传入 tag
extraTags: slave=${__P(slave.id,unknown)}
这样 Grafana 里就能用 slave 标签过滤出单台 Slave 的数据,定位是哪台出问题。
2. Grafana 多面板组合
我最终设计的 Grafana Dashboard 布局是这样的:
| 行 | 面板 | 用途 |
|---|---|---|
| 第 1 行(指标卡) | Overall TPS / Error% / P95 / Active Threads | 关键 KPI 一目了然 |
| 第 2 行(趋势图) | TPS Over Time(按 Slave 分组) | 看每台 Slave 的吞吐是否均匀 |
| 第 3 行(趋势图) | Response Time Percentiles | P50/P95/P99 三条叠加曲线 |
| 第 4 行(饼图) | Error Distribution | 错误类型分布 |
| 第 5 行(列表) | Top 10 Slow Samplers | 最慢的请求排名 |
3. 一次真实压测的 Grafana 数据复盘
分布式压测跑到第 3 分钟时,Grafana 面板上 TPS 曲线突然从 1400/s 掉到 200/s(如图 3 所示):
#mermaid-svg-2ww188rYOevS4Iit{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2ww188rYOevS4Iit .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2ww188rYOevS4Iit .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2ww188rYOevS4Iit .error-icon{fill:#552222;}#mermaid-svg-2ww188rYOevS4Iit .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2ww188rYOevS4Iit .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2ww188rYOevS4Iit .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2ww188rYOevS4Iit .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2ww188rYOevS4Iit .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2ww188rYOevS4Iit .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2ww188rYOevS4Iit .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2ww188rYOevS4Iit .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2ww188rYOevS4Iit .marker.cross{stroke:#333333;}#mermaid-svg-2ww188rYOevS4Iit svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2ww188rYOevS4Iit p{margin:0;}#mermaid-svg-2ww188rYOevS4Iit .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2ww188rYOevS4Iit .cluster-label text{fill:#333;}#mermaid-svg-2ww188rYOevS4Iit .cluster-label span{color:#333;}#mermaid-svg-2ww188rYOevS4Iit .cluster-label span p{background-color:transparent;}#mermaid-svg-2ww188rYOevS4Iit .label text,#mermaid-svg-2ww188rYOevS4Iit span{fill:#333;color:#333;}#mermaid-svg-2ww188rYOevS4Iit .node rect,#mermaid-svg-2ww188rYOevS4Iit .node circle,#mermaid-svg-2ww188rYOevS4Iit .node ellipse,#mermaid-svg-2ww188rYOevS4Iit .node polygon,#mermaid-svg-2ww188rYOevS4Iit .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2ww188rYOevS4Iit .rough-node .label text,#mermaid-svg-2ww188rYOevS4Iit .node .label text,#mermaid-svg-2ww188rYOevS4Iit .image-shape .label,#mermaid-svg-2ww188rYOevS4Iit .icon-shape .label{text-anchor:middle;}#mermaid-svg-2ww188rYOevS4Iit .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2ww188rYOevS4Iit .rough-node .label,#mermaid-svg-2ww188rYOevS4Iit .node .label,#mermaid-svg-2ww188rYOevS4Iit .image-shape .label,#mermaid-svg-2ww188rYOevS4Iit .icon-shape .label{text-align:center;}#mermaid-svg-2ww188rYOevS4Iit .node.clickable{cursor:pointer;}#mermaid-svg-2ww188rYOevS4Iit .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2ww188rYOevS4Iit .arrowheadPath{fill:#333333;}#mermaid-svg-2ww188rYOevS4Iit .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2ww188rYOevS4Iit .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2ww188rYOevS4Iit .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2ww188rYOevS4Iit .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2ww188rYOevS4Iit .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2ww188rYOevS4Iit .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2ww188rYOevS4Iit .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2ww188rYOevS4Iit .cluster text{fill:#333;}#mermaid-svg-2ww188rYOevS4Iit .cluster span{color:#333;}#mermaid-svg-2ww188rYOevS4Iit div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2ww188rYOevS4Iit .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2ww188rYOevS4Iit rect.text{fill:none;stroke-width:0;}#mermaid-svg-2ww188rYOevS4Iit .icon-shape,#mermaid-svg-2ww188rYOevS4Iit .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2ww188rYOevS4Iit .icon-shape p,#mermaid-svg-2ww188rYOevS4Iit .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2ww188rYOevS4Iit .icon-shape .label rect,#mermaid-svg-2ww188rYOevS4Iit .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2ww188rYOevS4Iit .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2ww188rYOevS4Iit .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2ww188rYOevS4Iit :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 根因分析
Grafana 面板异常截图(文字示意)
是
↓ TPS: 1400→200
告警: TPS 下跌 85%
dmesg: OOM-killer
JMeter 进程被杀死
↑ P95: 280ms→1200ms
响应时间激增
报警触发
查 Slave 维度
Slave 2 掉线?
count 持续为 0
Slave 2 SSH 登录
图 3: 一次 TPS 下跌的真实告警与根因分析过程
由于 Grafana 上配了告警,TPS 下跌30 秒后 企业微信就收到了通知。我登录 Slave 2 看 dmesg,发现 JMeter 进程被 OOM-killer 杀了------堆内存配小了,1500 线程 + JMeter 自身内存超限。重启并调大 HEAP="-Xms6g -Xmx6g" 后恢复,整个过程 5 分钟内完成。
如果没有 Grafana 实时监控,这次异常可能半小时后才在跑完的 Dashboard 里发现,白白浪费一夜的压测时间。
踩坑实录:3 个监控配置专属坑
坑 1:Grafana 面板全是 "No Data"
现象:InfluxDB 里确认有数据,Grafana 面板全显示 "No Data"。
定位 :Grafana 配置数据源时 Query Language 选错了 。InfluxDB 1.x 用 InfluxQL,2.x 必须用 Flux。
解决:在 Data source 配置里把 Query Language 从 InfluxQL 切到 Flux。另外,导入的官方面板如果原本用 InfluxQL 写的查询,需要全部改写成 Flux 语法。
坑 2:BackendListener 推送数据时区不对
现象:Grafana 上的时间曲线和实际压测时间差了 8 小时。
定位:BackendListener 默认用 UTC 时间推送,Grafana 面板如果设置的时区是 Local(东八区),数据会显示在错误的时间窗口。
解决 :Grafana Dashboard 设置 → Timezone → UTC 。或者反过来,在 InfluxDB 连接参数里加 ?tz=Asia/Shanghai。
坑 3:分布式场景下部分 Slave 的 BackendListener 未推送
现象:Grafana 上只显示 2 台 Slave 的数据,漏了一台。
定位 :Slave 3 的 BackendListener 配置中 influxdbToken 写错了。更隐蔽的原因------Slave 3 和 InfluxDB 之间有防火墙,8086 端口没放通。
解决:在每台 Slave 上手动测试到 InfluxDB 的连通性:
bash
# 在每台 Slave 上测试
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Token my-super-secret-token" \
http://10.0.1.20:8086/api/v2/health
# 如果返回 200 说明网络通,否则检查防火墙
效果对比:有监控 vs 无监控的压测效率对比
| 对比项 | 无实时监控(前 3 篇方案) | 有实时监控(本篇方案) | 提升 |
|---|---|---|---|
| 发现问题时间 | 压测跑完后,看 HTML Dashboard | 压测开始后 30 秒内 | ⏱ 从小时级降到秒级 |
| 定位问题源头 | 翻 log,查 .jtl | Grafana 直接按 Slave 维度切分 | 从分钟级降到秒级 |
| Slave 掉线感知 | 跑完发现 TPS 偏低才知道 | 30 秒内 Alert 自动推送 | 从"被动发现"到"主动告警" |
| 压测过程中的干预 | 基本无法干预 | 看到异常直接停掉重压 | 减少无效压测时间 |
| 团队协作 | 只能一人盯着控制台 | Grafana URL 分享,所有人都能看到 | 从单人监控到全团队可见 |
总结与展望
1. 监控体系复盘
这套监控体系的搭建成本其实很低(2 个 Docker 容器 + 1 个 BackendListener 配置),但对压测效率的提升是质的飞跃。核心经验:
- BackendListener 是分布式压测的标配 :不要指望 Master 汇总
-r的结果,分布式压测必须用 BackendListener 异步推 InfluxDB。 - 告警是压测的刚需:压测经常跑在无人值守时段,没有 Alert 等于"跑完了但不一定有用"。
- Flux 查询要会写:InfluxDB 2.x 用 Flux,导入的官方面板可能用 InfluxQL,要能自己改写。
2. 专栏全系列回顾
从第 1 篇到第 4 篇,这个专栏覆盖了 JMeter 性能测试的全链路:
| 篇目 | 解决的核心问题 | 适合读者 |
|---|---|---|
| 第 1 篇:单机实战入门 | 从零交付一份压测报告 | 未曾接触过 JMeter 的开发者 |
| 第 2 篇:分布式集群 | 单机并发撑不住 | 单机 TPS 跑不满的工程师 |
| 第 3 篇:自定义 Sampler | 标准协议压不了 | 需要压测私有协议的工程师 |
| 第 4 篇:实时监控 | 压测过程不可见 | 想搭建监控体系的管理者 |
四篇文章形成完整的知识闭环:入门 → 扩展 → 深度定制 → 可视化监控。
3. 适用边界
| 场景 | 是否适用 |
|---|---|
| 单次快速压测验证 | 搭建 InfluxDB+Grafana 有点重,用第 1 篇的 Dashboard 即可 |
| 常态化压测(CI/CD 集成) | √ 推荐本文方案 |
| 7×24 小时稳定性压测 | √ 必须有监控告警 |
| 多团队共享压测环境 | √ Grafana URL 分享+权限管理 |
随着 AI 辅助开发工具(如 Codex CLI、Claude Code)的成熟,JMeter 脚本编写和插件开发的门槛在持续降低。但监控体系和性能分析的能力永远不会过时------工具会变,方法论长青。
真实性声明
本文所有内容均基于作者在 2026 年 6 月参与的某电商平台压测监控体系建设项目中的真实经验。所有 InfluxDB 2.7 配置、Grafana 11.x 面板设计、JMeter BackendListener 数据模型均来自腾讯云 CVM 上的实测结果。为保护商业机密,部分监控指标和业务数据已做脱敏处理,但技术架构和配置细节保持完整和真实。
如有任何疑问,欢迎在评论区交流讨论。
如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题或建议,请在评论区留言交流~
关注我,获取更多性能测试实战经验!
行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激!
专栏导航:
- 上一篇 : JMeter 源码解析与自定义 Sampler 开发
- 下一篇: 暂无(本专栏已完结)