searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

如何基于JMeter设计和实现云压测平台

2023-07-24 06:38:15
127
0

一、平台背景

为什么要设计和实现一个云压测平台,而不是使用现有开源压测工具?

  1. 平台化:企业需要一个平台化的压测工具,每个团队都可以在这个平台上协作,而开源工具大多是 C/S 类型( 客户端 / 服务器体系结构),缺乏平台化支持。
  2. 标准化: 企业需要一个统一的标准化压测平台,最好能够和公司的其他平台集成,而开源工具在这方面的扩展性一般不强。
  3. 控制成本: 企业需要控制压测平台的维护成本,对于规模大的公司,自研优于使用开源。虽然开源压测工具由社区维护,但反馈较慢,自己维护的成本又比较高,不如重写一个或者二次开发。

二、压测工具对比

2.1 开源工具

Apache JMeter

Apache JMeter是Apache组织开发的基于Java的压力测试工具。Apache JMeter具备如下特性:
  • 支持分布式施压。
  • 支持图形化界面,且支持流程编排,同时支持断言、逻辑控制器等高级指令,可满足复杂业务压测需求。
  • 扩展性强,开发、测试人员可通过编写自己的插件,满足各种压测需求。
  • 技术生态好,有强大的开源社区支持,开发者活跃度高。
  • 支持查看资源监控、性能报告,但可查看的监控和报告指标较少。
  • 基于并发模型,受限于JVM,单机无法支持超高并发。且只支持并发施压模型,不支持吞吐量施压模型。
  • 开源支持的分布式能力无法大规模应用到生产环境,部署成本高。
  • 不支持测试用例管理、压测脚本管理等功能

ApacheBench

ApacheBench(ab)是一款针对HTTP协议做性能压测的命令行工具。ApacheBench具备如下特性:
  • 具有较好的扩展性。
  • 支持协议单一。对HTTP协议支持度较好,不支持主流的HTTPS、WebSocket等协议。
  • 支持请求总数、并发数、压测时长控制。
  • 作为一款命令行压测工具,上手较为简单。
  • 单机压测工具,无分布式施压能力,无可视化界面。
  • 不支持链路编排、场景管理等功能,无法做带业务含义的复杂压测。
  • 单次压测,只能对单个域名或地址发起流量请求。
  • 压测统计指标维度少,缺少压测过程中的统计数据,无法获取系统负载等指标

wrk

wrk是一款针对HTTP协议的基准测试工具。wrk具备如下特性:
  • 轻量级性能测试工具,安装简单。
  • 学习成本低。
  • 基于异步事件驱动框架,单机支持并发高。
  • 单机压测工具,无分布式施压能力。
  • 只支持HTTP协议。
  • 无可视化界面,不支持流程编排、断言等能力,无法满足复杂压测需求。

2.2 能力对比

对比项
 
商用PTS
Jmeter
AB
wrk
成本
学习成本
是否收费
开源免费
开源免费
开源免费
分布式能力
支持分布式
是,有性能缺陷
引擎能力
支持多协议
施压量级
压测场景
是否支持流程编排
 
 
支持断言/出参提取
数据构造
支持文件数据源
支持使用函数生成
压力控制
支持并发模式
支持阶梯递增
支持吞吐量模式
压测过程手动调速
数据可视化
支持实时数据监控
支持
支持,维度有限
不支持
不支持
支持压测报告
支持
支持,报告简单
支持,报告简单
支持,报告简单
支持施压机系统监控数据
支持
不支持
不支持
不支持

 

2.3 引擎选型

综上比较,JMeter无论是功能完整度,社区成熟度还是受众方面都明显优于其他开源工具,并且JMeter作为顶级开源项目,其插件丰富,二次开发成本较低,故使用JMeter作为压测平台底层测试引擎。
 

三、Jmeter概览

3.1 Jmeter介绍

启动方式

Jmeter压测启动方式主要有下面两种:
  1. 脚本命令执行
