JMeter + InfluxDB + Grafana:搭建生产级压测实时监控与告警体系

摘要: 前 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

  1. 在 Grafana 左侧菜单点 Dashboards → Import
  2. 在 Import via grafana.com 输入 ID: 5496(JMeter Dashboard by petersutter)
  3. 选择刚配好的 InfluxDB 数据源
  4. 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=OKtransaction=下单接口)区分数据。

关键配置区别 :每台 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 配置),但对压测效率的提升是质的飞跃。核心经验:

  1. BackendListener 是分布式压测的标配 :不要指望 Master 汇总 -r 的结果,分布式压测必须用 BackendListener 异步推 InfluxDB。
  2. 告警是压测的刚需:压测经常跑在无人值守时段,没有 Alert 等于"跑完了但不一定有用"。
  3. 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 上的实测结果。为保护商业机密,部分监控指标和业务数据已做脱敏处理,但技术架构和配置细节保持完整和真实。

如有任何疑问,欢迎在评论区交流讨论。
如果本文对你有帮助,欢迎点赞、收藏、转发!

有任何问题或建议,请在评论区留言交流~

关注我,获取更多性能测试实战经验!

行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激!

专栏导航:

相关推荐
2601_960906721 小时前
Anthropic把Claude Code 2.1.236及以上版本的Fable 5会话
人工智能·kafka·时序数据库·etcd·tdengine
wuhuhuan18 小时前
【JMeter 学习打卡 Day 7】从会用到会用对,完成一次完整压测
学习·jmeter
2601_9609067221 小时前
全球AI产业正加速迈入全域渗透
elasticsearch·百度·database·时序数据库·memcache
玉&心2 天前
在grafana中引入prometheus的数据监控
grafana·prometheus
行者-全栈开发3 天前
JMeter 自定义 Sampler 开发:从零写 TCP 私有协议压测插件
jmeter·tcp协议·源码解析·插件开发·gradle构建·自定义sampler·abstractsampler
wuhuhuan3 天前
【JMeter 学习打卡 Day 5】控制流程和节奏的利器
学习·jmeter
玉&心3 天前
在grafana中加入elasticsearch的日志dashboard
elasticsearch·grafana
byte轻骑兵4 天前
告别手工调参!TimechoAI时序大模型实战:打通TimechoDB存储到智能时序分析全链路
时序数据库·预测性维护·timechodb·工业大数据·时序大模型·timechoai