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 自动化
理论、感悟、视频