概念介绍
测试计划
测试计划是JMeter脚本的根元素,其中可以包含其他元件,例如,线程、配置元素、定时器、前置处理器、后置处理器、断言以及监听器。同时,测试计划也提供
了一小部分自己的配置。首先,可以定义用于脚本中的用户变量(键_值对)。然后,可以配置测试计划所含的线程组应该如何运行,即,指定线程组是否同时运行。随着时间的推移,一个测试计划通常会有多个线程组。通过这个选项决定线程组如何运行。默认所有线程组同时运行。刚开始的时候一个比较有用的选项是Functional Test Mode。检查结束时,每个样本返回的服务器响应都会被记录下来。对于小的模拟运行,确保JMeter设置正确且服务器端返回了预期的结果是非常有用的,但是不好的地方是JMeter的性能会恶化且文件非常大。这个选项默认是关闭(off) 的,在模拟真正的测试场景时通常是不会选中的。另一个比较有用的选择是添加第三方库的能力,这可用于为测试用例提供额外的功能。当你的模拟测试场景需要JMeter默认自带库以外的其他库时就用到这个选项了。通常,你都会通过这个选项来添加JAR文件。
线程组
线程组是所有测试计划的入口点。它们代表了JMeter用于运行测试计划的线程用户的数量。一个测试的所有控制器和采样器都必须放在线程组下面。在你想把其他元素(如 监听器)应用于所有 线程组时,可以直接把这些元素 放在测试计划下,如果它们只应用于一个线程组,就放在指定的线程组下。线程组设置提供的选项包括指定用于测试计划的线程个数、所有线程启动要花的时间以及测试将会运行的次数。每一个线程都将完全独立于其他线程运行测试计划。JMeter将 多个线程拆分开来模拟对服务器的并发连接。注意,启动所有线程的时间应该设置足够长以避免在测试开始时负载过大,过大的负载可能导致网络饱和,测试结果失败。如果你希望模拟系统中的X个活跃用户,最好慢慢提升并增加迭代次数。最后一个设置选项是调度器。通过调度器可以设置测试执行的开始时间和结束时间。比如,可以在非高峰时段启动测试,可以精确到小时。
控制器
控制器负责驱动测试的运行,包括取样控制器和逻辑控制器。一方面,取样控制器向服务器端发送请求。其中包括HTTP、FTP、JDBC、LDAP等请求。尽管JMeter有一系列取样器,但因为我们主要关注Web应用的测试。另一方面,逻辑控制器可以定制用于发送请求的逻辑。例如,循环控制器可以用于重复执行指定次数的操作,选择控制器可以用于选择性执行请求,同时条件控制器会持续执行请求,直到某些条件不满足等。
取样器用于向服务器发送请求并等待响应。所有树上的请求会依次处理。JMeter包含以下类型的取样器:
●HTTP请求;
●JDBC请求;
●L DAP请求;
●SOAP/XML- RPC请求;
●WebService (SOAP) 请求;
●FTP请求。
这些取样器的属性可以根据需求进一步调整。多数情况下,可以使用默认的配置。为取样器增加断言,可以对服务器响应进行基本验证。通常在测试过程中,服务器端都会返回状态码200,代表请求是成功的,但是可能无法正确显示页面。在这种情况下,断言可以帮助你确保请求是成功的。
逻辑控制器
逻辑控制器帮助我们定制用于决定请求如何发往服务器的逻辑。其中可能涉及修改请求,重复发送请求,交错发送请求,控制请求执行的时间,切换请求,测量请求执行所花的总时间等。请参考Apache网站上的JMeter在线用户指南来了解每一种逻辑控制器的详细说明。
测试块
测试块是专门用于测试计划中代码重用的一种特殊的控制器。它们在测试计划树上与线程组处于同一级,除非被包含或被模块控制器引用,否则它们不会执行。
监听器
监听器主要用于收集用于进一步分析的测试执行结果。此外,监听器也可将数据直接写入文件,供下一步使用。此外,监听器还可以定义保存哪些字段以及使用CSV格式还是XML格式。所有监听器都会保存相同的数据,唯一不同的是展现在屏幕上的方式。监听器可以在测试的任何地方添加,包括直接加在测试计划下面。监听器
只会收集同级或下级元素的数据。针对各种不同的用途,JMeter包含 了18种不同的监听器。虽然你可能经常只会用到其中的一部分,但是建议你熟悉它们,知道在什么时候使用。
定时器
默认JMeter线程组在发送每个请求的过程中不会暂停。建议为线程组添加一个定时器,用于指定一个短暂的延时。这将使测试计划更接近实际使用场景,真正的用户不可能同时发送多个请求。定时器用于使JMeter在发送每个请求前暂停固定的时间。
断言
断言用于判断从服务器端接收到的响应。从本质上说,通过断言可以判断应用的功能是否正确以及服务器端是否返回预期结果。断言可在XML、JSON、 HTTP以及从服务器端返回的其他形式的响应上运行。因为断言也会占用大量资源,所以在实际测试运行期间不要使用断言。
配置元件
配置元件通常和取样器起使用, 可以修改或添加请求。 只有在放置元件的分支里面,才能访问树中的配置元件。配置元素包括HTTP Cookie管理器、HTTP头管理器等。
前置处理器和后置处理器
顾名思义,前置处理器在发出请求之前会执行一系列操作。前置处理器通常用于在请求执行前修改请求设置或更新那些不是从响应文本中提取出来的变量。后置处理器会在发出请求后执行一系列操作。后置处理器通常用于操作响应文本并从中提取相关数值。
jmeter入门
首先打开Jmeter软件。
jmeter_gui
添加线程组
所有的测试工作都是从新建一个线程组开始的。
它的作用其实是为了模拟用户,所以也叫Users。一个线程组模块可以包含多个线程,每个线程代表一个用户,这样可以模拟高并发下的请求,并根据网站的响应信息来判断网站的相关性能。
线程组包含很多属性,目前我们只关注线程属性那一块。其中线程数代表访问的并发数,默认是1。Ramp-UpPeriod表示多长时间内容启动所有线程,如果时间很短,会造成网站的瞬间高并发,默认值是1秒。循环次数是表示执行多少次,默认值为1,表示执行一次结束,这里可以勾选永远,让其一直运行下去。
这些属性暂时不用动,因为还没有将工程配置好,测试工程配置的时候使用单次测试容易排查问题,以后压力测试直接修改该面板的值即可。
添加HTTP请求
因为是HTTP接口,这里添加一个HTTP请求,用来访问网站的API接口。
HTTP请求面板主要的目的是设置测试时候HTTP请求的相关信息,模拟浏览器访问或者其他程序访问后台的相关配置。
该面板主要的配置包括协议、服务器IP、端口、方法、路径和参数等内容,接下来可以将测试样例的相关信息填入。
这里测试使用的是淘宝IP地址库,首页有RestAPI接口的测试接口说明:
1.请求接口(GET):
/service/getIpInfo.php?ip=[ip地址字串]
2.响应信息:
(json格式的)国家、省(自治区或直辖市)、市(县)、运营商
3.返回数据格式:
{"code":0,"data":{"ip":"210.75.225.254","country":"\u4e2d\u56fd","area":"\u534e\u5317",
"region":"\u5317\u4eac\u5e02","city":"\u5317\u4eac\u5e02","county":"","isp":"\u7535\u4fe1",
"country_id":"86","area_id":"100000","region_id":"110000","city_id":"110000",
"county_id":"-1","isp_id":"100017"}}
其中code的值的含义为,0:成功,1:失败。
因为是GET请求,所以具体内容填写如下:
该接口的参数比较简单,只有一个ip参数,如果复杂的可以添加多个或者直接在路径后面添加也可,例如:/service/getIpInfo.php?ip=xxx.xxx.xxx.xxx。
添加结果树
现在基本配置已经OK,但是这样执行后返回的结果却没有地方查看。为了方便查看结果,这里添加ViewResultsTree面板,有很多其它的结果查看面板,大家可以自己尝试一下。
从不同使用场景思考如何使用
如果想对压测结果进行数据分析怎么办?
只需要将结果导出为excel即可,所以问题变成了如何将压测结果导出为excel,如何添加 beanshell 取样器
添加bean shell取样器
运行,循环写入数据
某些情况不知道怎么配置测试计划怎么办,怎么办,比如要对zookeeper压测,或者嫌jmeter这些概念太复杂?
自己编写jmeter脚本
建立maven项目
添加maven依赖:
4.0.0
com.soybean
redpacketJmeter
0.0.1-SNAPSHOT
org.apache.jmeter
ApacheJMeter_java
4.0
org.apache.jmeter
ApacheJMeter_java
4.0
com.alibaba
fastjson
1.2.37
com.soybean
base-utils
0.0.1-SNAPSHOT
org.apache.maven.plugins
maven-compiler-plugin
3.1
1.8
org.apache.maven.plugins
maven-jar-plugin
target/classes/
com.soybean.Application
false
true
lib/
.
org.apache.maven.plugins
maven-dependency-plugin
copy-dependencies
package
copy-dependencies
jar
jar
${project.build.directory}/lib
建立执行类
新建一个类,类名为BaseJmeterHttp,该类继承AbstractJavaSamplerClient类,AbstractJavaSamplerClient存在于ApacheJMeter_java.jar这个JAR包中,引用即可调用。
BaseJmeterHttp类在继承AbstractJavaSamplerClient类的时候,需要实现四个方法,分别是
setupTest():初始化方法,用于初始化性能测试时的每个线程;
getDefaultParameters():主要用于设置传入的参数;
runTest():为性能测试时的线程运行体;
teardownTest():测试结束方法,用于结束性能测试中的每个线程。
具体代码:
package com.soybean.test.http;
import com.soybean.common.logger.DushuLogger;
import com.soybean.test.Entity.Packet;
import com.soybean.test.Entity.PacketShareUrlVO;
import com.soybean.test.util.HttpClientUtils;
import com.soybean.test.util.HttpResult;
import com.soybean.test.util.JsonUtil;
import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient;
import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.threads.JMeterVariables;
import java.util.*;
import java.util.concurrent.*;
/**
*
* Description: BaseJmeterHttpImpl
* Company : *********科技有限公司
* Author : WangLei
* Date : 2018/10/15 14:20
* Modify : 修改日期 修改人员 修改说明 JIRA编号
* v1.0.0 2018/10/15 WangLei 新增 1001
********************************************************************/
public class BaseJmeterHttp extends AbstractJavaSamplerClient {
// private String ip ="http://gateway-java-bench.dushu.io";
private final String ip = "http://10.80.60.181:1111";
// 线程数
private final static int THREAD_SIZE = 30;
private final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_SIZE);
@Override
public SampleResult runTest(JavaSamplerContext javaSamplerContext) {
Packet packet = new Packet();
JMeterVariables jMeterVariables = javaSamplerContext.getJMeterVariables();
Iterator> iterator = jMeterVariables.getIterator();
while (iterator.hasNext()) {
Map.Entry entry = iterator.next();//把Object型强转成int型
DushuLogger.info("获取到的参数为:" + JsonUtil.toJSONObject(entry));
if (entry.getKey().equals("senderId")) {
packet.setUserId((String) entry.getValue());
}
if (entry.getKey().equals("orderNo")) {
packet.setOrderNo((String) entry.getValue());
}
if (entry.getKey().equals("token")) {
packet.setToken(entry.getValue().toString().split("-"));
}
if (entry.getKey().equals("getId")) {
packet.setGetterId(entry.getValue().toString().split("-"));
}
packet.setAppId("1001");
}
boolean flag = true;
String errorInfo = "";
SampleResult sr= new SampleResult();
List> futures = new ArrayList<>();
int countSuccess = 0;
try {
// 记录程序执行时间以及执行结果
sr.sampleStart();
DushuLogger.info("接收到的参数:" + JsonUtil.toJSON(packet));
DushuLogger.info("开始创建红包");
HttpResult httpResult = HttpClientUtils.postUrlAsJson(ip + "/redPacket-system/redPacket/generateRedPacket", JsonUtil.toJSONObject(packet));
if (!httpResult.isSuccess()) {
DushuLogger.error("调用生成红包接口失败");
errorInfo = "调用生成红包接口失败";
flag = false;
}
Map map = JsonUtil.fromJSON(httpResult.getResponse(), Map.class);
DushuLogger.info("创建红包返回结果:" + JsonUtil.toJSON(map));
if (map.get("data") == null || !"0000".equals(map.get("status"))) {
DushuLogger.error("创建红包失败");
errorInfo = "调用生成红包接口成功,但是处理失败:" + JsonUtil.toJSON(map) + " ,请求参数为:" + JsonUtil.toJSONObject(packet);
flag = false;
} else {
//领红包
final CountDownLatch latch = new CountDownLatch(THREAD_SIZE);
for (int i = 0; i < THREAD_SIZE; i++) {
PacketShareUrlVO packetShareUrlVO = new PacketShareUrlVO();
packetShareUrlVO.setRedPacketId(Long.parseLong(map.get("data").toString()));
packetShareUrlVO.setAppId(packet.getAppId());
packetShareUrlVO.setToken(packet.getToken()[i]);
packetShareUrlVO.setUserId(packet.getGetterId()[i]);
DushuLogger.info(JsonUtil.toJSON(packetShareUrlVO));
DushuLogger.info(packetShareUrlVO);
Callable task1 = () -> {
String resultInfo = "";
//领红包
HttpResult httpResultSmall = HttpClientUtils.postUrlAsJson(ip + "/redPacket-system/redPacket/bindSmallPacketAndUser", JsonUtil.toJSONObject(packetShareUrlVO));
if (!httpResultSmall.isSuccess()) {
DushuLogger.error("【领红包失败】调用领红包接口失败:" + JsonUtil.toJSONObject(packetShareUrlVO));
resultInfo = "【领红包失败】调用领红包接口失败:" + JsonUtil.toJSONObject(packetShareUrlVO);
}
Map smallPacketMap = JsonUtil.fromJSON(httpResultSmall.getResponse(), Map.class);
if (smallPacketMap.get("status") == null || !"0000".equals(smallPacketMap.get("status"))) {
DushuLogger.error("【领红包失败】调用领红包接口成功,但是处理失败,入参为:" + JsonUtil.toJSONObject(packetShareUrlVO) + "处理结果为:" + JsonUtil.toJSONObject(smallPacketMap));
resultInfo = "【领红包失败】调用领红包接口成功,但是处理失败,入参为:" + JsonUtil.toJSONObject(packetShareUrlVO) + "处理结果为:" + JsonUtil.toJSONObject(smallPacketMap);
} else {
//更新会期
Map addVipParamMap = new HashMap<>();
addVipParamMap.put("token", packetShareUrlVO.getToken());
HttpResult addVipTermResult = HttpClientUtils.postUrlAsJson(ip + "/redPacket-system/redPacket/addVipTerm", addVipParamMap);
if (!addVipTermResult.isSuccess()) {
DushuLogger.error("【更新会期失败】调用更新会期接口失败:" + JsonUtil.toJSONObject(addVipParamMap));
resultInfo = "【更新会期失败】调用更新会期接口失败:" + JsonUtil.toJSONObject(addVipParamMap);
}
Map addVipMap = JsonUtil.fromJSON(addVipTermResult.getResponse(), Map.class);
if (addVipMap.get("status") == null || !"0000".equals(addVipMap.get("status"))) {
DushuLogger.error("【更新会期失败】调用更新会期接口成功,但是处理失败,入参:" + JsonUtil.toJSONObject(addVipParamMap) + "处理结果为:" + JsonUtil.toJSONObject(addVipMap));
resultInfo = "【更新会期失败】调用更新会期接口成功,但是处理失败,入参:" + JsonUtil.toJSONObject(addVipParamMap) + "处理结果为:" + JsonUtil.toJSONObject(addVipMap);
} else {
DushuLogger.info("更新会期成功");
}
}
latch.countDown();
return "".equals(resultInfo) ? 1 : 0;
};
futures.add(executorService.submit(task1));
}
latch.await();
}
//统计结果
for (Future f : futures) {
countSuccess = countSuccess + (int) f.get();
}
DushuLogger.info("【统计结果】一共抢红包的人有:" + THREAD_SIZE + "个,其中成功抢到红包的有:" + countSuccess + "个");
flag = countSuccess == 15;
if (!flag) {
errorInfo =errorInfo + "请求参数为:" + JsonUtil.toJSONObject(packet) +"【统计结果】一共抢红包的人有:" + THREAD_SIZE + "个,其中成功抢到红包的有:" + countSuccess + "个";
}
sr.setSuccessful(flag);
} catch (Exception e) {
sr.setSuccessful(false);
} finally {
sr.sampleEnd();
}
//将数据打印到查看结果树当中
sr.setResponseData(flag ? "红包流程执行成功,共有 " + countSuccess + " 个成功抢到红包,请求参数为:" + JsonUtil.toJSONObject(packet) : "红包流程执行失败,原因:" + errorInfo, null);
sr.setDataType(SampleResult.TEXT);
return sr;
}
}
Jmeter运行分析
1、将上述代码打包成jar包,生成的包名称为redpacketJmeter.jar,将jar包拷贝到Jmeter的安装目录lib/ext下面。
2、把依赖的jar包都拷贝到Jmeter安装目录的lib下,替换原有的同名jar包
2、运行Jmeter,添加线程组及java请求,显示如下:
3、添加监听器,这里我们添加查看结果树和聚合报告就好。
结果显示如下图:
如果一个接口依赖数据库中的正确数据,如何压测,比如压测登录接口?
先使用navcat将数据导出为csv,在使用该csv为入参。所以这里问题就变成了,将csv文件设置为测试计划的动态参数。
准备csv文件
桌面创建一个CityData.csv文件,根据前面接口测试文件,关于天气API三个变量,写入csv文件,最后一栏,中文是我添加的城市名称备注,例如下图
打开JMeter,创建一个Thread Group,设置如下
因为我们8个城市,只有一个Http Request,所以这个地方需要循环测试8次
添加一个CSV Data Set Config,设置如下
设置如下
第一个红圈,文件名要写对路径,第二个红圈是空白的,如果你csv第一行写的变量,就像我上面这样,这个地方就不填写
添加一个HTTP Request,设置如下
通过 ${变量名}来告诉JMeter是从外部文件读取变量
测试,查看结果
点击切换这8个 HTTP Request,看看是不是分别对应csv文件八个城市。JMeter如何通过CSV文件读取变量就介绍到这里。
单机压测的qps没法达到预期,如何使用分布式压测?
充分利用剩余服务器资源,问题就变成了,如何在centos中执行任务
下载:
wget https://mirrors.bfsu.edu.cn/apache//jmeter/binaries/apache-jmeter-5.3.tgz
解压:
tar -xvf apache-jmeter-5.3.tgz
用cli模式运行并加载测试模板,最后生成html报告:
cd apache-jmeter-5.3.tgz && bin/jmeter -n -t Test\ Plan.jmx -l result.jtl -e -o ./html
参数:
-n:以非GUI形式运行Jmeter
-t:运行JMX测试计划脚本文件的路径
-l:运行结果保存路径(.jtl),此文件必须不存在
-j: jmeter运行日志
-r: 运行分布式压测服务器,指明用jmeter属性"remote_hosts"
-R:运行分布式服务器,其后跟着服务器列表
-e:在脚本运行结束后生成html报告,此参数要与-l一起使用。
-o:用于存放html报告的目录,此目录必须为空。此参数与-e一起使用。
测试模板请在本机window下运行jmeter GUI来生成,保存后再上传到测试服务器上。
如果需要同时测几个测试接口太长,接口太复杂 不知道如何建立计划怎么办?
使用chrome浏览器录制工具通过录制功能,不需要自己构建计划
下载插件BlazeMeter,
直接在网上搜索是全英的,推荐大家从一个中文谷歌插件网下载,非常方便,具体方法可以参考这个链接。
https://blog.csdn.net/make_1998/article/details/103499772
下载完将插件文档移入谷歌网页的扩展程序中即可。
进行录制
点击网页右上角的图标,开始Start recording录制开始即可。
转格式
录制完成后的默认格式是.YAML的,需要转换成.JMX格式,这时候就需要登录账号BlazeMeter。由于是谷歌的插件,所以可以用谷歌账号进行登录。登陆后返回BlazeMeter界面,点击JMeter Script,
选择jmx即可
然后将下载的文件导入JMeter,任务完成。
Jmeter详细使用教程下载
https://www.wangdaye.net/upload/2021/11/jmeter%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B-786279483b524a8aabf91908a953da46.zip