3.3 多线程任务类

在线程模型中,首先创建固定数量的多线程任务,其次是把任务提交给线程池执行。因此,线程模型测试框架的核心之一就是多线程类。多线程类除了要执行测试任务以外,还需要收集、处理测试中的数据。

下面通过逐个功能的设计与实现,来拆解多线程类。

3.3.1 多线程实现方式

在第 1 章中我们讲到 Java 常用的两种多线程实现:继承 Thread 类或者实现 Runnable 接口。

这里我们选择实现 Runnable 接口,原因有如下几点:

除此以外,我们的多线程任务类还需要考虑拓展性,设计为 abstract 类。演示代码就不写了,后续功能会有相关的展示。

3.3.2 执行逻辑

在性能测试中,多线程任务类中有两种执行逻辑控制:

  1. 单个线程任务线程控制:控制单个任务的执行。
  2. 全局任务线程控制:控制全局任务的执行。

通常两者是相互关联的。某个线程出现一些异常(例如服务宕机返回 500 系列错误、数据异常导致压测无法继续进行),需要终止整个测试任务的执行;如果已经到达了预期目标(例如压测数据已经满足预期次数或者时间、准备的测试数据已经被消耗完无以为继),需要从全局视角结束整个测试任务。

最小颗粒度的控制开关,能够给我们后面的功能拓展提供良好的基础,不仅适用于静态模型启动和终止,而且在动态模型中也会大放异彩。

设计如下:

压测预期终止条件主流的有两种方式:限制测试次数限制测试时间。这里先通过次数限制类型终止条件来演示多线程任务类的实现。

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

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 多线程任务类
 */
public abstract class ThreadTask implements Runnable {

    /**
     * 全局是否终止测试任务开关
     */
    public static AtomicBoolean ABORT = new AtomicBoolean(false);

    /**
     * 是否终止单个任务执行开关
     */
    public boolean needStop = false;

    /**
     * 执行的总次数
     */
    public int totalNum;

    /**
     * 多线程任务执行方法
     */
    @Override
    public void run() {
        before(); // 前置处理
        while (true) {
            if (ABORT.get() || needStop) { // 判断是否终止测试任务
                break;
            }
            try {
                test(); // 测试方法
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        after(); // 后置处理
    }

    /**
     * 前置处理方法
     */
    public abstract void before();

    /**
     * 测试方法
     */
    public abstract void test();

    /**
     * 后置处理方法
     */
    public abstract void after();
}

以上就是一个简单的多线程任务类,后面其他任务类都会基于这个框架进行功能拓展。这里省去了构造方法,因为后面会给这个类增加其他功能和属性。

现在我们已经是踏出了性能测试引擎的第一步,下面继续探索性能测试引擎的其他领域。

3.3.3 测试数据处理

在性能测试中,对于本地的数据最重要的莫过于响应耗时,其次是执行信息(例如总次数、成功率、失败率、错误率等等)。这些信息通常都是由单个线程产生,在测试任务结束后汇总到一起,然后做分析处理。

响应耗时涉及到收集,那必定会用到集合类,又是列表数据类型,那么有些选择就是 java.util.List 实现类。到这里你可能会犹豫,是否需要选择前面第二章讲到的线程安全的类 java.util.Vector

其实在本小节第一段话中已经隐含了解决数据处理时线程安全问题的思路,就是通过将线程共享对象转换成线程独享,避免线程竞争。因为我们是线程模型,可以将同一个线程中的测试信息,通过线程不安全的类收集。在整个测试结束后,再使用线程安全的类将所有测试信息汇总到一起分析。

下面是性能测试中所需要收集的基本数据,以及在多线程任务类中的使用:

下面是基于上一节代码 ThreadTask 类演示,现在我们给 ThreadTask 类增加一些类对象属性:

/**
 * 执行次数
 */
public int executeNum = 0;

/**
 * 执行错误次数
 */
public int errorNum = 0;

/**
 * 用于记录所有请求时间
 */
public List<Integer> costTime;

然后我们在 run() 方法中,针对不同阶段的数据进行收集。

/**
 * 多线程任务执行方法
 */
@Override
public void run() {
    before(); // 前置处理
    while (true) {
        if (ABORT.get() || needStop || executeNum >= totalNum) { // 判断是否终止测试任务
            break;
        }
        try {
            executeNum++; // 记录执行次数
            long start = System.currentTimeMillis(); // 记录开始时间
            test(); // 测试方法
            long end = System.currentTimeMillis(); // 记录结束时间
            costTime.add((int) (end - start));
        } catch (Exception e) {
            errorNum++; // 记录错误次数
            e.printStackTrace();
        }
    }
    after(); // 后置处理
}

这样一来,在多线程任务类中的数据收集就已经完成了。前文提到失败率和错误率,一个指的是测试任务执行报错,一个指的测试任务执行完之后验证不通过。这里做了简化,对于新手来说,重点还是学习使用多线程编程知识,不必过度关注这些指标。后期能力到了,自然可以对这些功能拓展信手拈来,不在话下。

测试结束之后,数据汇总我们放在 after() 方法中实现,这里先贴一个伪代码。后面我们在多线程执行类中实现汇总功能时,会一起展示具体代码。

/**
 * 后置处理方法
 */
public void after() {
    // 此处汇总数据
}

这种方式可以统计到施压机本身的性能收集,即可以选择在测试结束后统一处理,也可以在测试过程中实时处理。也许你会问:大量的测试数据如果都在本地处理的话,会不会很消耗性能,影响测试任务的执行?

回答:是的。

在性能测试实战中,通常需要实时观察测试结果,这结果就是测试数据的展示。实时计算不仅会消耗更多资源,影响测试执行,还会导致压测引擎设计实现变得很复杂。所以笔者选择了相对简单的数据,只是记录了响应的耗时,执行计数这两个数据。还不带记录数据产生的时间戳。这些信息仅仅用来演示使用,如果实际工作中资源充足,数据量比较少,可以使用引擎自带的统计功能。若是遇到了无法满足的需求,那么又回到经典的回答:果断抛弃现在的方案,寻求更加简单、可靠的方案。

性能测试非常依赖完善的监控体系,完善的监控体系就是更好的解决方案。首先,我们可以通过监控实时看到被压服务的资源使用情况,服务数据可以更加准确描述该测试场景下,服务的性能表现;其次,现在流量的监控体系拥有非常好的数据处理能力,在性能测试引擎中只需要进行数据收集、上报即可。不仅能够降低引擎设计难度,解放引擎开发者,也减少了施压机的性能消耗,提升施压机所能提供压力的上限。受限于本人能力,这里不再对监控体系进行拓展。

书的名字:从 Java 开始做性能测试

如果本书内容对你有所帮助,希望各位不吝赞赏,让我可以贴补家用。赞赏两位数可以提前阅读未公开章节。我也会尝试制作本书的视频教程,包括必要的答疑。

FunTester 原创精华

【连载】从 Java 开始性能测试


↙↙↙阅读原文可查看相关链接,并与作者交流