对一些概念的理解和思路
1.基准场景,指单线程或少量线程1次性对单接口进行测试,然后将测试结果作为基准数据,为后面制定并发数提供决策数据。
有几个作用:
1) 验证测试脚本及测试参数的正确性,同时也可以验证脚本数据是否能够支持重复性测试等;
2) 通过少量线程访问系统获取结果数据,作为对比参考基准;
3) 根据测试结果,初步盘点可能成为系统瓶颈的场景,并决定是否进行后续的测试;
2.单接口负载场景,通过模拟多线程对单接口进行负载(压力从字面意思来讲就是给某物一定的,比如用手按压桌子等。压力测试就是给被测系统不断的加压(负载),测试系统的各项性能指标。直到系统的某一些性能指标达到了瓶颈为止,比如CPU的使用率达到了100%。这个不断给系统施加压力的过程,就叫做压力测试。然后,负载测试就是在系统性能指标已经有一项或几项达到了瓶颈的情况下,让它继续运行一段时间,测试系统运行过程中各项性能指标。这种在已经达到了最大负载(压力)时进行的测试叫负载测试。最后,总的来说,压力测试就是寻找系统性能瓶颈的,负载测试就是测试系统在处于瓶颈的状态的运行情况,比如响应时间是否延长等。)测试。
3.混合场景负载测试,是为了最大程度模拟用户真实操作。真实的线上操作不只有单接口的操作,一定是多业务同时进行,比如张三在浏览商品,李四在添加购物车等。
4.性能测试结果,主要查看的指标响应时间、tps(在jmeter中,大多数情况(未有错误时)下,Throughput吞吐率被认为等于TPS)、cpu占用率、错误率
5. 同步定时器,可以实现绝对的并发,持续测试时,会一直保持并发数为想要的值,比如一次请求10个并发,会等这10个并发处理全部结束后,再次发起10个并发,但实际上程序处理时基本不存在持续的绝对并发。要看需求要求,这种一般用在混合场景测试,比如10个人登录后,要求测试10个人同时添加购物车操作时,可以使用同步定时器。但如果对单接口测试时,需求没有要求下,无需使用同步定时器,以达到真实请求的效果。(例如开发的需求是,他想知道同时启用几个线程,请求ai识别接口,响应不会超时,报错等,这个时候是无需使用同步定时器,才能真实模拟他想要的效果)
例子:Ai接口性能测试(传入羊只的图片过去,然后获得图片中羊的重量和只数,还有原图被标注羊后返回)
基准场景测试,
1个线程请求,平均响应时间大概在3s左右;
3个线程1s内并发请求,平均响应时间大概在4s;
5个线程1s内并发请求,平均响应时间大概在6s;
10个线程1s内并发请求,平均响应时间大概在12s;
20个线程1s内并发请求,平均响应时间大概在26s;
30个线程1s内并发请求,平均响应时间大概在42s;
我们程序可接受的响应时间在30s内,是定时任务执行,所以,30s是我们一个参考值,通过基准场景,可以看出,在20个并发接近30s,但是系统cpu没有达到很大压力,这个时候,可以以响应时间做一个基准,去测试我们想要的合适的并发数;
对20个并发,进行负载测试,持续10分钟,观察响应时间,经过测试结果如下
通过报告,可以看出,95%的接口响应时间都小于30s,5%的接口响应时间可能会大于30s,跟开发讨论,20个并发基本是满足我们系统的最大并发数了;这个时候,20个并发可以多测试几轮10分钟,15分钟,以保证数据的准确性。
在来看下tps,因为请求都没有报错,这里就默认吞吐量等于tps,通过不断增加线程数,每个线程持续10分钟压测,得到如下图(横轴是线程数,纵轴是吞吐量)
由此看出,11个并发是1个拐点,大于11个并发,ai系统处理请求的能力开始下降。
至此,目前本次性能测试结束。
在测试过程中遇到的问题总结
1. 文件上传或者是图片上传时,大多数时候都需要转换成base64,这个时候,如果直接使用jmeter的http请求参数会很大,就会出现卡死的情况。百度尝试各种方法后,发现可是使用java请求来避免这个问题。
新增java请求步骤如下:
1.1 使用idea新建一个maven工程
1.2新增后,copy代码,可以百度,这次的代码如下:
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.protocol.java.sampler.AbstractJavaSamplerClient;
import org.apache.jmeter.protocol.java.sampler.JavaSamplerContext;
import org.apache.jmeter.samplers.SampleResult;
import sun.misc.BASE64Encoder;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class AiTest extends AbstractJavaSamplerClient {
@Override
public Arguments getDefaultParameters() {
//设置请求参数名
Arguments arguments = new Arguments();
arguments.addArgument("url","");//请求的接口地址
arguments.addArgument("filePath","");//传入图片本地地址
return arguments;
}
@Override
public SampleResult runTest(JavaSamplerContext javaSamplerContext) {
//获取传入的请求参数
String url = javaSamplerContext.getParameter("url");
String filePath = javaSamplerContext.getParameter("filePath");
SampleResult sr = new SampleResult();// 初始化一个SampleResult
sr.setSamplerData("请求参数url:" + url + "\n请求参数filePath:" + filePath);// 参数放进sr
try {
sr.sampleStart(); // jmeter 开始统计响应时间标识
String file=getImgData(filePath);//将通过地址获取图片并转换为base64
Map
param.put("file", file);
param.put("deviceId", 1);
String paramStr = JSON.toJSONString(param);
String post = HttpUtil.post(url, paramStr);
sr.setResponseData("响应结果是:" + post);// 结果放入sr,并设置字符集
sr.setDataType(SampleResult.TEXT);
sr.setSuccessful(true); // 设置响应执行成功
} catch (Throwable e) {
sr.setSuccessful(false);// 有异常,执行失败
e.printStackTrace();
} finally {
sr.sampleEnd(); // jmeter 结束统计响应时间标识
}return sr;}
// 图片转化成base64字符串
public static String getImageStr(String file) {
// 将图片文件转化为字节数组字符串,并对其进行Base64编码处理
String imgFile = file; // 待处理的图片
InputStream in = null;
byte[] data = null;
// 读取图片字节数组
try {in =new FileInputStream(imgFile);
data = new byte[in.available()];
in.read(data);
in.close();}catch (IOException e){
e.printStackTrace();
}
// 对字节数组Base64编码
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(data);// 返回Base64编码过的字节数组字符串
}
public static String getImgData(String filePath) {
// windows平台需要转换‘’该转义符
String filedata = filePath.replaceAll("%2F%2F", "%2F");
// 因编码出来不是在一行上,所以需要处理掉换行符
String data =getImageStr(filedata).replaceAll("\n", "");
return data;
}
public static void main(String[] args){
// 本地测试
String url="http://goatapi-test.shuyixin.cn/syx/weighingdata";
String filePath="D:\\work\\项目\\大足黑山羊\\ai接口性能测试\\goat\\310.jpg";
String file=getImgData(filePath);
Map
param.put("file", file);
param.put("deviceId", 1);
String paramStr = JSON.toJSONString(param);
String post = HttpUtil.post(url, paramStr);
System.out.println("响应结果:"+post);
}}
1.3 导入需要的jar包
导入完成后,会在这里看到
1.4 运行main方法,测试下
运行后,若发现会报错
百度后,需要引slf4j的包,这个时候,在pom里面添加
修改pom文件后,会提示如下,点击import Changes
完成后,会看到
再次运行main测试一下,可以正常请求成功;
1.5 测试成功后,导出jar包
有两种方式,1种是使用maven工具,1种是手动打包,手动打包
maven打包
手动打包
移除掉jmeter相关的包
复制jar包到jmeter,/lib/ext 目录下
启动jmeter,新增一个Java请求,选择自己创建的类名,填写参数即可
2. 持续压测时,设置了几个线程数,就从csv里面重复请求几个线程,而没有往下走。
后面通过百度和不断的尝试,发现是java请求打包的问题,因为打包时没有去掉jmeter相关的核心包导致这个问题,后面重新打包,问题解决。
3.持续压测时,需要大量的不同的图片。
使用java,在图片上加水印的方式,可以批量生成大量不同的图片。可以在刚刚新建的项目里面,新增一个类,copy代码如下(百度也可以),然后执行main方法(需要修改的只有原图片地址和目标图片地址):
package com.zds.autotest.agriculture.process;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
@Slf4j
public class WaterMarkUtils {
/**
*编辑图片,往指定位置添加文字
*@param srcImgPath :源图片路径
*@param targetImgPath :保存图片路径
*@param list :文字集合
*/
public static void writeImage(String srcImgPath, String targetImgPath, List list) {
FileOutputStream outImgStream =null;
try{
//读取原图片信息
File srcImgFile = new File(srcImgPath);//得到文件
Image srcImg = ImageIO.read(srcImgFile);//文件转化为图片
int srcImgWidth = srcImg.getWidth(null);//获取图片的宽
int srcImgHeight = srcImg.getHeight(null);//获取图片的高
//添加文字:
BufferedImage bufImg = new BufferedImage(srcImgWidth, srcImgHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g = bufImg.createGraphics();
g.drawImage(srcImg, 0, 0, srcImgWidth, srcImgHeight, null);
for(ImageDTO imgDTO : list) {
g.setColor(imgDTO.getColor()); //根据图片的背景设置水印颜色
g.setFont(imgDTO.getFont()); //设置字体
g.drawString(imgDTO.getText(), imgDTO.getX(), imgDTO.getY()); //画出水印
}
g.dispose();
// 输出图片
outImgStream = new FileOutputStream(targetImgPath);
ImageIO.write(bufImg,"jpg", outImgStream);
} catch (Exception e) {
log.error("==== 系统异常::{} ====",e);
}finally {
try {if (null != outImgStream){
outImgStream.flush();
outImgStream.close();
} }catch (IOException e) {
e.printStackTrace();}}
}
/**
*创建ImageDTO, 每一个对象,代表在该图片中要插入的一段文字内容:
*@param text :文本内容;
*@param color : 字体颜色(前三位)和透明度(第4位,值越小,越透明);
*@param font :字体的样式和字体大小;
*@param x :当前字体在该图片位置的横坐标;
*@param y :当前字体在该图片位置的纵坐标;
*@return
*/
private static ImageDTO createImageDTO(String text,Color color,Font font,int x,int y){
ImageDTO imageDTO =new ImageDTO();
imageDTO.setText(text);
imageDTO.setColor(color);
imageDTO.setFont(font);
imageDTO.setX(x);
imageDTO.setY(y);
return imageDTO;
}
/**
* main方法:
*@param args
*/
public static void main(String[] args) {
//todo 自己真实的本地地址:
String srcImgPath="D:\\work\\项目\\大足黑山羊\\ai接口性能测试\\goat\\1000.jpg"; //源图片地址
String tarImgPath="D:\\work\\项目\\大足黑山羊\\ai接口性能测试\\goat\\10.jpg"; //目标图片的地址
for (int i=14078;i<=16720;i++){
tarImgPath="D:\\work\\项目\\大足黑山羊\\ai接口性能测试\\goat\\"+i+".jpg"; //目标图片的地址
//获取数据集合;
ArrayList
list.add(createImageDTO(String.valueOf(i),new Color(255,59,48),new Font("微软雅黑", Font.PLAIN, 36), 350, 366));
//操作图片:
WaterMarkUtils.writeImage(srcImgPath, tarImgPath, list);
}
log.info("生成完成");
//这句代码,自己项目中可以不用加,在这里防止main方法报错的;
// System.exit(0);
}
}
/**
*存放文本内容的类
*/
@Setter
@Getter
class ImageDTO{
//文字内容
private String text;
//字体颜色和透明度
private Color color;
//字体和大小
private Font font;
//所在图片的x坐标
private int x;
//所在图片的y坐标
private int y;
}