# 平台根据前端的操作,自动拼接出一行可执行的命令,然后在指定服务器上执行这段脚本。
# 相当于是手工敲的命令平台帮着拼接和回车执行了。
# 即便是前端来生成测试脚本,也可以先保存成 jmx 文件,再脚本执行。 
# 特点是平台和 Jmeter_Home 完全分离,带来的: 
# 1. 平台代码可以不用 Java 写了,什么语言写都可以,仅仅是拼装命令。 
# 2. 毕竟是脚本执行,Jmeter 随意切换版本。 
# 3. 平台和 Jmeter 可以不部署在同一台服务器上,即不是相同的进程内了。 
# 4. Jmeter 挂掉不影响平台运行。
 jmeter -n -t create.jmx -l test.jtl
  1. Jmeter的API调用执行,平台代码需要使用Java编写,调用较简单
     // 基于jmx文件创建并启动引擎
        HashTree tree= SaveService.loadTree(jmxFile);
        StandardJMeterEngine engine = new StandardJMeterEngine();
        engine.configure(tree);
        engine.run();
平台这两种都支持:
  • 对于接口调试或者场景调试这种临时一次性的操作,通过Jmeter的API调用执行。
  • 对应启动场景这种长时间压测任务,通过在容器Pod里面启动脚本命令执行。

核心组件

JMeter 有多种组件,我们重点看下这七类: 配置元件、取样器、定时器、前置处理器、后置处理器、断言、监听器。我们来看下它们各自的作用。
  • 配置元件:用于初始化变量,以便采样器使用。类似于框架的配置文件,参数化需要的配置都在配置元件中。
  • 前置处理器:在进行取样器请求之前执行一些操作,比如生成入参数据。
  • 定时器:一般用来指定请求发送的延时策略。在没有定时器的情况下,JMeter 发送请求是不会暂停的。
  • 取样器:承担 JMeter 发送请求的核心功能,支持多种请求类型,如 HTTP、FTP、JDBC 等,也可以使用 Java 类型的请求进行自定义编写。
  • 后置处理器:在取样器请求完成后执行一些操作,通常用于处理响应数据,从中提取需要的值。
  • 断言:主要用于判断取样器请求或对应的响应是否返回了期望的结果。
  • 监听器:监听器可以在 JMeter 执行测试的过程中搜集相关的数据,然后将这些数据在 JMeter 界面上以树、图、报告等形式呈现出来。不过图形化的呈现非常消耗客户端性能,在正式性能测试中并不推荐使用。

二次开发

  1. 自定义Sample
自定义一个JavaSampler需实现下面接口方法即可,其他组件类似实现对应接口方法
public interface JavaSamplerClient {
    /**
     * 初始化
     */
    void setupTest(JavaSamplerContext context);

    /**
     * 具体逻辑实现
     */
    SampleResult runTest(JavaSamplerContext context);

    /**
     * 收尾工作
     */
    void teardownTest(JavaSamplerContext context);

    /**
     * 在Jmeter中显示的属性
     */
    Arguments getDefaultParameters();
}

 

3.2 Jmeter不足

不足

  1. 不支持压测场景管理/复杂的压力控制
  2. 不支持弹性压测和报告实时查看
  3. 原生分布式集群压测有性能瓶颈
  • 分布式执行和单机执行方式的差异较大,需要做很多额外配置,数据文件需要手动上传
  • master 节点通常不参与压测,而是收集 slave 节点的压测信息,这会造成一定程度上的资源浪费
  • slave 节点会将每个请求打点都实时回传给 master 节点,造成大量的带宽消耗和单点瓶颈

扩展

针对上面这些不足,我们可以考虑在平台层来实现这些功能。针对分布式压测我们可以在每台压测机中启动一个压测引擎(基于Jmeter),它能够与压测平台服务器建立通信,这样平台就可以直接对压测机进行调度,相当于我们在平台层重新实现分布式调度功能。
 
分布式压测
Jmeter
压测平台实现
压测脚本分发
master节点进行分发
平台服务统一分发
数据文件分发
用户自行上传到每台压测机
通过对象存储统一获取,根据压测机数量切分文件
结果回传
各slave节点实时回传到master节点
异步回传到平台,统一聚合和入库

 

四、平台设计和实现

4.1 平台方案

状态机

