在以往的性能测试中,我一般都是先将测试数据保存,然后等测试完成之后再进行数据统计和出图展示,既减少了用例运行时资源消耗,也能对测试数据进行二次分析。
单这种模式下无法对测试过程进行监控,有时候运行用例的时候,会有长达数分钟的真空期。有点难熬,所以前段时间增加了一个性能测试中异步展示测试进度的功能。
在某次思考人生的时候突然从JMeter
取样器sampler
得到了灵感,我要是也能实时获取当前系统的QPS
处理能力的数据的话,既可以提前预估到本次测试结果QPS
的数值,也能观察到QPS
在整个过程中变化的曲线,如果不符合标准压测模型的话,还可以辅助排查瓶颈,可谓一举多得。
说干就干,本来想重新写一个异步类来完成这个功能,但是写完发现功能和之前写过的进度条功能类重合度太高了,最终决定把功能整合在一个类中,在检测进度条的时候也输出当前系统QPS
。
这次对进度条类Progress
进行了功能丰富,改动较大,所以这次把代码都贴过来了。
package com.funtester.frame.execute;
import com.funtester.base.constaint.FixedQpsThread;
import com.funtester.base.constaint.ThreadBase;
import com.funtester.base.constaint.ThreadLimitTimeCount;
import com.funtester.base.constaint.ThreadLimitTimesCount;
import com.funtester.base.exception.ParamException;
import com.funtester.config.HttpClientConstant;
import com.funtester.frame.SourceCode;
import com.funtester.utils.StringUtil;
import com.funtester.utils.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* 用于异步展示性能测试进度的多线程类
*
* @param <F> 多线程任务{@link ThreadBase}对象的实现子类
*/
public class Progress<F extends ThreadBase> extends SourceCode implements Runnable {
private static Logger logger = LoggerFactory.getLogger(Progress.class);
/**
* 会长
*/
private static final String SUFFIX = "QPS变化曲线";
/**
* 记录每一次获取QPS的值,可能用于结果展示
*/
public List<Integer> qs = new ArrayList<>();
/**
* 多线程任务类对象
*/
private List<F> threads;
/**
* 线程数,用于计算实时QPS
*/
private int threadNum;
/**
* 进度条的长度
*/
private static final int LENGTH = 67;
/**
* 标志符号
*/
private static final String ONE = getPart(3);
/**
* 总开关,是否运行,默认true
*/
private boolean st = true;
/**
* 是否次数模型
*/
private boolean isTimesMode;
/**
* 用于区分固定QPS请求模型,这里不计算固定QPS模型中的实时QPS
*/
private boolean canCount;
/**
* 多线程任务基类对象,本类中不处理,只用来获取值,若使用的话请调用clone()方法
*/
private F base;
/**
* 在固定QPS模式中使用
*/
private AtomicInteger excuteNum;
/**
* 限制条件
*/
private int limit;
/**
* 非精确时间,误差可以忽略
*/
private long startTime = Time.getTimeStamp();
/**
* 描述
*/
private String taskDesc;
/**
* 固定线程模型
*
* @param threads
* @param desc
*/
public Progress(final List<F> threads, String desc) {
this.threads = threads;
this.threadNum = threads.size();
this.taskDesc = desc;
this.base = threads.get(0);
init();
}
/**
* 适配固定QPS模型
*
* @param threads
* @param desc
* @param excuteNum
*/
public Progress(final List<F> threads, String desc, final AtomicInteger excuteNum) {
this.threads = threads;
this.threadNum = threads.size();
this.taskDesc = desc;
this.base = threads.get(0);
init();
}
/**
* 初始化对象,对istimesMode和limit赋值
*/
private void init() {
if (base instanceof ThreadLimitTimeCount) {
this.isTimesMode = false;
this.canCount = true;
this.limit = ((ThreadLimitTimeCount) base).time;
} else if (base instanceof ThreadLimitTimesCount) {
this.isTimesMode = true;
this.canCount = true;
this.limit = ((ThreadLimitTimesCount) base).times;
} else if (base instanceof FixedQpsThread) {
FixedQpsThread fix = (FixedQpsThread) base;
this.canCount = false;
this.isTimesMode = fix.isTimesMode;
this.limit = fix.limit;
} else {
ParamException.fail("创建进度条对象失败!");
}
}
@Override
public void run() {
double pro = 0;
while (st) {
sleep(HttpClientConstant.LOOP_INTERVAL);
pro = isTimesMode ? base.executeNum == 0 ? FixedQpsConcurrent.executeTimes.get() * 1.0 / limit : base.executeNum * 1.0 / limit : (Time.getTimeStamp() - startTime) * 1.0 / limit;
if (pro > 0.95) break;
if (st)
logger.info("{}进度:{} {} ,当前QPS: {}", taskDesc, getManyString(ONE, (int) (pro * LENGTH)), getPercent(pro * 100), getQPS());
}
}
/**
* 获取某一个时刻的QPS
*
* @return
*/
private int getQPS() {
int qps = 0;
if (canCount) {
List<Integer> times = new ArrayList<>();
for (int i = 0; i < threadNum; i++) {
List<Integer> costs = threads.get(i).costs;
int size = costs.size();
if (size < 3) continue;
times.add(costs.get(size - 1));
times.add(costs.get(size - 2));
}
qps = times.isEmpty() ? 0 : (int) (1000 * threadNum / (times.stream().collect(Collectors.summarizingInt(x -> x)).getAverage()));
} else {
qps = excuteNum.get() / (int) (Time.getTimeStamp() - startTime);
}
qs.add(qps);
return qps;
}
/**
* 关闭线程,防止死循环
*/
public void stop() {
st = false;
logger.info("{}进度:{} {}", taskDesc, getManyString(ONE, LENGTH), "100%");
printQPS();
}
/**
* 打印QPS变化曲线
*/
private void printQPS() {
int size = qs.size();
if (size < 5) return;
if (size <= BUCKET_SIZE) {
output(StatisticsUtil.draw(qs, StringUtil.center(taskDesc + SUFFIX, size * 3)) + LINE + LINE);
} else {
double v = size * 1.0 / BUCKET_SIZE;
List<Integer> qpss = range(BUCKET_SIZE).mapToObj(x -> qs.get((int) (x * v))).collect(Collectors.toList());
output(StatisticsUtil.draw(qpss, StringUtil.center(taskDesc + SUFFIX, BUCKET_SIZE * 3) + LINE + LINE));
}
}
}
随便写了一个内部类,随机休眠的方式重写了doing()
方法。
package com.funtester.groovy
import com.funtester.base.constaint.ThreadBase
import com.funtester.base.constaint.ThreadLimitTimesCount
import com.funtester.frame.SourceCode
import com.funtester.frame.execute.Concurrent
import com.funtester.utils.StringUtil
class WebT extends SourceCode {
static void main(String[] args) {
def ts = []
10.times {
ts << new FunTester(StringUtil.getString(10), 400)
}
new Concurrent(ts, "FunTester测试进度条取样器").start()
}
private static class FunTester extends ThreadLimitTimesCount<String> {
FunTester(String s, int times) {
super(s, times, null)
}
@Override
protected void doing() throws Exception {
sleep(0.01 + getRandomDouble())
}
@Override
ThreadBase clone() {
new FunTester(StringUtil.getString(10), times)
}
}
}
省略了无关内容。
INFO-> 当前用户:fv,IP:10.60.193.37,工作目录:/Users/fv/Documents/workspace/funtester/,系统编码格式:UTF-8,系统Mac OS X版本:10.16
INFO-> FunTester测试进度条取样器进度:▍ 2.25% ,当前QPS: 14
INFO-> FunTester测试进度条取样器进度:▍▍▍ 4.75% ,当前QPS: 22
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍ 8% ,当前QPS: 25
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍ 10.25% ,当前QPS: 16
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍ 12.5% ,当前QPS: 18
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍ 14.5% ,当前QPS: 18
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍ 16.75% ,当前QPS: 21
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍ 19% ,当前QPS: 19
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 21.5% ,当前QPS: 21
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 23.75% ,当前QPS: 21
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 26.75% ,当前QPS: 25
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 29.75% ,当前QPS: 28
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 31.75% ,当前QPS: 18
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 34% ,当前QPS: 22
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 36.25% ,当前QPS: 22
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 38.25% ,当前QPS: 18
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 40.25% ,当前QPS: 18
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 42.5% ,当前QPS: 21
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 45.25% ,当前QPS: 19
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 47% ,当前QPS: 24
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 49.5% ,当前QPS: 25
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 51.75% ,当前QPS: 17
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 54.25% ,当前QPS: 16
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 56.75% ,当前QPS: 19
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 59.75% ,当前QPS: 17
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 62% ,当前QPS: 25
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 64.25% ,当前QPS: 16
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 66.75% ,当前QPS: 23
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 69% ,当前QPS: 20
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 71.5% ,当前QPS: 19
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 74.25% ,当前QPS: 18
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 77.5% ,当前QPS: 27
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 80.75% ,当前QPS: 24
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 83.25% ,当前QPS: 16
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 86% ,当前QPS: 23
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 88% ,当前QPS: 18
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 90.25% ,当前QPS: 21
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 93.5% ,当前QPS: 21
INFO-> 线程:FunTester测试进度条取样器2,执行次数:400,错误次数: 0,总耗时:198.109 s
INFO-> 线程:FunTester测试进度条取样器0,执行次数:400,错误次数: 0,总耗时:200.369 s
INFO-> 线程:FunTester测试进度条取样器1,执行次数:400,错误次数: 0,总耗时:204.173 s
INFO-> 线程:FunTester测试进度条取样器3,执行次数:400,错误次数: 0,总耗时:205.531 s
INFO-> 线程:FunTester测试进度条取样器5,执行次数:400,错误次数: 0,总耗时:206.551 s
INFO-> 线程:FunTester测试进度条取样器8,执行次数:400,错误次数: 0,总耗时:208.543 s
INFO-> 线程:FunTester测试进度条取样器4,执行次数:400,错误次数: 0,总耗时:208.618 s
INFO-> 线程:FunTester测试进度条取样器9,执行次数:400,错误次数: 0,总耗时:208.856 s
INFO-> 线程:FunTester测试进度条取样器6,执行次数:400,错误次数: 0,总耗时:209.112 s
INFO-> 线程:FunTester测试进度条取样器7,执行次数:400,错误次数: 0,总耗时:211.758 s
INFO-> FunTester测试进度条取样器进度:▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍▍ 100%
INFO->
FunTester测试进度条取样器QPS变化曲线
图片往下看
INFO-> 总计10个线程,共用时:211.762 s,执行总数:4000,错误数:0,失败数:0
INFO-> 数据保存成功!文件名:/Users/fv/Documents/workspace/funtester/long/data/FunTester测试进度条取样器181550_10
INFO->
~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~ JSON ~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~
> {
> ① . "rt":515,
> ① . "total":4000,
> ① . "qps":19.417,
> ① . "failRate":0.0,
> ① . "threads":10,
> ① . "startTime":"2021-03-18 15:50:18",
> ① . "endTime":"2021-03-18 15:53:50",
> ① . "errorRate":0.0,
> ① . "executeTotal":4000,
> ① . "mark":"FunTester测试进度条取样器181550",
> ① . "table":"省略压缩字符串"
> }
~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~ JSON ~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~
INFO->
FunTester测试进度条取样器 10 thread
Response Time: x-serial num, y-median
min median:30 ms,max:995 ms
图片往下看
Process finished with exit code 0