FunTester 性能测试框架中 QPS 取样器实现

FunTester · 2021年03月19日 · 888 次阅读

在以往的性能测试中,我一般都是先将测试数据保存,然后等测试完成之后再进行数据统计和出图展示,既减少了用例运行时资源消耗,也能对测试数据进行二次分析。

单这种模式下无法对测试过程进行监控,有时候运行用例的时候,会有长达数分钟的真空期。有点难熬,所以前段时间增加了一个性能测试中异步展示测试进度的功能。

在某次思考人生的时候突然从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

FunTester测试进度条取样器QPS变化曲线

FunTester测试进度条取样器 10 thread


FunTester腾讯云年度作者Boss 直聘签约作者,非著名测试开发 er,欢迎关注。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册