状态机是分布式压测的核心,控制中心和各个压测引擎的行为都受状态机变化的影响。
  1. 创建任务(JobExecution)并开始执行以后,各个任务分片(JobSliceExecution)首先会进入 preparing 状态,各个压测引擎会从云存储下载各自对应的脚本数据,下载完成后作为压测引擎的数据输入。如果下载失败则会重试,即 Prepare。
  2. 如果所有压测引擎都成功下载了脚本数据,则相继进入 prepared 状态,等全部进入 prepared 状态后,JobExecution 也会进入 prepared 状态,并向各个压测引擎发起执行指令,各个 JobSliceExecution 进入 running 状态,等所有压测引擎执行完成且各个 JobSliceExecution 变成 completed 状态之后,JobExecution 也会进入 completed 状态,此时压测任务执行完成并生成压测报告。
  3. 如果各个任务分片在 preparing、prepared 或 running 过程中有任何一个出错,则出错的分片会进入 failed 状态并通知控制中心,控制中心则控制其他分片中止正在执行的任务并进入 Stopping 状态,等这些分片中止成功并都变成 stopped 状态后,JobExecution 会被置成 failed 状态。
  4. 支持手动停止压测任务,这时候 JobSliceExecution 和 JobExecution 都会被置成 stopping->stopped 状态。

 

脚本生成

我们可以知道需要构建的 jmx 结构,最外层是 TestPlan,TestPlan 是 HashTree 结构,包含 ThreadGroup(线程组)、HTTPSamplerProxy、ResultCollector(结果收集)等节点。
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
    <hashTree>
        <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="测试计划" enabled="true">
        </TestPlan>
        <hashTree>
            <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树" enabled="true">
            </ResultCollector>
            <hashTree/>
            <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组" enabled="true">
            </ThreadGroup>
            <hashTree>
                <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true">
                </HTTPSamplerProxy>
                <hashTree/>
            </hashTree>
            <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组" enabled="true">
            </ThreadGroup>
            <hashTree>
                <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true">
                </HTTPSamplerProxy>
                <hashTree/>
            </hashTree>
        </hashTree>
    </hashTree>
</jmeterTestPlan>

 

Jmeter的API

通过Jmeter的API将页面多个协议转换为JMX脚本
//创建执行计划
    TestPlan testPlan = new TestPlan("创建 JMeter 测试脚本");
    testPlan.setProperty(TestElement.TEST_CLASS, TestPlan.class.getName());
    testPlan.setProperty(TestElement.GUI_CLASS, TestPlanGui.class.getName());
    testPlan.setUserDefinedVariables((Arguments) new ArgumentsPanel().createTestElement());
    //创建 ThreadGroup
    ThreadGroup threadGroup = new ThreadGroup();
    threadGroup.setName("Example Thread Group");
    threadGroup.setNumThreads(1);
    threadGroup.setRampUp(1);
    threadGroup.setProperty(TestElement.TEST_CLASS, ThreadGroup.class.getName());
    threadGroup.setProperty(TestElement.GUI_CLASS, ThreadGroupGui.class.getName());
    //创建 Sampler
    HTTPSamplerProxy httpSamplerProxy = new HTTPSamplerProxy();
    httpSamplerProxy.setDomain("127.0.0.1:8080/index");
    httpSamplerProxy.setPort(80);
    httpSamplerProxy.setPath("/");
    httpSamplerProxy.setMethod("GET");
    httpSamplerProxy.setName("Open ip");
    httpSamplerProxy.setProperty(TestElement.TEST_CLASS, HTTPSamplerProxy.class.getName());
    httpSamplerProxy.setProperty(TestElement.GUI_CLASS, HttpTestSampleGui.class.getName());
    //创建结果收集器
    ResultCollector resultCollector = new ResultCollector();
    resultCollector.setName(ResultCollector.class.getName());
    //构建 HashTree
    HashTree subTree = new HashTree();
    subTree.add(httpSamplerProxy);
    subTree.add(loopController);
    subTree.add(threadGroup);
    subTree.add(resultCollector);
    HashTree tree = new HashTree();
    tree.add(testPlan,subTree);

XML工具包

通过dom4j递归扫描Jmx脚本元素进行处理
SAXReader reader = getSAXReader();
// 将jmx输入流转换为Document
Document document = reader.read(source);
Element jmeterTestPlan = document.getRootElement();
List<Element> childNodes = jmeterTestPlan.elements();
for (Element ele : childNodes) {
    parseHashTree(ele);
}

