FunTester 【连载 21】性能测试实践——超时结账第一回合

FunTester · 2025年03月11日 · 554 次阅读

3.7.1 超市结账第一回合

让我们把目光转回小八超市。最近生意红火,8 个收银台忙得团团转,早高峰时连上厕所的时间都没有。收银员们叫苦不迭,纷纷建议老板临时增加 2 个收银台。小八思前想后,决定先对现有的 8 个收银台进行一次摸底,看看在满负荷运转的情况下,每分钟能结账多少顾客。根据摸底结果,再决定是否增加临时收银台。

以此为背景,我们来设计一个性能测试用例。根据需求分析,我们选择线程模型,也就是排队模型,总并发数量为 8。测试内容就是模拟顾客结账的流程,简化为三个步骤:扫码计价、付款结账和打包走人。为了给收银员留出热身时间,我们设置了 2 分钟的 Rump-Up 时间。

相信大家对这种场景已经驾轻就熟,下面是我设计的多线程类。为了增加一点挑战性,我分别统计了三个步骤的耗时,并且支持异步输出实时信息,这样可以更快定位系统瓶颈,提升排查效率。

既然要增加额外的统计和异步输出功能,必然需要一个额外的线程来完成这个任务。为了避免线程开销,我在 TaskExecutor 类中增加了一个属性 realTimeThread:

/**
 * 实时信息输出线程,用于实时统计一段时间的TPS和平均耗时
 */
public Thread realTimeThread;

这样在 start() 方法中稍加改造即可使用。当 realTimeThread 未赋值时,默认只统计当前实时 TPS 和 RT。

if (realTimeThread == null) realTimeThread = new Thread() {
    @Override
    public void run() {
        while (realTimeKey) {
            // 重复代码,省略
        }
    }
};
realTimeThread.start();

接下来我们编写多线程任务类代码。增加了三个阶段方法以及对应的统计属性。本次数据统计采用了实时 TPS 和 RT 相同的方案,使用一个全局线程安全对象计算总耗时,然后依据实时 TPS 计算平均耗时。

为了增加统计数据的波动性,在 pay() 方法中增加了时间相关变量作为休眠参数。多线程任务类代码如下:

package org.funtester.performance.books.chapter03.section7;

import org.funtester.performance.books.chapter03.common.ThreadTool;
import org.funtester.performance.books.chapter03.section3.ThreadTask;

import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 超市收银台性能测试用例
 */
public class SupermarketCheckoutTaskFirst extends ThreadTask {

    /**
     * 计价耗时统计
     */
    public static AtomicLong priceCostTime;

    /**
     * 支付耗时统计
     */
    public static AtomicLong payCostTime;

    /**
     * 打包耗时统计
     */
    public static AtomicLong packCostTime;

    /**
     * 构造方法
     * @param totalNum 执行的总次数
     */
    public SupermarketCheckoutTaskFirst(int totalNum) {
        this.totalNum = totalNum;
        this.costTime = new ArrayList<>(totalNum);
        priceCostTime = new AtomicLong();
        payCostTime = new AtomicLong();
        packCostTime = new AtomicLong();
    }

    /**
     * 业务操作,计价、支付、打包
     */
    @Override
    public void test() {
        long start = System.currentTimeMillis();
        price();
        long price = System.currentTimeMillis();
        priceCostTime.addAndGet(price - start);
        pay();
        long pay = System.currentTimeMillis();
        payCostTime.addAndGet(pay - price);
        pack();
        long pack = System.currentTimeMillis();
        packCostTime.addAndGet(pack - pay);
    }

    /**
     * 计价
     */
    public void price() {
        ThreadTool.sleep(10);
    }

    /**
     * 支付
     */
    public void pay() {
        ThreadTool.sleep((int) (System.currentTimeMillis() % 10000) / 100);
    }

    /**
     * 打包
     */
    public void pack() {
        ThreadTool.sleep(10);
    }
}

测试用例如下:

package org.funtester.performance.books.chapter03.section7;

import org.funtester.performance.books.chapter03.common.ThreadTool;
import org.funtester.performance.books.chapter03.section3.ThreadTask;
import org.funtester.performance.books.chapter03.section4.TaskExecutor;

import java.util.ArrayList;
import java.util.List;

/**
 * 超市收银台性能测试用例
 */
public class SupermarketCheckoutCase {

    public static void main(String[] args) throws InterruptedException {
        int total = 1000;
        List<ThreadTask> tasks = new ArrayList<>();
        for (int i = 0; i < 8; i++) {
            SupermarketCheckoutTaskFirst supermarketCheckoutTask = new SupermarketCheckoutTaskFirst(total);
            tasks.add(supermarketCheckoutTask);
        }
        TaskExecutor taskExecutor = new TaskExecutor(tasks, "超市收银台性能测试用例", 120);
        Thread thread = new Thread() {
            @Override
            public void run() {
                while (taskExecutor.realTimeKey) {
                    ThreadTool.sleep(1000);
                    long sumCost = TaskExecutor.realTimeCostTime.sumThenReset();
                    long sumTimes = TaskExecutor.realTimeCostTimes.sumThenReset();
                    System.out.println(String.format("实时统计TPS: %d, 平均耗时: %d", sumTimes, sumTimes == 0 ? 0 : sumCost / sumTimes));
                    long price = SupermarketCheckoutTaskFirst.priceCostTime.getAndSet(0);
                    long pay = SupermarketCheckoutTaskFirst.payCostTime.getAndSet(0);
                    long pack = SupermarketCheckoutTaskFirst.packCostTime.getAndSet(0);
                    System.out.println(String.format("实时统计各阶段耗时: price: %d, pay: %d, pack: %d", price / sumTimes, pay / sumTimes, pack / sumTimes));
                }
            }
        };
        taskExecutor.realTimeThread = thread;
        taskExecutor.start();
    }
}

控制台输出信息如下(省略了重复内容):

实时统计各阶段耗时: price: 11, pay: 51, pack: 11
实时统计TPS: 12, 平均耗时: 84
实时统计各阶段耗时: price: 10, pay: 52, pack: 11
实时统计TPS: 23, 平均耗时: 84
// 其他Rump-Up阶段信息省略
Rump-Up结束,开始执行测试任务!
实时统计TPS: 88, 平均耗时: 90
实时统计各阶段耗时: price: 11, pay: 67, pack: 11
实时统计TPS: 80, 平均耗时: 100
实时统计各阶段耗时: price: 11, pay: 78, pack: 11
任务执行完毕! 预期执行次数: 1000, 实际执行次数 1000, 错误次数 0, 耗时收集数量: 1000
// 此处省略相同多线程任务结束日志
测试TPS: 129, 平均耗时: 62
测试TPS: 129, 总执行次数: 8000
最小值:20
最大值:127
平均值:62
50分位值:57
90分位值:107
95分位值:116
99分位值:124
999分位值:126
任务执行完毕! 压测时长: 62 秒, 预期执行次数: 8000, 实际执行次数 8000, 错误次数 0, 耗时收集数量: 8000

通过这次测试,小八超市的收银台性能一目了然,接下来就是根据数据做决策了。正所谓 “磨刀不误砍柴工”,有了这些数据支持,小八的决策会更加科学合理。

FunTester 原创精华
【连载】从 Java 开始性能测试
故障测试与 Web 前端
服务端功能测试
性能测试专题
Java、Groovy、Go
白盒、工具、爬虫、UI 自动化
理论、感悟、视频
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册