在最近测试工作中,遇到了一些新的问题,也对自己的测试框架提出了新的需求,其中一个就是性能测试软启动的问题,还有一个就是高 QPS 提出新的挑战。
例如在固定线程模型中,我之前一般都是同时并发 N 个线程去发起性能测试。但是这种粗暴的方式还是比较硬,在大部分处理能力偏低的服务的时候是没啥问题的,压力不会一下子飙到非常高,由于线程固定的原因,初始时发出的请求消耗时间间隔内,就已经相对平稳了,本身 QPS 就不高也是重要原因之一。对于高 QPS 服务来讲,初始时请求的消耗时间非常短,在到达稳定期的时间内(虽然也非常短),但是发出的请求会非常多,对比非测试阶段就会突增现象非常明显。
所以,我抽时间做了一些软启动的功能初探,分享一下经验。
软启动概念
下面是百科定义:
电压由零慢慢提升到额定电压,电机启动的全过程都不存在冲击转矩,而是平滑的启动运行。这就是软启动。
还是跟物理相关的,印象中初中学的概念。
思路
固定线程模型
这个比较简单,实现方案可以参考JMeter
中的方案,就是在启动线程的时候增加一个线程启动间隔,这样就初步实现了。
这里我设置了一个常量:
/**
* 性能测试启动时间
*/
public static double RUNUP_TIME = 30.0;
复制代码
然后在com.funtester.frame.execute.Concurrent
类方法com.funtester.frame.execute.Concurrent#start
中启动代码增加一行:
startTime = Time.getTimeStamp();
for (int i = 0; i < threadNum; i++) {
ThreadBase thread = threads.get(i);
if (StringUtils.isBlank(thread.threadName)) thread.threadName = StatisticsUtil.getTrueName(desc) + i;
thread.setCountDownLatch(countDownLatch);
sleep(RUNUP_TIME / threadNum);
executorService.execute(thread);
}
shutdownService(executorService, countDownLatch);
endTime = Time.getTimeStamp();
复制代码
然后在之后的测试用例的时候,可以比较灵活的赋值:
Constant.RUNUP_TIME = util.getIntOrdefault(2, 30)
复制代码
固定 QPS 模型
这个处理没有找到现成的方案,我自己想了一个思路:取固定时间(默认 10s)线性拉伸到某个时间(默认 30s)执行,QPS 线程增加到设置的 QPS。例如设置 QPS 为 5000/s。在默认情况下,性能测试执行的过程中 QPS 由 1000QPS(5000/((30s / 10s) * 2 -1)),在 30s 内逐渐增加到 5000QPS,当然这只是一个粗略的估计。
interval = 1_000_000_000 / qps;//此处单位1s=1000ms,1ms=1000000ns
int runupTotal = qps * PREFIX_RUN;//计算总的请求量
double diffTime = 2 * (Constant.RUNUP_TIME / PREFIX_RUN * interval - interval);//计算最大时间间隔和最小时间间隔差值
double piece = diffTime / runupTotal;//计算每一次请求时间增量
for (int i = runupTotal; i > 0; i--) {
executorService.execute(threads.get(limit-- % queueLength).clone());
sleep((long) (interval + i * piece));
}
logger.info("预热完成,开始测试!");
复制代码
误差影响
因为最近也在研究性能测试的误差计算,也产出了一些文章,软启动当然也会对本地性能测试指标的计算还是有影响的。先分享一下之前的文章:
性能测试误差对比研究(二)还在我脑子里……
解决误差增大造成的影响思路的话,就是在预热系统完成之后,重置计数器中的各种数据。
PS:经过我实践发现,实际 QPS 更贴近于使用平均响应时间计算的值QPS
,而非QPS2
。
固定线程模型
这里我是思路就是先让软启动线程启动,然后暂停,清空各种计数器中的数据,然后继续进行全量并发测试。
for (int i = 0; i < threadNum; i++) {
ThreadBase thread = threads.get(i);
if (StringUtils.isBlank(thread.threadName)) thread.threadName = StatisticsUtil.getTrueName(desc) + i;
thread.setCountDownLatch(countDownLatch);
sleep(RUNUP_TIME / threadNum);
executorService.execute(thread);
}
sleep(1.0);
ThreadBase.stop();
try {
countDownLatch.await();
} catch (InterruptedException e) {
FailException.fail("软启动性能测试失败!");
}
threads.forEach(f -> f.initBase());
logger.info("预热完成,开始测试!");
countDownLatch = new CountDownLatch(threadNum);
复制代码
这里有个坑,就是中间必需sleep
1s,不然会因为最后 1 个线程启动时候调用before()
方法重置了private static boolean ABORT = false;
,导致一直都是false
。
其中com.funtester.base.constaint.ThreadBase#initBase
代码如下:
/**
* 用于对象拷贝之后,清空存储列表
*/
public void initBase() {
this.executeNum = 0;
this.errorNum = 0;
this.costs = new ArrayList<>();
this.marks = new ArrayList<>();
}
复制代码
固定 QPS 模型
这个相对来讲就非常简单了,因为直接控制任务发生器就行了。等所有任务发出之后,清空计数器即可,可能会有一些漏网之鱼,但是无伤大雅。
int runupTotal = qps * PREFIX_RUN;//计算总的请求量
double diffTime = 2 * (Constant.RUNUP_TIME / PREFIX_RUN * interval - interval);//计算最大时间间隔和最小时间间隔差值
double piece = diffTime / runupTotal;//计算每一次请求时间增量
for (int i = runupTotal; i > 0; i--) {
executorService.execute(threads.get(limit-- % queueLength).clone());
sleep((long) (interval + i * piece));
}
sleep(1.0);
allTimes = new Vector<>();
marks = new Vector<>();
executeTimes.getAndSet(0);
errorTimes.getAndSet(0);
logger.info("预热完成,开始测试!");
复制代码
PS:这里我并没有使用CyclicBarrier
和Phaser
,原因在于不同于性能测试中集合点和多阶段同步问题初探中提到的问题,在固定线程模型下,两次启动虽然在时间上连贯,但是并没有强关联性,使用这两个类可能带来其他问题。其实我是对这俩类用得少,掌握不深。
Have Fun ~ Tester !
FunTester,一群有趣的灵魂,腾讯云 &Boss 认证作者,GDevOps 官方合作媒体。
评论