启动压测

弹性压测

用户进行压测时需根据压测参数动态部署压测节点,在压测完成后自动回收压测节点资源。
以每个虚机4C8G设置固定并发(需根据实际测试换算)来换算虚机数量,每个虚机里运行压测引擎作为一个节点。这样有两个好处:
  • 节点易管理:节点统一资源规格,节点调度分配计算实现较简单。
  • 节点易扩展:可通过虚机数量控制并发数,从而提升压测上限。

压力控制

JMeter 提供了吞吐量控制器的插件,我们可以通过设定吞吐量上限来限制 QPS/TPS,能够确保将吞吐量控制在一个固定值上,但这样还远远不够,实际工作中我们希望在每次压测执行时能够随时动态调节吞吐量,这样的功能该如何实现呢?
BeanShell 解释器有一个非常有用的特性——它可以充当服务器,支持可以通过 telnet 或 http 访问。
使用时在 jmeter.properties 中定义以下内容:
beanshell.server.port=9000
beanshell.server.file=../extras/startup.bsh
基于吞吐量控制器,将吞吐量限制值设为占位符(如 ${__P(throughput, 99999999)},throughput 就是占位符),利用 JMeter 的 BeanShell 功能,通过执行外部命令的方式,在运行时注入具体值,达到动态调节吞吐量的目的。编写 update.bsh BeanShell 脚本为:
import org.apache.jmeter.util.JMeterUtils;

getprop(p){ // get a JMeter property
    return JMeterUtils.getPropDefault(p,"");
}

setprop(p,v){ // set a JMeter property
    print("Setting property '"+p+"' to '"+v+"'.");
    JMeterUtils.getJMeterProperties().setProperty(p, v);
}

setprop("throughput", args[0]);
通过运行命令动态调整TPS
sudo java -jar /<jmeter_home>/lib/bshclient.jar localhost 9000 throughput.bsh <参数>

 

结果收集

BackendListener

BackendListener是一种异步监听并获取到测试结果的监听器,也就是说测试结果(如HTTP请求的响应结果)都会被封装在SampleResult对象中并被其实现类监听接收。
public interface BackendListenerClient {
    //开启多线程执行压测之前,传入线程上下文,进行前置处理
    void setupTest(BackendListenerContext context) throws Exception; // NOSONAR

    //多线程压测过程中获取到测试结果集,传入当前方法进行处理
    void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context);

    //多线程压测结束之后进行一个后置处理
    void teardownTest(BackendListenerContext context) throws Exception;
}
通过实现上面 BackendListenerClient 接口来将异步获取到的测试结果SampleResult进行相应处理,然后将元数据上报至kakfa,最后压测聚合模块通过消费kafka Topic异步来收集测试结果。
 

报告聚合

由于压测平台自己实现了分布式压测模式,因此在拿到每台压测机的JTL结果文件后,需要自行对这些JTL文件的内容进行合并和解析,并持久化记录下来。
一个JTL文件的片段
timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
1685412971451,15,1111,200,OK,测试场景1-0 1-1,text,true,,301,868,1,1,http://localhost:40050/grpc-gw-push/pushSync,15,0,0
1685412971821,14,1111,200,OK,测试场景1-0 1-1,text,true,,301,868,1,1,http://localhost:40050/grpc-gw-push/pushSync,14,0,0
1685412972017,16,1111,200,OK,测试场景1-0 1-1,text,true,,301,868,1,1,http://localhost:40050/grpc-gw-push/pushSync,15,0,0
JTL 文件对应每一行的单条结果数据的大小很小(大约只有 100 多个字节),但总量很大,如果我们只是简单的将所有数据存储起来,将会占用大量的存储空间,因此结果数据需要做预聚合再存入。
通常以label(JTL 中的 label)作大分类,维度(errorMsg、errorCode 等)作小分类,以时间作为聚合标准,聚合时间随压测时长调整,从而保证存储大小不会过大,又可以汇总比较丰富的数据。
0条评论
0 / 1000
x****n
4文章数
1粉丝数
x****n
4 文章 | 1 粉丝
原创

如何基于JMeter设计和实现云压测平台

2023-07-24 06:38:15
127
0

