性能測試框架中QPS取樣器實現

在以往的性能測試中,我一般都是先將測試數據保存,然後等測試完成之後再進行數據統計和出圖展示,既減少了用例運行時資源消耗,也能對測試數據進行二次分析。

但這種模式下無法對測試過程進行監控,有時候運行用例的時候,會有長達數分鐘的真空期。有點難熬,所以前段時間增加了一個性能測試中異步展示測試進度的功能。

在某次思考人生的時候突然從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 ThreadBaseextends 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.95break;
            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 < 3continue;
                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 < 5return;
        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,歡迎關注。

點擊閱讀原文,查看公衆號歷史文章
- END -


本文分享自微信公衆號 - FunTester(NuclearTester)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章