一、平台背景

为什么要设计和实现一个云压测平台,而不是使用现有开源压测工具?

  1. 平台化:企业需要一个平台化的压测工具,每个团队都可以在这个平台上协作,而开源工具大多是 C/S 类型( 客户端 / 服务器体系结构),缺乏平台化支持。
  2. 标准化: 企业需要一个统一的标准化压测平台,最好能够和公司的其他平台集成,而开源工具在这方面的扩展性一般不强。
  3. 控制成本: 企业需要控制压测平台的维护成本,对于规模大的公司,自研优于使用开源。虽然开源压测工具由社区维护,但反馈较慢,自己维护的成本又比较高,不如重写一个或者二次开发。

二、压测工具对比

2.1 开源工具

Apache JMeter

Apache JMeter是Apache组织开发的基于Java的压力测试工具。Apache JMeter具备如下特性:
  • 支持分布式施压。
  • 支持图形化界面,且支持流程编排,同时支持断言、逻辑控制器等高级指令,可满足复杂业务压测需求。
  • 扩展性强,开发、测试人员可通过编写自己的插件,满足各种压测需求。
  • 技术生态好,有强大的开源社区支持,开发者活跃度高。
  • 支持查看资源监控、性能报告,但可查看的监控和报告指标较少。
  • 基于并发模型,受限于JVM,单机无法支持超高并发。且只支持并发施压模型,不支持吞吐量施压模型。
  • 开源支持的分布式能力无法大规模应用到生产环境,部署成本高。
  • 不支持测试用例管理、压测脚本管理等功能

ApacheBench

ApacheBench(ab)是一款针对HTTP协议做性能压测的命令行工具。ApacheBench具备如下特性:
  • 具有较好的扩展性。
  • 支持协议单一。对HTTP协议支持度较好,不支持主流的HTTPS、WebSocket等协议。
  • 支持请求总数、并发数、压测时长控制。
  • 作为一款命令行压测工具,上手较为简单。
  • 单机压测工具,无分布式施压能力,无可视化界面。
  • 不支持链路编排、场景管理等功能,无法做带业务含义的复杂压测。
  • 单次压测,只能对单个域名或地址发起流量请求。
  • 压测统计指标维度少,缺少压测过程中的统计数据,无法获取系统负载等指标

wrk

wrk是一款针对HTTP协议的基准测试工具。wrk具备如下特性:
  • 轻量级性能测试工具,安装简单。
  • 学习成本低。
  • 基于异步事件驱动框架,单机支持并发高。
  • 单机压测工具,无分布式施压能力。
  • 只支持HTTP协议。
  • 无可视化界面,不支持流程编排、断言等能力,无法满足复杂压测需求。

2.2 能力对比

对比项
 
商用PTS
Jmeter
AB
wrk
成本
学习成本
是否收费
开源免费
开源免费
开源免费
分布式能力
支持分布式
是,有性能缺陷
引擎能力
支持多协议
施压量级
压测场景
是否支持流程编排
 
 
支持断言/出参提取
数据构造
支持文件数据源
支持使用函数生成
压力控制
支持并发模式
支持阶梯递增
支持吞吐量模式
压测过程手动调速
数据可视化
支持实时数据监控
支持
支持,维度有限
不支持
不支持
支持压测报告
支持
支持,报告简单
支持,报告简单
支持,报告简单
支持施压机系统监控数据
支持
不支持
不支持
不支持

 

2.3 引擎选型

综上比较,JMeter无论是功能完整度,社区成熟度还是受众方面都明显优于其他开源工具,并且JMeter作为顶级开源项目,其插件丰富,二次开发成本较低,故使用JMeter作为压测平台底层测试引擎。
 

三、Jmeter概览

3.1 Jmeter介绍

启动方式

Jmeter压测启动方式主要有下面两种:
  1. 脚本命令执行
# 平台根据前端的操作,自动拼接出一行可执行的命令,然后在指定服务器上执行这段脚本。
# 相当于是手工敲的命令平台帮着拼接和回车执行了。
# 即便是前端来生成测试脚本,也可以先保存成 jmx 文件,再脚本执行。 
# 特点是平台和 Jmeter_Home 完全分离,带来的: 
# 1. 平台代码可以不用 Java 写了,什么语言写都可以,仅仅是拼装命令。 
# 2. 毕竟是脚本执行,Jmeter 随意切换版本。 
# 3. 平台和 Jmeter 可以不部署在同一台服务器上,即不是相同的进程内了。 
# 4. Jmeter 挂掉不影响平台运行。
 jmeter -n -t create.jmx -l test.jtl
  1. Jmeter的API调用执行,平台代码需要使用Java编写,调用较简单
     // 基于jmx文件创建并启动引擎
        HashTree tree= SaveService.loadTree(jmxFile);
        StandardJMeterEngine engine = new StandardJMeterEngine();
        engine.configure(tree);
        engine.run();
平台这两种都支持:
  • 对于接口调试或者场景调试这种临时一次性的操作,通过Jmeter的API调用执行。
  • 对应启动场景这种长时间压测任务,通过在容器Pod里面启动脚本命令执行。

核心组件

JMeter 有多种组件,我们重点看下这七类: 配置元件、取样器、定时器、前置处理器、后置处理器、断言、监听器。我们来看下它们各自的作用。
  • 配置元件:用于初始化变量,以便采样器使用。类似于框架的配置文件,参数化需要的配置都在配置元件中。
  • 前置处理器:在进行取样器请求之前执行一些操作,比如生成入参数据。
  • 定时器:一般用来指定请求发送的延时策略。在没有定时器的情况下,JMeter 发送请求是不会暂停的。
  • 取样器:承担 JMeter 发送请求的核心功能,支持多种请求类型,如 HTTP、FTP、JDBC 等,也可以使用 Java 类型的请求进行自定义编写。
  • 后置处理器:在取样器请求完成后执行一些操作,通常用于处理响应数据,从中提取需要的值。
  • 断言:主要用于判断取样器请求或对应的响应是否返回了期望的结果。
  • 监听器:监听器可以在 JMeter 执行测试的过程中搜集相关的数据,然后将这些数据在 JMeter 界面上以树、图、报告等形式呈现出来。不过图形化的呈现非常消耗客户端性能,在正式性能测试中并不推荐使用。

二次开发

  1. 自定义Sample
自定义一个JavaSampler需实现下面接口方法即可,其他组件类似实现对应接口方法
public interface JavaSamplerClient {
    /**
     * 初始化
     */
    void setupTest(JavaSamplerContext context);

    /**
     * 具体逻辑实现
     */
    SampleResult runTest(JavaSamplerContext context);

    /**
     * 收尾工作
     */
    void teardownTest(JavaSamplerContext context);

    /**
     * 在Jmeter中显示的属性
     */
    Arguments getDefaultParameters();
}

 

3.2 Jmeter不足

不足

  1. 不支持压测场景管理/复杂的压力控制
  2. 不支持弹性压测和报告实时查看
  3. 原生分布式集群压测有性能瓶颈
  • 分布式执行和单机执行方式的差异较大,需要做很多额外配置,数据文件需要手动上传
  • master 节点通常不参与压测,而是收集 slave 节点的压测信息,这会造成一定程度上的资源浪费
  • slave 节点会将每个请求打点都实时回传给 master 节点,造成大量的带宽消耗和单点瓶颈

扩展

针对上面这些不足,我们可以考虑在平台层来实现这些功能。针对分布式压测我们可以在每台压测机中启动一个压测引擎(基于Jmeter),它能够与压测平台服务器建立通信,这样平台就可以直接对压测机进行调度,相当于我们在平台层重新实现分布式调度功能。
 
分布式压测
Jmeter
压测平台实现
压测脚本分发
master节点进行分发
平台服务统一分发
数据文件分发
用户自行上传到每台压测机
通过对象存储统一获取,根据压测机数量切分文件
结果回传
各slave节点实时回传到master节点
异步回传到平台,统一聚合和入库

 

四、平台设计和实现

4.1 平台方案

状态机

状态机是分布式压测的核心,控制中心和各个压测引擎的行为都受状态机变化的影响。
  1. 创建任务(JobExecution)并开始执行以后,各个任务分片(JobSliceExecution)首先会进入 preparing 状态,各个压测引擎会从云存储下载各自对应的脚本数据,下载完成后作为压测引擎的数据输入。如果下载失败则会重试,即 Prepare。
  2. 如果所有压测引擎都成功下载了脚本数据,则相继进入 prepared 状态,等全部进入 prepared 状态后,JobExecution 也会进入 prepared 状态,并向各个压测引擎发起执行指令,各个 JobSliceExecution 进入 running 状态,等所有压测引擎执行完成且各个 JobSliceExecution 变成 completed 状态之后,JobExecution 也会进入 completed 状态,此时压测任务执行完成并生成压测报告。
  3. 如果各个任务分片在 preparing、prepared 或 running 过程中有任何一个出错,则出错的分片会进入 failed 状态并通知控制中心,控制中心则控制其他分片中止正在执行的任务并进入 Stopping 状态,等这些分片中止成功并都变成 stopped 状态后,JobExecution 会被置成 failed 状态。
  4. 支持手动停止压测任务,这时候 JobSliceExecution 和 JobExecution 都会被置成 stopping->stopped 状态。

 

脚本生成

我们可以知道需要构建的 jmx 结构,最外层是 TestPlan,TestPlan 是 HashTree 结构,包含 ThreadGroup(线程组)、HTTPSamplerProxy、ResultCollector(结果收集)等节点。
<?xml version="1.0" encoding="UTF-8"?>
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.5">
    <hashTree>
        <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="测试计划" enabled="true">
        </TestPlan>
        <hashTree>
            <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="查看结果树" enabled="true">
            </ResultCollector>
            <hashTree/>
            <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组" enabled="true">
            </ThreadGroup>
            <hashTree>
                <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true">
                </HTTPSamplerProxy>
                <hashTree/>
            </hashTree>
            <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="线程组" enabled="true">
            </ThreadGroup>
            <hashTree>
                <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="HTTP请求" enabled="true">
                </HTTPSamplerProxy>
                <hashTree/>
            </hashTree>
        </hashTree>
    </hashTree>
</jmeterTestPlan>

 

Jmeter的API

通过Jmeter的API将页面多个协议转换为JMX脚本
//创建执行计划
    TestPlan testPlan = new TestPlan("创建 JMeter 测试脚本");
    testPlan.setProperty(TestElement.TEST_CLASS, TestPlan.class.getName());
    testPlan.setProperty(TestElement.GUI_CLASS, TestPlanGui.class.getName());
    testPlan.setUserDefinedVariables((Arguments) new ArgumentsPanel().createTestElement());
    //创建 ThreadGroup
    ThreadGroup threadGroup = new ThreadGroup();
    threadGroup.setName("Example Thread Group");
    threadGroup.setNumThreads(1);
    threadGroup.setRampUp(1);
    threadGroup.setProperty(TestElement.TEST_CLASS, ThreadGroup.class.getName());
    threadGroup.setProperty(TestElement.GUI_CLASS, ThreadGroupGui.class.getName());
    //创建 Sampler
    HTTPSamplerProxy httpSamplerProxy = new HTTPSamplerProxy();
    httpSamplerProxy.setDomain("127.0.0.1:8080/index");
    httpSamplerProxy.setPort(80);
    httpSamplerProxy.setPath("/");
    httpSamplerProxy.setMethod("GET");
    httpSamplerProxy.setName("Open ip");
    httpSamplerProxy.setProperty(TestElement.TEST_CLASS, HTTPSamplerProxy.class.getName());
    httpSamplerProxy.setProperty(TestElement.GUI_CLASS, HttpTestSampleGui.class.getName());
    //创建结果收集器
    ResultCollector resultCollector = new ResultCollector();
    resultCollector.setName(ResultCollector.class.getName());
    //构建 HashTree
    HashTree subTree = new HashTree();
    subTree.add(httpSamplerProxy);
    subTree.add(loopController);
    subTree.add(threadGroup);
    subTree.add(resultCollector);
    HashTree tree = new HashTree();
    tree.add(testPlan,subTree);

XML工具包

通过dom4j递归扫描Jmx脚本元素进行处理
SAXReader reader = getSAXReader();
// 将jmx输入流转换为Document
Document document = reader.read(source);
Element jmeterTestPlan = document.getRootElement();
List<Element> childNodes = jmeterTestPlan.elements();
for (Element ele : childNodes) {
    parseHashTree(ele);
}

启动压测

弹性压测

用户进行压测时需根据压测参数动态部署压测节点,在压测完成后自动回收压测节点资源。
以每个虚机4C8G设置固定并发(需根据实际测试换算)来换算虚机数量,每个虚机里运行压测引擎作为一个节点。这样有两个好处:
  • 节点易管理:节点统一资源规格,节点调度分配计算实现较简单。
  • 节点易扩展:可通过虚机数量控制并发数,从而提升压测上限。

压力控制

JMeter 提供了吞吐量控制器的插件,我们可以通过设定吞吐量上限来限制 QPS/TPS,能够确保将吞吐量控制在一个固定值上,但这样还远远不够,实际工作中我们希望在每次压测执行时能够随时动态调节吞吐量,这样的功能该如何实现呢?
BeanShell 解释器有一个非常有用的特性——它可以充当服务器,支持可以通过 telnet 或 http 访问。
使用时在 jmeter.properties 中定义以下内容:
beanshell.server.port=9000
beanshell.server.file=../extras/startup.bsh
基于吞吐量控制器,将吞吐量限制值设为占位符(如 ${__P(throughput, 99999999)},throughput 就是占位符),利用 JMeter 的 BeanShell 功能,通过执行外部命令的方式,在运行时注入具体值,达到动态调节吞吐量的目的。编写 update.bsh BeanShell 脚本为:
import org.apache.jmeter.util.JMeterUtils;

getprop(p){ // get a JMeter property
    return JMeterUtils.getPropDefault(p,"");
}

setprop(p,v){ // set a JMeter property
    print("Setting property '"+p+"' to '"+v+"'.");
    JMeterUtils.getJMeterProperties().setProperty(p, v);
}

setprop("throughput", args[0]);
通过运行命令动态调整TPS
sudo java -jar /<jmeter_home>/lib/bshclient.jar localhost 9000 throughput.bsh <参数>

 

结果收集

BackendListener

BackendListener是一种异步监听并获取到测试结果的监听器,也就是说测试结果(如HTTP请求的响应结果)都会被封装在SampleResult对象中并被其实现类监听接收。
public interface BackendListenerClient {
    //开启多线程执行压测之前,传入线程上下文,进行前置处理
    void setupTest(BackendListenerContext context) throws Exception; // NOSONAR

    //多线程压测过程中获取到测试结果集,传入当前方法进行处理
    void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context);

    //多线程压测结束之后进行一个后置处理
    void teardownTest(BackendListenerContext context) throws Exception;
}
通过实现上面 BackendListenerClient 接口来将异步获取到的测试结果SampleResult进行相应处理,然后将元数据上报至kakfa,最后压测聚合模块通过消费kafka Topic异步来收集测试结果。
 

报告聚合

由于压测平台自己实现了分布式压测模式,因此在拿到每台压测机的JTL结果文件后,需要自行对这些JTL文件的内容进行合并和解析,并持久化记录下来。
一个JTL文件的片段
timeStamp,elapsed,label,responseCode,responseMessage,threadName,dataType,success,failureMessage,bytes,sentBytes,grpThreads,allThreads,URL,Latency,IdleTime,Connect
1685412971451,15,1111,200,OK,测试场景1-0 1-1,text,true,,301,868,1,1,http://localhost:40050/grpc-gw-push/pushSync,15,0,0
1685412971821,14,1111,200,OK,测试场景1-0 1-1,text,true,,301,868,1,1,http://localhost:40050/grpc-gw-push/pushSync,14,0,0
1685412972017,16,1111,200,OK,测试场景1-0 1-1,text,true,,301,868,1,1,http://localhost:40050/grpc-gw-push/pushSync,15,0,0
JTL 文件对应每一行的单条结果数据的大小很小(大约只有 100 多个字节),但总量很大,如果我们只是简单的将所有数据存储起来,将会占用大量的存储空间,因此结果数据需要做预聚合再存入。
通常以label(JTL 中的 label)作大分类,维度(errorMsg、errorCode 等)作小分类,以时间作为聚合标准,聚合时间随压测时长调整,从而保证存储大小不会过大,又可以汇总比较丰富的数据。
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
2
2